diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index a900a1a41ad..00000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @Sofie-Automation/maintainers diff --git a/.github/workflows/audit.yaml b/.github/workflows/audit.yaml index f7a2653a8d6..e1135e3b9f1 100644 --- a/.github/workflows/audit.yaml +++ b/.github/workflows/audit.yaml @@ -4,6 +4,9 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: +permissions: + contents: read + jobs: validate-prod-core-dependencies: name: Validate Core production dependencies @@ -20,7 +23,7 @@ jobs: node-version-file: ".node-version" - uses: ./.github/actions/setup-meteor - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | meteor/node_modules @@ -50,7 +53,7 @@ jobs: node-version-file: ".node-version" - uses: ./.github/actions/setup-meteor - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | meteor/node_modules diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000000..ad9c1a294ed --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,89 @@ +name: Deploy Docs to GitHub Pages + +on: + push: + branches: + - main + # Review gh actions docs if you want to further define triggers, paths, etc + # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#on + +# Ensure we avoid any race conditions with rapid pushes to main +concurrency: + group: "Deploy to GitHub Pages" + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Build Docusaurus + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + fetch-depth: 0 + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version-file: ".node-version" + - uses: ./.github/actions/setup-meteor + - name: restore node_modules + uses: actions/cache@v5 + with: + path: | + packages/node_modules + key: ${{ runner.os }}-${{ hashFiles('packages/yarn.lock') }} + - name: Prepare Environment + run: | + corepack enable + + yarn config set cacheFolder /home/runner/publish-docs-cache + yarn install + + # setup zodern:types. No linters are setup, so this simply installs the packages + yarn meteor lint + + cd packages + yarn build:all + env: + CI: true + - name: Run docusaurus + run: | + cd packages/documentation + yarn docs:build + env: + CI: true + - name: Run typedoc + run: | + cd packages + yarn docs:typedoc + cp docs documentation/build/typedoc -R + env: + CI: true + + - name: Upload Build Artifact + uses: actions/upload-pages-artifact@v4 + with: + path: packages/documentation/build + + deploy: + name: Deploy to GitHub Pages + needs: build + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index cd09b51aafc..6ee0cfb6594 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -9,6 +9,9 @@ on: pull_request: workflow_dispatch: +permissions: + contents: read + jobs: lint-core: name: Typecheck and Lint Core @@ -24,7 +27,7 @@ jobs: node-version-file: ".node-version" - uses: ./.github/actions/setup-meteor - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | node_modules @@ -65,7 +68,7 @@ jobs: node-version-file: ".node-version" - uses: ./.github/actions/setup-meteor - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | node_modules @@ -102,6 +105,11 @@ jobs: name: Build Core and publish docker image runs-on: ubuntu-latest timeout-minutes: 30 + + permissions: + contents: read + packages: write + steps: - uses: actions/checkout@v6 with: @@ -112,7 +120,7 @@ jobs: node-version-file: ".node-version" - uses: ./.github/actions/setup-meteor - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | node_modules @@ -153,7 +161,7 @@ jobs: cd meteor/bundle/programs/server meteor npm install - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 # Check how the image should be built and pushed - name: Determine if images should be published to DockerHub @@ -188,7 +196,7 @@ jobs: # No-push build if no destination - name: Build without push if: steps.check-build-and-push.outputs.enable != 'true' - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: ./meteor/Dockerfile.circle @@ -199,7 +207,7 @@ jobs: - name: Get the Docker tag for GHCR id: ghcr-tag if: steps.check-build-and-push.outputs.enable == 'true' - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | ghcr.io/${{ github.repository }}-server-core @@ -210,14 +218,14 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} - name: Login to GitHub Container Registry if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push to GHCR if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: ./meteor/Dockerfile.circle @@ -231,7 +239,7 @@ jobs: - name: Get the Docker tag for DockerHub id: dockerhub-tag if: steps.check-build-and-push.outputs.enable == 'true' - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | ${{ secrets.DOCKERHUB_IMAGE_PREFIX }}server-core @@ -242,13 +250,13 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} - name: Login to DockerHub if: steps.check-build-and-push.outputs.enable == 'true' && steps.dockerhub.outputs.dockerhub-publish == '1' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push to DockerHub if: steps.check-build-and-push.outputs.enable == 'true' && steps.dockerhub.outputs.dockerhub-publish == '1' - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . file: ./meteor/Dockerfile.circle @@ -266,7 +274,7 @@ jobs: echo "image=$image" >> $GITHUB_OUTPUT - name: Trivy scanning if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.35.0 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: @@ -297,6 +305,10 @@ jobs: matrix: gateway-name: [playout-gateway, mos-gateway, "live-status-gateway"] + permissions: + contents: read + packages: write + steps: - uses: actions/checkout@v6 with: @@ -307,7 +319,7 @@ jobs: with: node-version-file: ".node-version" - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | packages/node_modules @@ -322,7 +334,7 @@ jobs: yarn run pinst --disable yarn workspaces focus ${{ matrix.gateway-name }} --production - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 # Check how the image should be built and pushed - name: Determine if images should be published to DockerHub @@ -357,7 +369,7 @@ jobs: # No-push build if no destination - name: Build without push if: steps.check-build-and-push.outputs.enable != 'true' - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: ./packages file: ./packages/${{ matrix.gateway-name }}/Dockerfile.circle @@ -368,7 +380,7 @@ jobs: - name: Get the Docker tag for GHCR id: ghcr-tag if: steps.check-build-and-push.outputs.enable == 'true' - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | ghcr.io/${{ github.repository }}-${{ matrix.gateway-name }} @@ -379,14 +391,14 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} - name: Login to GitHub Container Registry if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push to GHCR if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: ./packages file: ./packages/${{ matrix.gateway-name }}/Dockerfile.circle @@ -399,7 +411,7 @@ jobs: - name: Get the Docker tag for DockerHub id: dockerhub-tag if: steps.check-build-and-push.outputs.enable == 'true' - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | ${{ secrets.DOCKERHUB_IMAGE_PREFIX }}${{ matrix.gateway-name }} @@ -410,13 +422,13 @@ jobs: type=raw,value=latest,enable={{is_default_branch}} - name: Login to DockerHub if: steps.check-build-and-push.outputs.enable == 'true' && steps.dockerhub.outputs.dockerhub-publish == '1' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push to DockerHub if: steps.check-build-and-push.outputs.enable == 'true' && steps.dockerhub.outputs.dockerhub-publish == '1' - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: ./packages file: ./packages/${{ matrix.gateway-name }}/Dockerfile.circle @@ -434,7 +446,7 @@ jobs: echo "image=$image" >> $GITHUB_OUTPUT - name: Trivy scanning if: steps.check-build-and-push.outputs.enable == 'true' && steps.check-ghcr.outputs.enable == 'true' && steps.ghcr-tag.outputs.tags != 0 - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.35.0 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: @@ -455,29 +467,10 @@ jobs: echo $CODE_BLOCK >> $GITHUB_STEP_SUMMARY lint-packages: - name: Lint Package ${{ matrix.package-name }} + name: Lint Packages runs-on: ubuntu-latest timeout-minutes: 15 - strategy: - fail-fast: false - matrix: - package-name: - - blueprints-integration - - server-core-integration - - playout-gateway - - mos-gateway - - corelib - - shared-lib - - meteor-lib - - job-worker - - openapi - - live-status-gateway - - live-status-gateway-api - include: - - package-name: webui - tsconfig-name: tsconfig.json - steps: - uses: actions/checkout@v6 with: @@ -486,30 +479,32 @@ jobs: uses: actions/setup-node@v6 with: node-version-file: ".node-version" + - uses: ./.github/actions/setup-meteor - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | + node_modules + meteor/node_modules packages/node_modules - key: ${{ runner.os }}-${{ hashFiles('packages/yarn.lock') }} + key: ${{ runner.os }}-${{ hashFiles('yarn.lock', 'meteor/yarn.lock', 'meteor/.meteor/release', 'packages/yarn.lock') }} - name: Prepare Environment run: | corepack enable - cd packages - yarn config set cacheFolder /home/runner/${{ matrix.package-name }}-cache + yarn config set cacheFolder /home/runner/publish-docs-cache yarn install - if [ "${{ matrix.package-name }}" = "openapi" ]; then - yarn workspace @sofie-automation/openapi run build - else - yarn build:single ${{ matrix.package-name }}/${{ matrix.tsconfig-name || 'tsconfig.build.json' }} - fi + # setup zodern:types. No linters are setup, so this simply installs the packages + yarn meteor lint + + cd packages + yarn build:all env: CI: true - name: Run typecheck and linter run: | - cd packages/${{ matrix.package-name }} + cd packages yarn lint env: CI: true @@ -565,7 +560,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | packages/node_modules @@ -661,11 +656,12 @@ jobs: env: CI: true - publish-docs: - name: Publish Docs + build-docs: + name: Build Docs runs-on: ubuntu-latest timeout-minutes: 15 + # This is just to ensure the docs build, another job performs the build & publish steps: - uses: actions/checkout@v6 with: @@ -676,7 +672,7 @@ jobs: node-version-file: ".node-version" - uses: ./.github/actions/setup-meteor - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | packages/node_modules @@ -708,13 +704,6 @@ jobs: cp docs documentation/build/typedoc -R env: CI: true - - name: Publish - if: github.ref == 'refs/heads/main' # always publish for just the main branch - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./packages/documentation/build - force_orphan: true check-for-multiple-library-versions: name: Check for multiple library versions @@ -730,7 +719,7 @@ jobs: node-version-file: ".node-version" - uses: ./.github/actions/setup-meteor - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | node_modules diff --git a/.github/workflows/prune-container-images.yml b/.github/workflows/prune-container-images.yml index 9fccdf40905..a1fc9496fe6 100644 --- a/.github/workflows/prune-container-images.yml +++ b/.github/workflows/prune-container-images.yml @@ -5,6 +5,10 @@ on: schedule: - cron: "12 14 * * *" +permissions: + contents: read + packages: write + jobs: prune-container-images: if: ${{ github.repository_owner == 'Sofie-Automation' }} diff --git a/.github/workflows/prune-tags.yml b/.github/workflows/prune-tags.yml index 9bf477b288c..117c2c77007 100644 --- a/.github/workflows/prune-tags.yml +++ b/.github/workflows/prune-tags.yml @@ -14,6 +14,9 @@ on: schedule: - cron: "0 0 * * 0" +permissions: + contents: write + jobs: prune-tags: if: ${{ github.repository_owner == 'Sofie-Automation' }} diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index 71ea216fbfc..110c02e805f 100644 --- a/.github/workflows/publish-libs.yml +++ b/.github/workflows/publish-libs.yml @@ -43,7 +43,7 @@ jobs: fi lint-packages: - name: Lint Lib + name: Lint packages runs-on: ubuntu-latest continue-on-error: true timeout-minutes: 15 @@ -52,15 +52,6 @@ jobs: if: ${{ needs.check-publish.outputs.can-publish == '1' }} - strategy: - fail-fast: false - matrix: - package-name: - - blueprints-integration - - server-core-integration - - shared-lib - - live-status-gateway-api - steps: - uses: actions/checkout@v6 with: @@ -69,18 +60,24 @@ jobs: uses: actions/setup-node@v6 with: node-version-file: ".node-version" + - uses: ./.github/actions/setup-meteor - name: Prepare Environment run: | corepack enable - cd packages + yarn config set cacheFolder /home/runner/publish-docs-cache yarn install - yarn build:single ${{ matrix.package-name }}/tsconfig.build.json + + # setup zodern:types. No linters are setup, so this simply installs the packages + yarn meteor lint + + cd packages + yarn build:all env: CI: true - name: Run typecheck and linter run: | - cd packages/${{ matrix.package-name }} + cd packages yarn lint env: CI: true @@ -119,7 +116,12 @@ jobs: cd packages yarn install - yarn build:single ${{ matrix.package-name }}/tsconfig.build.json + + if [ "${{ matrix.package-name }}" = "openapi" ]; then + yarn workspace @sofie-automation/openapi run build + else + yarn build:single ${{ matrix.package-name }}/tsconfig.build.json + fi env: CI: true - name: Run tests @@ -146,12 +148,16 @@ jobs: uses: actions/setup-node@v6 with: node-version-file: ".node-version" + - uses: ./.github/actions/setup-meteor - name: Prepare Environment run: | corepack enable - cd packages - yarn install + yarn config set cacheFolder /home/runner/lint-core-cache + yarn + + # setup zodern:types. No linters are setup, so this simply installs the packages + yarn meteor lint env: CI: true - name: Bump version @@ -190,7 +196,7 @@ jobs: yarn install --no-immutable - name: Upload release artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: publish-dist path: | @@ -225,7 +231,7 @@ jobs: node-version-file: ".node-version" - name: Download release artifact - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v8 with: name: publish-dist @@ -270,4 +276,4 @@ jobs: echo "**Published:** $NEW_VERSION as $NPM_TAG" >> $GITHUB_STEP_SUMMARY env: NPM_CONFIG_PROVENANCE: true - CI: true + CI: true \ No newline at end of file diff --git a/.github/workflows/sonar.yaml b/.github/workflows/sonar.yaml index 91acb75503b..1753c637904 100644 --- a/.github/workflows/sonar.yaml +++ b/.github/workflows/sonar.yaml @@ -9,6 +9,9 @@ on: types: [opened, synchronize, reopened] workflow_dispatch: +permissions: + contents: read + name: SonarCloud analysis jobs: sonarqube: @@ -30,7 +33,7 @@ jobs: node-version-file: ".node-version" - uses: ./.github/actions/setup-meteor - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | node_modules @@ -51,6 +54,6 @@ jobs: env: CI: true - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v6 + uses: SonarSource/sonarqube-scan-action@v7 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 3f5379e7d22..00426058cbd 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -4,6 +4,10 @@ on: schedule: - cron: "0 10 * * 1" +permissions: + contents: read + packages: read + jobs: trivy: if: ${{ github.repository_owner == 'Sofie-Automation' }} @@ -17,7 +21,7 @@ jobs: steps: - name: Run Trivy vulnerability scanner (json) - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.35.0 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: @@ -26,7 +30,7 @@ jobs: output: "${{ matrix.image }}-trivy-scan-results.json" - name: Run Trivy vulnerability scanner (table) - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.35.0 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: @@ -44,7 +48,7 @@ jobs: echo $CODE_BLOCK >> $GITHUB_STEP_SUMMARY - name: Run Trivy in GitHub SBOM mode and submit results to Dependency Graph - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.35.0 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: diff --git a/.node-version b/.node-version index 442c7587a99..85e502778f6 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -22.20.0 +22.22.0 diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index 62331c2da38..8e1e72f9f1e 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,258 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [26.3.0-2](https://github.com/Sofie-Automation/sofie-core/compare/v26.3.0-1...v26.3.0-2) (2026-02-18) + + +### Bug Fixes + +* Add missing 'rootDir' to tsconfig ([20e7d12](https://github.com/Sofie-Automation/sofie-core/commit/20e7d12aabdf4c9cf36f8a271435fce8aa253c2d)) +* missed projection values when executing adlib action ([#1648](https://github.com/Sofie-Automation/sofie-core/issues/1648)) ([af8a7d1](https://github.com/Sofie-Automation/sofie-core/commit/af8a7d19accdc74d3cc5909b1afefd3efeeb755c)) + +## [26.3.0-1](https://github.com/Sofie-Automation/sofie-core/compare/v26.3.0-0...v26.3.0-1) (2026-02-11) + +## [26.3.0-0](https://github.com/Sofie-Automation/sofie-core/compare/v1.52.0...v26.3.0-0) (2026-02-04) + + +### Features + +* add ab session names to logging SOFIE-213 ([efb819e](https://github.com/Sofie-Automation/sofie-core/commit/efb819e5c63153c151aa3acd1aefaabca4500f26)) +* add autonext status to piece part counter ([5f4e24e](https://github.com/Sofie-Automation/sofie-core/commit/5f4e24e751aaee1795201d2c11bd28fddfb3a175)) +* add BlueprintAssetIcon component ([e05afd6](https://github.com/Sofie-Automation/sofie-core/commit/e05afd68386fbdcc7e21c23ef60f3f138048df78)) +* add flag to hide rundown header ([43c1aaa](https://github.com/Sofie-Automation/sofie-core/commit/43c1aaa0c0399615205d8562357f435660cd05da)) +* Add forms for presenter view and camera view options ([c08787d](https://github.com/Sofie-Automation/sofie-core/commit/c08787d6f592ca591a9282aa82a5402fab82f0ef)) +* add freeze color var and use general colors where applicable ([5bd486e](https://github.com/Sofie-Automation/sofie-core/commit/5bd486e781103cb1b8e6985d8c6ba2a37a2aea7b)) +* add getUpcomingParts method to OnSetAsNextContext ([#1577](https://github.com/Sofie-Automation/sofie-core/issues/1577)) ([aba5ed4](https://github.com/Sofie-Automation/sofie-core/commit/aba5ed42b51e7132c2d1c50878b260aa268989b3)) +* Add getUpcomingParts to action context ([#1524](https://github.com/Sofie-Automation/sofie-core/issues/1524)) ([0d1552d](https://github.com/Sofie-Automation/sofie-core/commit/0d1552dca9fc3f3dbaa94a8edb7f0f25c369f7dc)) +* add health endpoints to MOS- and Playout-Gateway ([5b590dd](https://github.com/Sofie-Automation/sofie-core/commit/5b590ddbaf86ee90d338837867a4d3bfc2e11c97)) +* add message for 2.stage scroll to log how often this happens. (it shouldn't happen at all) ([978e38b](https://github.com/Sofie-Automation/sofie-core/commit/978e38b10b292a8c1e951bdd10f0ea523c4ebbe2)) +* add object to timeline to trigger a regeneration at point in time ([ad450c3](https://github.com/Sofie-Automation/sofie-core/commit/ad450c39ceef5fcf3373905dd6a55adf4dd9cbb6)) +* add piece status to indicate invalid package container source SOFIE-2991 ([#14](https://github.com/Sofie-Automation/sofie-core/issues/14)) ([#1551](https://github.com/Sofie-Automation/sofie-core/issues/1551)) ([6b680d8](https://github.com/Sofie-Automation/sofie-core/commit/6b680d86f520fcc7874dae055ce59dec8bdb66ee)) +* Add prompter screen configuration form ([e92975c](https://github.com/Sofie-Automation/sofie-core/commit/e92975cdec2df5d33c3d41ff821c1eaa04e51364)) +* add resize support to virtual elements ([5333e7a](https://github.com/Sofie-Automation/sofie-core/commit/5333e7a6c0dcac925575054fa189e2cc39231ceb)) +* Add support for a multiline integer array form ([#1476](https://github.com/Sofie-Automation/sofie-core/issues/1476)) ([15f5aa2](https://github.com/Sofie-Automation/sofie-core/commit/15f5aa22b19fa73a1cbd041f4f56080052b4f482)) +* Add support for Gateway configuration from the studio API ([#1539](https://github.com/Sofie-Automation/sofie-core/issues/1539)) ([963542a](https://github.com/Sofie-Automation/sofie-core/commit/963542aa060f7db768d47a1d7e4e1f25367bb321)) +* Add XBox controller support including take button ([f22f8a2](https://github.com/Sofie-Automation/sofie-core/commit/f22f8a2ec230c548747531ad1af75c570c940b9a)) +* added singleton resize manager ([8f0b58c](https://github.com/Sofie-Automation/sofie-core/commit/8f0b58c84ea2338789fe6add0dbaf22398f2f27a)) +* AdjustLabelWidth, remove maxfontWidth as max is defined by default ([c2a4d32](https://github.com/Sofie-Automation/sofie-core/commit/c2a4d3292a5c186b56b6cbdc2dc18a100f3124bd)) +* allow adlib-actions to be marked as invalid ([#1609](https://github.com/Sofie-Automation/sofie-core/issues/1609)) ([6271ffd](https://github.com/Sofie-Automation/sofie-core/commit/6271ffd8bef5abe5691fa7b726209fc7d3758341)) +* allow part to be queued from onTake ([#1497](https://github.com/Sofie-Automation/sofie-core/issues/1497)) ([1a6619f](https://github.com/Sofie-Automation/sofie-core/commit/1a6619f42d1c7621faf10238edbcde646ef2eb33)) +* Allow restricting dragging to current part ([e9f66e7](https://github.com/Sofie-Automation/sofie-core/commit/e9f66e7e21e577822eb432f85f62c80770d5a5f2)) +* blueprint dev mode ([e896bdd](https://github.com/Sofie-Automation/sofie-core/commit/e896bdddcec42d84dd63e6eddd0b44f3936c2690)) +* **BlueprintAssetIcon:** support data urls ([1225a9e](https://github.com/Sofie-Automation/sofie-core/commit/1225a9e0ff836543d846cc97319372d48deff08c)) +* **blueprints-integration:** Add isRehearsal property to action contexts ([8d923a5](https://github.com/Sofie-Automation/sofie-core/commit/8d923a5e627ea50764eefa8cd2c345373c86453f)) +* change structure of ExpectedPackage documents ([9cd57af](https://github.com/Sofie-Automation/sofie-core/commit/9cd57af4d047d7632c61a537fefcc4ca4b3d31ea)) +* clean up dead code ([a93f8c2](https://github.com/Sofie-Automation/sofie-core/commit/a93f8c217a9d6631241a52d8e9bb3579dc7ee3b3)) +* cleanup media manager support ([#1509](https://github.com/Sofie-Automation/sofie-core/issues/1509)) ([76dfbd2](https://github.com/Sofie-Automation/sofie-core/commit/76dfbd2fa8cd18bda5713484c40e5bfe5c838529)) +* director screen initial commit ([8a83cf0](https://github.com/Sofie-Automation/sofie-core/commit/8a83cf0e831d040de0da6a6d21939e00f814d56b)) +* dynamic resize handler ([bcaa633](https://github.com/Sofie-Automation/sofie-core/commit/bcaa633025ef09af1f8bc7675f16be10922f49cb)) +* **EAV-111:** add current segment parts to LSG ([85fe434](https://github.com/Sofie-Automation/sofie-core/commit/85fe434b6642c690c7561e7b126df9717c064b37)) +* **EAV-296:** implement tally for device trigger previews ([3f21504](https://github.com/Sofie-Automation/sofie-core/commit/3f215046ccf8495e359383191f96913cfad5dd0d)) +* **EAV-487:** add buckets topic to LSG ([513c048](https://github.com/Sofie-Automation/sofie-core/commit/513c04863f84bd56cbfff16e7fd0e167054eac5f)) +* **EAV-488:** add packages topic to LSG ([5308430](https://github.com/Sofie-Automation/sofie-core/commit/5308430233d606a7e237eb6b66bf5119be6c35df)) +* **EAV-603:** add `manuallySelected` to OnSetAsNextContext ([ec1114e](https://github.com/Sofie-Automation/sofie-core/commit/ec1114e99c77bd395cf69912e92527d91afcc845)) +* edit mode for drag operations ([4347c6a](https://github.com/Sofie-Automation/sofie-core/commit/4347c6ad0762ed5081c377aa92841bebfb5800c6)) +* enable support for tsr plugins ([51a2379](https://github.com/Sofie-Automation/sofie-core/commit/51a237969092deda4972734e04e2aea01b78fe5a)) +* expose getSegment in blueprint context ([e727028](https://github.com/Sofie-Automation/sofie-core/commit/e7270281ccd3cde2ac6490f34055f039cf24404a)) +* expose persistent playout store to more methods ([ab7c6bc](https://github.com/Sofie-Automation/sofie-core/commit/ab7c6bc116b768dd030c9160a90554db37880762)) +* GW config types in Blueprints ([c8e669f](https://github.com/Sofie-Automation/sofie-core/commit/c8e669f333010cc88930d1684bd2d2795104cc88)) +* implement Bucket Panel Icon ([fbcc6e8](https://github.com/Sofie-Automation/sofie-core/commit/fbcc6e8eeb780b24f7595b5386e729ea9d1dda9a)) +* improve ab notifications SOFIE-207 ([efb9c42](https://github.com/Sofie-Automation/sofie-core/commit/efb9c4224add897528661aff8d2420c1574a6311)) +* limit the system to have a single studio [#1450](https://github.com/Sofie-Automation/sofie-core/issues/1450) ([#1534](https://github.com/Sofie-Automation/sofie-core/issues/1534)) ([38439f9](https://github.com/Sofie-Automation/sofie-core/commit/38439f96dd68ce3d1e3f4878026711caceb7aeaa)) +* List available studio views at /countdowns/[studioID] ([75d49e0](https://github.com/Sofie-Automation/sofie-core/commit/75d49e0f9c060fb6afee307578149a8c379b04e9)) +* live status gateway type generation SOFIE-188 ([#24](https://github.com/Sofie-Automation/sofie-core/issues/24)) ([b3ee84e](https://github.com/Sofie-Automation/sofie-core/commit/b3ee84e69b88c9605ba42543a1262d5dff31d619)) +* lower delay before scrollstart ([3bdcaa8](https://github.com/Sofie-Automation/sofie-core/commit/3bdcaa8468c5fcc4e415e85ad364fa7d419e0169)) +* **lsg:** add notification support to LSG ([0c6692c](https://github.com/Sofie-Automation/sofie-core/commit/0c6692cc9ca5d1d733607533f3446811651d8755)) +* **LSG:** sort buckets and their adlibs ([3e74c66](https://github.com/Sofie-Automation/sofie-core/commit/3e74c66fc1168215b117da89e44d762f028a6f3b)) +* make Video previews larger ([#1499](https://github.com/Sofie-Automation/sofie-core/issues/1499)) ([518977c](https://github.com/Sofie-Automation/sofie-core/commit/518977c621d7eb35b3e4fd4681b70522476a8b03)) +* mini shelfview ([0bad4dd](https://github.com/Sofie-Automation/sofie-core/commit/0bad4dde8f2b98c4f4bb741307e23ea6636a7572)) +* missed graphicsInputIcon in last commit ([0a87149](https://github.com/Sofie-Automation/sofie-core/commit/0a87149d18cc79afb0fabb3a0fb1ee08b9723a50)) +* more WIP ([e49eb6a](https://github.com/Sofie-Automation/sofie-core/commit/e49eb6a797aaa864e2ed5c8badddaabc36927ef1)) +* mos status flow rework ([#1356](https://github.com/Sofie-Automation/sofie-core/issues/1356)) ([672f2bd](https://github.com/Sofie-Automation/sofie-core/commit/672f2bd2873ae306db9dfcbbc3064fdcc9ea1cd0)) +* move GW config types to generated in shared lib ([f54d9ca](https://github.com/Sofie-Automation/sofie-core/commit/f54d9ca63bc00a05915aac45e0be5b595c980567)) +* optional studioLabelShort for presenters view ([cf62762](https://github.com/Sofie-Automation/sofie-core/commit/cf6276289b3bc47df3635b34ca75994ccc37713b)) +* PieceGeneric type - optional nameShort and nameTruncated ([c7d87a7](https://github.com/Sofie-Automation/sofie-core/commit/c7d87a7b463a4dbb546e967f87620badedfd0046)) +* prepare for dynamic resize observers based on inView ([16f3027](https://github.com/Sofie-Automation/sofie-core/commit/16f30273d0c84aa378e00fd59886369957a63f1b)) +* **PreviewPopUpContext:** convertSourceLayerItemToPreview set preview to large for Videos ([7001d6d](https://github.com/Sofie-Automation/sofie-core/commit/7001d6d572a200bcdb0e0773d5e6fdbd8dc38f24)) +* remove maxFont width as it's currently not used ([56d77aa](https://github.com/Sofie-Automation/sofie-core/commit/56d77aab2ffb685b1df7e86951e0d2fc90d59646)) +* remove remnants of 'organisations' ([#1535](https://github.com/Sofie-Automation/sofie-core/issues/1535)) ([de8774a](https://github.com/Sofie-Automation/sofie-core/commit/de8774a9c3bf7829fa9bc4311e6595a0e3e30f42)) +* replace `wasActive` in onRundownActivate with context ([#1514](https://github.com/Sofie-Automation/sofie-core/issues/1514)) ([007a9da](https://github.com/Sofie-Automation/sofie-core/commit/007a9da74583702b347c613e5aed8514422d5c3d)) +* replace builtin clientside mongodb writes with custom method ([b282691](https://github.com/Sofie-Automation/sofie-core/commit/b282691f82402b7b9a055e2342340fcfd8b4f0f8)) +* replace deprecated mongodb fields with projection ([00cca86](https://github.com/Sofie-Automation/sofie-core/commit/00cca86bcbc4df5191efdcf95558981e0736a647)) +* replace origo with react-bootstrap ([d7ca0ed](https://github.com/Sofie-Automation/sofie-core/commit/d7ca0ed9783130e41ab3c489e2d73f466cda63fe)) +* retime piece user action ([385e884](https://github.com/Sofie-Automation/sofie-core/commit/385e884e8f3f9d1165fcfa06af649d5af951b516)) +* rework ExpectedPackages generation/management to add PieceInstances as owners to existing docs ([3fbd39c](https://github.com/Sofie-Automation/sofie-core/commit/3fbd39c929abc3fa3612fe3d8ce48968299a7c6a)) +* rework ExpectedPackages generation/management to share documents within rundown/bucket ([45fc8f2](https://github.com/Sofie-Automation/sofie-core/commit/45fc8f2c5e1cdd71ac3c4db282bfd8d820a61bc0)) +* rework ExpectedPackages generation/management to share packages between ingest and playout ([03346be](https://github.com/Sofie-Automation/sofie-core/commit/03346be59a3dab697513c5428ac5cec665c4a368)) +* Set sub-device peripheralDeviceId from deviceOptions parentDeviceName ([#1505](https://github.com/Sofie-Automation/sofie-core/issues/1505)) ([4d34cec](https://github.com/Sofie-Automation/sofie-core/commit/4d34cecac83929d999b088423f98fd9b787c0c31)) +* show screen name in screen-saver ([893cd9a](https://github.com/Sofie-Automation/sofie-core/commit/893cd9aa27c03119b652a282e5447455b1565636)) +* simplify size measure and inititaly use default height - prepare resize ([11b3077](https://github.com/Sofie-Automation/sofie-core/commit/11b30775c0691c5887764318ae0b0251ec19be5d)) +* simplify VirtualElemt to avoid racecondition between useEffect and useLayoutEffect. When using the 'contain: 'size layout' option, a static placeholder is fine Chrome ([ba24157](https://github.com/Sofie-Automation/sofie-core/commit/ba2415713ebae29042ccc5d77d406c581ac18afb)) +* Styling on PieceIcons ([38846fe](https://github.com/Sofie-Automation/sofie-core/commit/38846fe96ff31fc68193bf36f53bc680894d5cd1)) +* support custom types from tsr plugins ([#1585](https://github.com/Sofie-Automation/sofie-core/issues/1585)) ([3bae757](https://github.com/Sofie-Automation/sofie-core/commit/3bae7576ede0e2f71cf9882e6f2c1ac5589d9b63)) +* support hosting sofie under subdirectory SOFIE-94 ([#48](https://github.com/Sofie-Automation/sofie-core/issues/48)) ([2dbf81f](https://github.com/Sofie-Automation/sofie-core/commit/2dbf81f617af3de7c5149c915971f5b3ece50988)) +* testtool - show AB-Session in Timeline ([05471e3](https://github.com/Sofie-Automation/sofie-core/commit/05471e3ddce14862bb96fb15adc4b9c2e9f2ff99)) +* time of day pieces ([#1406](https://github.com/Sofie-Automation/sofie-core/issues/1406)) ([2500780](https://github.com/Sofie-Automation/sofie-core/commit/25007807845e03e92c17e623c159611f89703672)) +* UI - presenter timing counter remaing part/segment ([fe1c159](https://github.com/Sofie-Automation/sofie-core/commit/fe1c159ebff48a2873d9e662bb46be5fbd8d17b7)) +* UI - presenter timing only use PartOrSegmentRemaining if type is SEGMENT_BUDGET_DURATION ([e217db1](https://github.com/Sofie-Automation/sofie-core/commit/e217db10bd7b835a761bf8c0f570bbc1772cb896)) +* **UI Schema:** ui:displayType bread-crumbs ([e5cd51e](https://github.com/Sofie-Automation/sofie-core/commit/e5cd51e7a3b7da43b6e5cce506d8942da58745c7)) +* unify Piece Icons styling and handle empty vs undefined abbreviation ([3c4a4fa](https://github.com/Sofie-Automation/sofie-core/commit/3c4a4faaf0b32f315bbfa8739301dcba7857baab)) +* update meteor to 3.3.2 ([#1529](https://github.com/Sofie-Automation/sofie-core/issues/1529)) ([9bd232e](https://github.com/Sofie-Automation/sofie-core/commit/9bd232e8f0561a46db8cc6143c5353d7fa531206)) +* useLetterSpacing option (default false) and static opticalfontSize option (default 120) ([060b2ac](https://github.com/Sofie-Automation/sofie-core/commit/060b2acc18e1a582d1fa09b9107c123ad611f8ca)) +* WIP ([ceb338f](https://github.com/Sofie-Automation/sofie-core/commit/ceb338f6f1ff9a6582088dd1dae2f36021ca24b4)) +* WIP ([e9491bb](https://github.com/Sofie-Automation/sofie-core/commit/e9491bb8a8af57b822abc8bbee72366cd266661d)) +* wrap text on mini shelf buttons ([353950f](https://github.com/Sofie-Automation/sofie-core/commit/353950f4891106b4db761288efa02dca63bab008)) + + +### Bug Fixes + +* timer active was lost - using useRef for timer reference ([b386d29](https://github.com/Sofie-Automation/sofie-core/commit/b386d29e50dcec0f7a1a7d098566d025f7f7ea34)) +* `PeripheralDevice.configManifest` is an optional field ([c61bec6](https://github.com/Sofie-Automation/sofie-core/commit/c61bec64b286e3c2daa5cd54c40ce4035f20f9c0)) +* abreviation should be used even if it's an empty string ([2e9ff13](https://github.com/Sofie-Automation/sofie-core/commit/2e9ff13db3e56a82c21d1b3688cd3bdb90b43818)) +* add "presenter's screen" label to it's screensaver ([c458989](https://github.com/Sofie-Automation/sofie-core/commit/c4589898ff4a3a8c05e72c7de07d877b4103992c)) +* add `getCurrentTime` to `SyncIngestUpdateToPartInstanceContext` ([ccbdd3c](https://github.com/Sofie-Automation/sofie-core/commit/ccbdd3cc6830cff6aadec202432e8116ea5f4e50)) +* add delay on extra check when scrolling long list ([7b5491f](https://github.com/Sofie-Automation/sofie-core/commit/7b5491f9f8499c51f5b9d69080fb36e04df8717f)) +* add dependency to useEffect for onSetEditMode ([5e0a795](https://github.com/Sofie-Automation/sofie-core/commit/5e0a795954425492e43842f8011941ecaade4b90)) +* add initial measurement of elements in view, to ensure correct size after load ([325873a](https://github.com/Sofie-Automation/sofie-core/commit/325873a50605dee8cf1543183791662c2d96107a)) +* Add missing 'part' to useCallback dependency array ([3c79398](https://github.com/Sofie-Automation/sofie-core/commit/3c79398e5817b4d13e1fe9b69f694eaf893c9b0c)) +* Add missing imports ([e0ef880](https://github.com/Sofie-Automation/sofie-core/commit/e0ef88094a4d34fa27b44641919f20579371a047)) +* add mutation observer for resizes on activate and on take ([cef0db0](https://github.com/Sofie-Automation/sofie-core/commit/cef0db05b6edc4303cf5a96a70287745c4b2c359)) +* add plannedStartedPlayback and plannedStoppedPlayback to IBlueprintPartInstanceTimings interface ([#1515](https://github.com/Sofie-Automation/sofie-core/issues/1515)) ([9e8ee71](https://github.com/Sofie-Automation/sofie-core/commit/9e8ee71863a8b00be521a2325b2375f03a32956c)) +* add position interval check to ensure that all elements in view are visible ([449f5ef](https://github.com/Sofie-Automation/sofie-core/commit/449f5ef42fb52bb98c980541d11dcb093e197d7d)) +* add precalculated measurement ([b20f677](https://github.com/Sofie-Automation/sofie-core/commit/b20f6772bc5197c15221fc22b4b2fe11ba4235a4)) +* add small delay to ensure nextPartInfo is ready prior to scroll ([07ed44a](https://github.com/Sofie-Automation/sofie-core/commit/07ed44a8db181cb5a3431d90d4f43c2fd868149b)) +* adjust padding of rundown list ([7834c88](https://github.com/Sofie-Automation/sofie-core/commit/7834c88aaab107bc8f2ae1448c22bab40e31c10c)) +* after bootstrap was added the scrollBy(x, y) was using smooth scroll. using scrollBy(top: y, behaviour: instant) solves that problem ([43e25f0](https://github.com/Sofie-Automation/sofie-core/commit/43e25f0a0570742e491f6e3c7a8169514cfb4bc9)) +* **AfterBroadcastForm:** shouldDeactivateRundown should be true when loop is _not_ running ([#1504](https://github.com/Sofie-Automation/sofie-core/issues/1504)) ([1d6a22e](https://github.com/Sofie-Automation/sofie-core/commit/1d6a22e64dd0f72131852c50b0008843ec38792a)) +* allow bucketId to be null in bucketAdLibActions pub ([723b0ac](https://github.com/Sofie-Automation/sofie-core/commit/723b0acfac5f4abbecf186c39ecdae8131c2503c)) +* bad header-clear merge ([fbdecca](https://github.com/Sofie-Automation/sofie-core/commit/fbdeccacfe28433404d5e1937eb6f28aa5c9fd20)) +* **Base64ImageInput:** component uploads contents instead of a data: url ([2719444](https://github.com/Sofie-Automation/sofie-core/commit/2719444ee056302cd3c8cd5f77b14a1412e6a690)) +* better JSON parsing, serialization for UserErrors ([9cf4b58](https://github.com/Sofie-Automation/sofie-core/commit/9cf4b586025de81471542c44e260e1f835d3612b)) +* **BlueprintAssetIcon:** data URLs have null origin ([b8a586b](https://github.com/Sofie-Automation/sofie-core/commit/b8a586bc575a219231fcde96e6430b4a7fefd501)) +* broken system settings ([e7322ca](https://github.com/Sofie-Automation/sofie-core/commit/e7322caf34ba156126aa943f802c672b7027db3f)) +* buckets gone from the UI ([6e12eff](https://github.com/Sofie-Automation/sofie-core/commit/6e12eff38b7ecf2e2179e62f80bfce362ad6e3d7)) +* Clean up gamepad event listeners in destroy() ([177faef](https://github.com/Sofie-Automation/sofie-core/commit/177faef34af5eb168f8b55ba183ec7b95fae361b)) +* clean up some more properties-grid buttons ([a9a65a4](https://github.com/Sofie-Automation/sofie-core/commit/a9a65a4c354e67a0739b640926d4faa04418d971)) +* clean up white-spaces ([12e83c3](https://github.com/Sofie-Automation/sofie-core/commit/12e83c354d9511421a903b7df001117d47355b92)) +* cleanup after pieceInstancesLiveQuery ([95b1187](https://github.com/Sofie-Automation/sofie-core/commit/95b11871c60ab10ca1d570640fb8e0e091829957)) +* cleanup pendingFirstStagetimeout ([da7f9b3](https://github.com/Sofie-Automation/sofie-core/commit/da7f9b3a93fb6bdf3d0d90569fa336d8f88575ea)) +* **core-integration:** use setMaxListeners on CoreConnection to avoid MaxListenersExceededWarning message ([a02ef23](https://github.com/Sofie-Automation/sofie-core/commit/a02ef236b8a396847bc467ccd5f459a0862e6abe)) +* correct height of VirtualElements ([bfa84e9](https://github.com/Sofie-Automation/sofie-core/commit/bfa84e90ca2ccc987a0fba0b048374775e53ee75)) +* css adjustments ([5bed05b](https://github.com/Sofie-Automation/sofie-core/commit/5bed05ba12b95c1932d99a8ad082329310072cf9)) +* css adjustments ([45b96f1](https://github.com/Sofie-Automation/sofie-core/commit/45b96f15e70def0798cc34974e4d63bbc531636e)) +* css adjustments ([1209084](https://github.com/Sofie-Automation/sofie-core/commit/120908412cb87efa63d8c593bb9b686706b44048)) +* css adjustments ([b40b4e9](https://github.com/Sofie-Automation/sofie-core/commit/b40b4e980d913313a162872f02beeb6b300b058e)) +* css adjustments ([93abc5c](https://github.com/Sofie-Automation/sofie-core/commit/93abc5c8c40c48db8da2daf858d14d24cadc3ba1)) +* direction rtl would move any dots from beginning of string to end of string. ([6fece09](https://github.com/Sofie-Automation/sofie-core/commit/6fece09f6622d089c96030997731a826da0640b1)) +* Directors screen - colors in livespeak split was not hardcoded ([310c73c](https://github.com/Sofie-Automation/sofie-core/commit/310c73c22748dfa397661e564a7b0049c91d5818)) +* disable some null subscriptions ([#15](https://github.com/Sofie-Automation/sofie-core/issues/15)) ([#1571](https://github.com/Sofie-Automation/sofie-core/issues/1571)) ([8c199ef](https://github.com/Sofie-Automation/sofie-core/commit/8c199ef79c4dde39a46a61ba0876ac5be66adf73)) +* do not interpolate translation on user controlled strings ([e8410da](https://github.com/Sofie-Automation/sofie-core/commit/e8410da1b3ee02deabd8b2349f3903386416846c)) +* do not override existing error codes when no code is specified ([f2cd97b](https://github.com/Sofie-Automation/sofie-core/commit/f2cd97b818109cdf00cc57e9f1d2749bca1f91d2)) +* docker images using CMD instead of ENTRYPOINT ([e1beb6e](https://github.com/Sofie-Automation/sofie-core/commit/e1beb6e082c7c9ce4a8009feceb58eb7ef89f308)) +* don't expose viewPortScrollingState use getViewPortScrollingState() instead ([b3292e3](https://github.com/Sofie-Automation/sofie-core/commit/b3292e3b62f5e3aeaec695dc92a6361ade2c720c)) +* don't hide global adlibs from hidden sourceLayers ([4396665](https://github.com/Sofie-Automation/sofie-core/commit/43966658a12fc3a44771903bd4709ef4f2811c82)) +* Don’t keep history on gh-pages branch ([4763743](https://github.com/Sofie-Automation/sofie-core/commit/47637432a7fd4d4c50eba68234e6eb80759ffb74)) +* **EAV-372:** settings lost on studio update ([#1455](https://github.com/Sofie-Automation/sofie-core/issues/1455)) ([794fc9e](https://github.com/Sofie-Automation/sofie-core/commit/794fc9ed56b2aca092f2f9b8991226e46f204646)) +* **EAV-450:** missing null activePlaylist update when playlist gets deactivated ([4d991aa](https://github.com/Sofie-Automation/sofie-core/commit/4d991aa3fbe917e68a3f00976f575d3d71a74d47)) +* enable in out words in new VT previews ([d445b33](https://github.com/Sofie-Automation/sofie-core/commit/d445b3382474f3854cd10f43287f00182b797f78)) +* enforce element visibility if resizing while scrolling ([57e4a8f](https://github.com/Sofie-Automation/sofie-core/commit/57e4a8f32275a622faf9fb98ef7c770d8242504b)) +* ensure 2.stage is not ran until virtualelement has been updated (fix if more than 1 segment is invalid) ([2b60c8d](https://github.com/Sofie-Automation/sofie-core/commit/2b60c8d79e54e8d92016022bf5d1caf406382016)) +* ensure elements in view are always visible ([1d50f35](https://github.com/Sofie-Automation/sofie-core/commit/1d50f355aa71088f9c4755a16213cba56d1c3d15)) +* ensure the previousPartInstnace is cleaned up when belonging to a Rundown being removed from the playlist ([4d04bef](https://github.com/Sofie-Automation/sofie-core/commit/4d04bef35b26bf301041c3ee37195dee7621f336)) +* error messages returned by the api ([6842226](https://github.com/Sofie-Automation/sofie-core/commit/6842226281484c22e677a0b2ea3b8c2466bc0cef)) +* eventlistener on segmentBlock wasn't cleaned up ([046eeb0](https://github.com/Sofie-Automation/sofie-core/commit/046eeb049a02623cada70b02a99e686de9ada25b)) +* findMarkerPosition always needs all parts available ([a52209e](https://github.com/Sofie-Automation/sofie-core/commit/a52209e8e6916c503708bb32dee25e242f636cd4)) +* Fix formatting of release53 branch ([a7dff50](https://github.com/Sofie-Automation/sofie-core/commit/a7dff504347a754ab87106500a23ada8ffb20e10)) +* fix logic for calculating "source missing" warning message ([e9ed43d](https://github.com/Sofie-Automation/sofie-core/commit/e9ed43d00cbb506edeb02d12b051b26bd78edde4)) +* generate type for upstreal/release53 ([1c12694](https://github.com/Sofie-Automation/sofie-core/commit/1c126940032921a7e36bfca2807114856693e9c3)) +* hashObj not handling null values ([62e5e50](https://github.com/Sofie-Automation/sofie-core/commit/62e5e507d20438366050c335b6c1c27b709ac992)) +* hot standby was not refering to it's full name ([28b9ce1](https://github.com/Sofie-Automation/sofie-core/commit/28b9ce1893f5f495f2798123180bae9ed35cd8f2)) +* If an infinite pieceinstance has no package statuses available, try using them from the previous pieceinstance instead ([cd0cb1f](https://github.com/Sofie-Automation/sofie-core/commit/cd0cb1f9cb6a293601c55f32a756747778f8a861)) +* ignore invalid partInstances during syncChangesToPartInstances ([ce586bc](https://github.com/Sofie-Automation/sofie-core/commit/ce586bcb9bdca0526d5c7ab69a6cedb0960ce1e2)) +* Improve clock accuracy ([964ef70](https://github.com/Sofie-Automation/sofie-core/commit/964ef705689f3b587c900a285e54c432b70f6524)) +* improve error messaging when uploading blueprints ([#1568](https://github.com/Sofie-Automation/sofie-core/issues/1568)) ([bf677a9](https://github.com/Sofie-Automation/sofie-core/commit/bf677a92a057eeeda1227c66ee17fb34a8fc860c)) +* In-Out words was placed and styled wrong ([c8f1974](https://github.com/Sofie-Automation/sofie-core/commit/c8f19743807b6d3059469ab570064c11d3122c32)) +* ingest parts not being updated when rank changes ([aee51ea](https://github.com/Sofie-Automation/sofie-core/commit/aee51ea2fea948539185f0cfa652406415bd2a6e)) +* keyboard naviagation UX improvement ([ed66237](https://github.com/Sofie-Automation/sofie-core/commit/ed662371694aac391d60e7c3c8b740f62e238ffe)) +* let UI settle before testing for segment being in view ([01f1699](https://github.com/Sofie-Automation/sofie-core/commit/01f1699fffa78e4da545e5acdb7202d23f06323d)) +* limit part/piece title size to max 120 ([7d43807](https://github.com/Sofie-Automation/sofie-core/commit/7d43807908628dbefa3637b5e1a06604ce99f62d)) +* lint ([ce33333](https://github.com/Sofie-Automation/sofie-core/commit/ce333335a9b06c1b359358bf3ddabe35a7d26e67)) +* live speak and remote speak align split to base of font ([02b7a01](https://github.com/Sofie-Automation/sofie-core/commit/02b7a01fcc599c3004f995358a770ae12620af36)) +* lower time for UI settle before changing isVisible ([256b0b0](https://github.com/Sofie-Automation/sofie-core/commit/256b0b0f3dc98e66bc210793182ea633392721e3)) +* **LSG:** don't return null for `packageName` ([3edfcd8](https://github.com/Sofie-Automation/sofie-core/commit/3edfcd8b306044f5a1aa09b527ae8477f83ac367)) +* **LSG:** expose package status as custom enum ([4e91099](https://github.com/Sofie-Automation/sofie-core/commit/4e91099ac407e3630e7f1eade35955777b89b947)) +* maintainFocusOnPartInstance race condition ([e372cb7](https://github.com/Sofie-Automation/sofie-core/commit/e372cb7b215ca4056c3d1083509d032911b3364e)) +* Match exact paths for countdown routes and add 404 page ([bccefe4](https://github.com/Sofie-Automation/sofie-core/commit/bccefe475b0d994d6910379744bb2284c0c76b83)) +* Memoryleak fixed in @jstarpl/react-contextmenu 2.15.1 ([49778d5](https://github.com/Sofie-Automation/sofie-core/commit/49778d587e3d9c4d2a9253afc01b7eef1cb47abd)) +* memoryleaks in hoverpreviews ([e16308b](https://github.com/Sofie-Automation/sofie-core/commit/e16308b8a505fc63182f2c747e8122d3d57fda19)) +* missing await of promise ([51b69f9](https://github.com/Sofie-Automation/sofie-core/commit/51b69f9f257281fe04c6885ba0a24167ab1ebf15)) +* missing export ([7956f7b](https://github.com/Sofie-Automation/sofie-core/commit/7956f7bba509d892389bb3c564312da730c0495b)) +* more accurate initial heigth for VirtualElements + fallback fix in VirtualElement ([f251cd4](https://github.com/Sofie-Automation/sofie-core/commit/f251cd4e1f94483cf4a93981bba782313f37ea25)) +* **mountedTriggers:** documents quickly being added and removed can cause non-existent documents in the publication to be removed ([e38a9cb](https://github.com/Sofie-Automation/sofie-core/commit/e38a9cba5ed7cd40066bd4d43e518eeeb1805394)) +* next piece titel had wrong default size ([f24adbd](https://github.com/Sofie-Automation/sofie-core/commit/f24adbd9d48ef132b97aa7a3e334170be5797dd8)) +* on air button could disappear permanently when scrolling just after the on air button is clicked ([e30dd08](https://github.com/Sofie-Automation/sofie-core/commit/e30dd089357b5e64f25e5a0e2a85760f5ca277f5)) +* parent device settings confusing use of config id ([#1596](https://github.com/Sofie-Automation/sofie-core/issues/1596)) ([6fb736b](https://github.com/Sofie-Automation/sofie-core/commit/6fb736b07ade7f150ecb7ab67415cad8e9765bfe)) +* **PGW:** handle situation when device is not initialized yet ([6060e7e](https://github.com/Sofie-Automation/sofie-core/commit/6060e7e2645dfbc19fde263de35f545d9200e02c)) +* piece icon cam squashed ([283dfb5](https://github.com/Sofie-Automation/sofie-core/commit/283dfb5866a24fc660940e2c132a3270bf771c34)) +* piece-part title after upstreammerge ([776725a](https://github.com/Sofie-Automation/sofie-core/commit/776725aec90742b556f24cb3c2bbd9983099daaa)) +* PieceIcons layout, RundownView loading spinner ([ed98911](https://github.com/Sofie-Automation/sofie-core/commit/ed9891133c8e975f8c8b0b67c1be7b7df28cfd9b)) +* playlistId can be optional ([c1cdf87](https://github.com/Sofie-Automation/sofie-core/commit/c1cdf87c2d8fc3be542245b016786c409f2ee2f9)) +* **Presenter Screen:** Diff is showing incorrect values ([#1491](https://github.com/Sofie-Automation/sofie-core/issues/1491)) ([bf84734](https://github.com/Sofie-Automation/sofie-core/commit/bf84734d4dba07c5ba8e765416aa21e599bb530b)) +* Presenters Screen align icon text with label ([d10d85d](https://github.com/Sofie-Automation/sofie-core/commit/d10d85d0a72fcfa106477e9cd68a5baf5fb3307d)) +* prevent event propagation on Enter ([3e23880](https://github.com/Sofie-Automation/sofie-core/commit/3e2388065d56044d26076ef36e07e172b20b8b68)) +* prevent long IDs in warnings from pushing the dismiss button offscreen ([2c9fe65](https://github.com/Sofie-Automation/sofie-core/commit/2c9fe65df3fc04111791fc2a279f527af4dbe20f)) +* **PreviewPopUpContext:** only use large preview if previewUrl is set ([779f681](https://github.com/Sofie-Automation/sofie-core/commit/779f681e47fa03dd64a5dbcdc33b62450f274803)) +* **prompter:** Broken scroll jumping on button press ([89723bd](https://github.com/Sofie-Automation/sofie-core/commit/89723bdbb2a36cfbead922e57b1f5cb701a54b2f)) +* **prompter:** Broken scroll to top ([adc8ce6](https://github.com/Sofie-Automation/sofie-core/commit/adc8ce68afa29fa839ae8286c4f9e594e0a6e447)) +* raise secondStage scroll time for slow machines ([6fe3507](https://github.com/Sofie-Automation/sofie-core/commit/6fe35073a11f6e5c478f1d92ab9f63d5c7b54805)) +* raise time for detach live segment ([4976696](https://github.com/Sofie-Automation/sofie-core/commit/49766966c00d1146a3efe64a006437b304ca5fca)) +* raise wait before scroll to ensure element is ready ([040c1bd](https://github.com/Sofie-Automation/sofie-core/commit/040c1bde2c41dbfffc5503157d115bafb59d4d2e)) +* react uses a-tag for Link, and that has underline as default ([f7b522e](https://github.com/Sofie-Automation/sofie-core/commit/f7b522e7c3106c6f10cc53ae78f6b0e346e1d484)) +* recursive event emits to onGoToPartInstance when scrollToPartInstance was called ([eac3da2](https://github.com/Sofie-Automation/sofie-core/commit/eac3da2a51ce83f3203ceef8f88782a2975c7a70)) +* reimplement `removePartInstance` flow for `syncChangesToPartInstances` ([55e9871](https://github.com/Sofie-Automation/sofie-core/commit/55e9871e50129b142d5fdbea4b8a41eaddcbe823)) +* remote double measurement on load, as the observer takes care of that now ([26b7004](https://github.com/Sofie-Automation/sofie-core/commit/26b70046dcfcf6cbbcbfa2fcf6eb1bfd23b0441a)) +* remove left over console.logs ([ee09bf8](https://github.com/Sofie-Automation/sofie-core/commit/ee09bf8711181f3c1518518eb32a256c31034695)) +* remove over-eager debug logging filtering from connectionManager ([#1594](https://github.com/Sofie-Automation/sofie-core/issues/1594)) ([462a27a](https://github.com/Sofie-Automation/sofie-core/commit/462a27a3c68176fbcf3c5ab3d22fa0f79037db1d)) +* remove unimplemented return type of blueprint executeAction ([5e74d4f](https://github.com/Sofie-Automation/sofie-core/commit/5e74d4ff2b5322683d6bb2bff1bc228ec9709ec8)) +* required buckets properties in LSG api ([f414691](https://github.com/Sofie-Automation/sofie-core/commit/f4146912ffcaaddb96218fc23d03ac3a8003d0fe)) +* resolve segment list header glitches ([c2224d6](https://github.com/Sofie-Automation/sofie-core/commit/c2224d62385e4218901a624112ce7ae6b5712875)) +* returned http api status codes on error ([b0cd19f](https://github.com/Sofie-Automation/sofie-core/commit/b0cd19fc6f9fbbef9787bac2866d67e617ea2210)) +* revert presenter screen typo changes ([788af75](https://github.com/Sofie-Automation/sofie-core/commit/788af7565ce555c574afc484df814c059ad37f2a)) +* rework targetNowTime in playout, make it part of the Model ([f87d372](https://github.com/Sofie-Automation/sofie-core/commit/f87d3721b207d14d3ffad3118c4614ea3a54e4f4)) +* **RundownListItemView:** the "Live" Rundown indicator is positioned incorrectly ([8c86f79](https://github.com/Sofie-Automation/sofie-core/commit/8c86f795edb0369e16e3a67eb2511d3ec0788120)) +* Safari race condition in virtualElement ([c05b83e](https://github.com/Sofie-Automation/sofie-core/commit/c05b83e1330a8cdc3870e3b45ea56e3da1da4767)) +* **ScriptPreview:** lastWords are not shown in Inspector when content.script contains only whitespace characters ([e229b75](https://github.com/Sofie-Automation/sofie-core/commit/e229b7511b282df1bdbc5b3225e41a4b806c55b4)) +* segment counter was jumping when number changed ([8ec65a0](https://github.com/Sofie-Automation/sofie-core/commit/8ec65a0b83499fa05352615b012139b7978dc9af)) +* set isShowingChildren imidiatly when element are in view to avoid timing issues ([9b1ae19](https://github.com/Sofie-Automation/sofie-core/commit/9b1ae19988d0cc1530c6138f2927a9c3ea2a1183)) +* Set origin on iFrame preview ([9601613](https://github.com/Sofie-Automation/sofie-core/commit/9601613673e3446217c4a9b83ed59162c64f6991)) +* **Settings GUI.Package Manager:** Add missing input form for the AtemMediaStore accessor type ([7184cf2](https://github.com/Sofie-Automation/sofie-core/commit/7184cf26b0de5b7e5fb78b064a2f1d9a1a96db88)) +* **Settings GUI.Package Manager:** Change input type for container.accessors.${accessorId}.ISAUrls to an array of strings ([20eb608](https://github.com/Sofie-Automation/sofie-core/commit/20eb60805f740b73f9d2ba911d7109521a64640f)) +* **Settings GUI.Package Manager:** Change input type for container.accessors.${accessorId}.serverId to an int and not a string ([ef06a60](https://github.com/Sofie-Automation/sofie-core/commit/ef06a604cd2335016700f2d64a6b5b4ecb4c50b6)) +* simplify meteor collection auth checks ([2fac520](https://github.com/Sofie-Automation/sofie-core/commit/2fac5208de11305e41ef33677ff9c2c09e32885b)) +* simplify PieceIcons.scss ([21dcefb](https://github.com/Sofie-Automation/sofie-core/commit/21dcefb2aafaacfb8b90e86ecb2ed9c37d783f9b)) +* splitscreen should follow the other PieceIcons style ([46c947f](https://github.com/Sofie-Automation/sofie-core/commit/46c947f9605df0059600cc85eaf6bdab25f1cc7c)) +* Standardise spelling of "collapsable" to "collapsible" in styles and components ([b6a9459](https://github.com/Sofie-Automation/sofie-core/commit/b6a94599586360fab25b739ad7db764533f6d456)) +* Subscription name check ([56823de](https://github.com/Sofie-Automation/sofie-core/commit/56823de4e34e4bff24700aad214eb6234d309b35)) +* take into account a situation when .duration is 0. resolves [#1414](https://github.com/Sofie-Automation/sofie-core/issues/1414) ([8ee3589](https://github.com/Sofie-Automation/sofie-core/commit/8ee3589024610bfb7c61348380eae6a278203076)) +* take unknown elements into account ([34e8021](https://github.com/Sofie-Automation/sofie-core/commit/34e802156865848df752402ac1814e2938f38581)) +* to slow update if segment need 2.stage adjustment ([8ee1e3a](https://github.com/Sofie-Automation/sofie-core/commit/8ee1e3aed1e8d8e143eb5899964d584ef3edd7ca)) +* Track and clear drag timeout to prevent interference between drags ([3cd0584](https://github.com/Sofie-Automation/sofie-core/commit/3cd058403ff77f03c8daa37cfd8c4660bca450fb)) +* translation and uppercase ([01a9f84](https://github.com/Sofie-Automation/sofie-core/commit/01a9f848b769364931edd74a23d7a9ee69a328f4)) +* trigger postMessage when changed while already showing iframePreview ([62321c7](https://github.com/Sofie-Automation/sofie-core/commit/62321c7473c58daecabd60bd22a0fac88787d7a6)) +* typo in css className ([1da5770](https://github.com/Sofie-Automation/sofie-core/commit/1da577062ffd22163b7a8358f7186bb5b96ba59b)) +* update dependencies for mos-connection, TSR and timeline ([#1517](https://github.com/Sofie-Automation/sofie-core/issues/1517)) ([e7ef19c](https://github.com/Sofie-Automation/sofie-core/commit/e7ef19cbd3a7bcd16e80140e50435b097a13ad0e)) +* update mos-connection for missing mosID bug fix ([#9](https://github.com/Sofie-Automation/sofie-core/issues/9)) ([e8e07e3](https://github.com/Sofie-Automation/sofie-core/commit/e8e07e3e86e0a6e4d1bb5802f0e782ad323f424e)) +* update Package Manager types ([eaecc08](https://github.com/Sofie-Automation/sofie-core/commit/eaecc08378ae08bb60cf244c2df4068388b7c3af)) +* update tsr and remove deprecated playout-gateway methods ([#1525](https://github.com/Sofie-Automation/sofie-core/issues/1525)) ([5b9c7ad](https://github.com/Sofie-Automation/sofie-core/commit/5b9c7ad68375301722057ef4927bab13ce6896c1)) +* Use origin from URL object ([f5a6414](https://github.com/Sofie-Automation/sofie-core/commit/f5a6414fa67e656a040db0e083a2c410eea8f921)) +* use screen term instead of view in screen name ([a45c3e1](https://github.com/Sofie-Automation/sofie-core/commit/a45c3e13fd10fe0870a8aa501d74b4a69324fc13)) +* use throttle in onWheelScrollInner for more fluid scrolling ([fb274e9](https://github.com/Sofie-Automation/sofie-core/commit/fb274e9f62284d55825515af7c089c9c09acc59e)) +* use translation on next/auto ([6cab9cc](https://github.com/Sofie-Automation/sofie-core/commit/6cab9cc61bab19d1cca18e91a5c64421fa8cd1fd)) +* UserError getting lost when returned from jobWorker ([a8effb8](https://github.com/Sofie-Automation/sofie-core/commit/a8effb821d32f3f49ae79e998126f5d7cfb39fbd)) +* vertical alignment of context menu icons ([9214e73](https://github.com/Sofie-Automation/sofie-core/commit/9214e737a0bb006ecc0ded110ac0193c2289e05d)) +* virtualElement shouldn't adjust while scrolling. Earlier there was just a 5sec delay to adjust the virtualElement. Instead there's a state in viewPort telling if it's scrolling ([815c1a6](https://github.com/Sofie-Automation/sofie-core/commit/815c1a64cf264b67305a80265fa84b068b59b3d0)) +* VirtualElement use segment styling on placehodlers ([26455fc](https://github.com/Sofie-Automation/sofie-core/commit/26455fcac2d430bd8b686e19bc752c4acbfeef13)) + ## [1.52.0](///compare/v1.52.0-in-testing.1...v1.52.0) (2025-06-30) diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index fa8a8934edb..0bc1d260dc9 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -52,6 +52,11 @@ export function defaultRundownPlaylist(_id: RundownPlaylistId, studioId: StudioI type: 'none' as any, }, rundownIdsInOrder: [], + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], } } export function defaultRundown( @@ -116,8 +121,10 @@ export function defaultStudio(_id: StudioId): DBStudio { routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], + packageContainerSettingsWithOverrides: wrapDefaultObject({ + previewContainerIds: [], + thumbnailContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), diff --git a/meteor/package.json b/meteor/package.json index 8397b76ef54..32c69ae4ee4 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "private": true, "engines": { "node": ">=22.20.0" @@ -28,82 +28,78 @@ "i18n-extract-pot": "node ./scripts/extract-i18next-pot.mjs -f \"{./lib/**/*.+(ts|tsx),./server/**/*.+(ts|tsx),../packages/job-worker/src/**/*.+(ts|tsx),../packages/corelib/src/**/*.+(ts|tsx),../packages/webui/src/**/*.+(ts|tsx)}\" -o i18n/template.pot", "i18n-compile-json": "node ./scripts/i18n-compile-json.mjs", "visualize": "meteor --production --extra-packages bundle-visualizer", - "release": "standard-version --commit-all", + "release": "commit-and-tag-version --commit-all", "prepareChangelog": "run release --prerelease --release-as patch", "validate:all-dependencies": "run validate:prod-dependencies && run validate:dev-dependencies && run license-validate", "validate:prod-dependencies": "yarn npm audit --environment production", "validate:dev-dependencies": "yarn npm audit --environment development --severity moderate" }, "dependencies": { - "@babel/runtime": "^7.26.7", + "@babel/runtime": "^7.28.6", "@koa/cors": "^5.0.0", - "@koa/router": "^13.1.0", + "@koa/router": "^15.3.0", "@mos-connection/helper": "^5.0.0-alpha.0", - "@slack/webhook": "^7.0.4", + "@slack/webhook": "^7.0.6", "@sofie-automation/blueprints-integration": "portal:../packages/blueprints-integration", "@sofie-automation/corelib": "portal:../packages/corelib", "@sofie-automation/job-worker": "portal:../packages/job-worker", "@sofie-automation/meteor-lib": "portal:../packages/meteor-lib", "@sofie-automation/shared-lib": "portal:../packages/shared-lib", - "app-root-path": "^3.1.0", - "bcrypt": "^5.1.1", - "body-parser": "^1.20.3", + "bcrypt": "^6.0.0", + "body-parser": "^1.20.4", "deep-extend": "0.6.0", "deepmerge": "^4.3.1", - "elastic-apm-node": "^4.11.0", + "elastic-apm-node": "^4.15.0", "i18next": "^21.10.0", "indexof": "0.0.1", - "koa": "^2.15.3", + "koa": "^3.1.1", "koa-bodyparser": "^4.4.1", - "koa-mount": "^4.0.0", + "koa-mount": "^4.2.0", "koa-static": "^5.0.0", - "meteor-node-stubs": "^1.2.12", + "meteor-node-stubs": "^1.2.25", "moment": "^2.30.1", - "nanoid": "^3.3.8", - "node-gyp": "^9.4.1", + "nanoid": "^3.3.11", "ntp-client": "^0.5.3", "object-path": "^0.11.8", "p-lazy": "^3.1.0", - "semver": "^7.6.3", + "semver": "^7.7.3", "superfly-timeline": "9.2.0", - "threadedclass": "^1.2.2", + "threadedclass": "^1.3.0", "timecode": "0.0.4", - "type-fest": "^4.33.0", + "type-fest": "^4.41.0", "underscore": "^1.13.7", - "winston": "^3.17.0" + "winston": "^3.19.0" }, "devDependencies": { - "@babel/core": "^7.26.7", - "@babel/plugin-transform-modules-commonjs": "^7.26.3", + "@babel/core": "^7.29.0", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", "@shopify/jest-koa-mocks": "^5.3.1", "@sofie-automation/code-standard-preset": "^3.0.0", - "@types/app-root-path": "^1.2.8", - "@types/body-parser": "^1.19.5", + "@types/body-parser": "^1.19.6", "@types/deep-extend": "^0.6.2", - "@types/jest": "^29.5.14", - "@types/koa": "^2.15.0", - "@types/koa-bodyparser": "^4.3.12", - "@types/koa-mount": "^4", + "@types/jest": "^30.0.0", + "@types/koa": "^3.0.1", + "@types/koa-bodyparser": "^4.3.13", + "@types/koa-mount": "^4.0.5", "@types/koa-static": "^4.0.4", - "@types/koa__cors": "^5.0.0", - "@types/koa__router": "^12.0.4", - "@types/node": "^22.10.10", - "@types/request": "^2.48.12", - "@types/semver": "^7.5.8", + "@types/koa__cors": "^5.0.1", + "@types/node": "^22.19.8", + "@types/semver": "^7.7.1", "@types/underscore": "^1.13.0", - "babel-jest": "^29.7.0", + "babel-jest": "^30.2.0", + "commit-and-tag-version": "^12.6.1", "ejson": "^2.2.3", - "eslint": "^9.18.0", + "eslint": "^9.39.2", "fast-clone": "^1.5.13", - "glob": "^11.0.1", + "glob": "^13.0.1", "i18next-conv": "^10.2.0", "i18next-scanner": "^4.6.0", - "jest": "^29.7.0", + "jest": "^30.2.0", + "jest-util": "^30.2.0", "legally": "^3.5.10", "open-cli": "^8.0.0", - "prettier": "^3.4.2", - "standard-version": "^9.5.0", - "ts-jest": "^29.2.5", + "prettier": "^3.8.1", + "ts-jest": "^29.4.6", "typescript": "~5.7.3", "yargs": "^17.7.2" }, @@ -122,7 +118,7 @@ "server": "server/main.ts" } }, - "standard-version": { + "commit-and-tag-version": { "scripts": { "postbump": "yarn libs:syncVersionsAndChangelogs && yarn install && git add yarn.lock" } diff --git a/meteor/scripts/babel-jest.js b/meteor/scripts/babel-jest.js index 9189f0d2db3..6554637722b 100644 --- a/meteor/scripts/babel-jest.js +++ b/meteor/scripts/babel-jest.js @@ -1,6 +1,6 @@ const babelJest = require('babel-jest') -module.exports = babelJest.createTransformer({ +module.exports = babelJest.default.createTransformer({ plugins: ['@babel/plugin-transform-modules-commonjs'], babelrc: false, configFile: false, diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index c61e36bdcb7..b5f6ce1d78e 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -458,6 +458,7 @@ describe('cronjobs', () => { _id: snapshot0, comment: '', fileName: '', + longname: '', name: '', type: SnapshotType.DEBUG, version: '', @@ -471,6 +472,7 @@ describe('cronjobs', () => { comment: '', fileName: '', name: '', + longname: '', type: SnapshotType.DEBUG, version: '', // Very old: @@ -592,8 +594,10 @@ describe('cronjobs', () => { routeSetsWithOverrides: newObjectWithOverrides({}), routeSetExclusivityGroupsWithOverrides: newObjectWithOverrides({}), packageContainersWithOverrides: newObjectWithOverrides({}), - previewContainerIds: [], - thumbnailContainerIds: [], + packageContainerSettingsWithOverrides: newObjectWithOverrides({ + previewContainerIds: [], + thumbnailContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: newObjectWithOverrides({}), ingestDevices: newObjectWithOverrides({}), @@ -618,6 +622,11 @@ describe('cronjobs', () => { type: PlaylistTimingType.None, }, activationId: protectString(''), + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], }) return { diff --git a/meteor/server/api/__tests__/cleanup.test.ts b/meteor/server/api/__tests__/cleanup.test.ts index d27bded17cb..89e22ef454e 100644 --- a/meteor/server/api/__tests__/cleanup.test.ts +++ b/meteor/server/api/__tests__/cleanup.test.ts @@ -403,6 +403,7 @@ async function setDefaultDatatoDB(env: DefaultEnvironment, now: number) { created: now, fileName: '', name: '', + longname: '', type: '' as any, version: '', }) diff --git a/meteor/server/api/__tests__/externalMessageQueue.test.ts b/meteor/server/api/__tests__/externalMessageQueue.test.ts index 801220a8f85..1b5fb53f938 100644 --- a/meteor/server/api/__tests__/externalMessageQueue.test.ts +++ b/meteor/server/api/__tests__/externalMessageQueue.test.ts @@ -41,6 +41,7 @@ describe('Test external message queue static methods', () => { type: PlaylistTimingType.None, }, rundownIdsInOrder: [protectString('rundown_1')], + tTimers: [] as any, }) await Rundowns.mutableCollection.insertAsync({ _id: protectString('rundown_1'), diff --git a/meteor/server/api/__tests__/peripheralDevice.test.ts b/meteor/server/api/__tests__/peripheralDevice.test.ts index 3c819cf20a7..594c44049ca 100644 --- a/meteor/server/api/__tests__/peripheralDevice.test.ts +++ b/meteor/server/api/__tests__/peripheralDevice.test.ts @@ -78,6 +78,7 @@ describe('test peripheralDevice general API methods', () => { type: PlaylistTimingType.None, }, rundownIdsInOrder: [rundownID], + tTimers: [] as any, }) await Rundowns.mutableCollection.insertAsync({ _id: rundownID, diff --git a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts b/meteor/server/api/blueprints/__tests__/migrationContext.test.ts deleted file mode 100644 index 0b38f6a005b..00000000000 --- a/meteor/server/api/blueprints/__tests__/migrationContext.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import '../../../../__mocks__/_extendJest' -import { setupDefaultStudioEnvironment } from '../../../../__mocks__/helpers/database' -import { literal } from '@sofie-automation/corelib/dist/lib' -import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { - TriggerType, - ClientActions, - PlayoutActions, - IBlueprintTriggeredActions, -} from '@sofie-automation/blueprints-integration' -import { MigrationContextSystem } from '../migrationContext' -import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { CoreSystem, TriggeredActions } from '../../../collections' - -describe('Test blueprint migrationContext', () => { - beforeAll(async () => { - await setupDefaultStudioEnvironment() - }) - - describe('MigrationContextSystem', () => { - async function getContext() { - const coreSystem = await CoreSystem.findOneAsync({}) - expect(coreSystem).toBeTruthy() - return new MigrationContextSystem() - } - async function getSystemTriggeredActions(): Promise { - const systemTriggeredActions = await TriggeredActions.findFetchAsync({ - showStyleBaseId: null, - }) - expect(systemTriggeredActions).toHaveLength(3) - return systemTriggeredActions.map((doc) => - literal({ - _id: unprotectString(doc._id), - _rank: doc._rank, - name: doc.name, - triggers: applyAndValidateOverrides(doc.triggersWithOverrides).obj, - actions: applyAndValidateOverrides(doc.actionsWithOverrides).obj, - }) - ) - } - describe('triggeredActions', () => { - test('getAllTriggeredActions: return all triggeredActions', async () => { - const ctx = await getContext() - - // default studio environment should have 3 core-level actions - expect(await ctx.getAllTriggeredActions()).toHaveLength(3) - }) - test('getTriggeredAction: no id', async () => { - const ctx = await getContext() - - await expect(ctx.getTriggeredAction('')).rejects.toThrowMeteor( - 500, - 'Triggered actions Id "" is invalid' - ) - }) - test('getTriggeredAction: missing id', async () => { - const ctx = await getContext() - - expect(await ctx.getTriggeredAction('abc')).toBeFalsy() - }) - test('getTriggeredAction: existing id', async () => { - const ctx = await getContext() - - const existingTriggeredActions = (await getSystemTriggeredActions())[0] - expect(existingTriggeredActions).toBeTruthy() - expect(await ctx.getTriggeredAction(existingTriggeredActions._id)).toMatchObject( - existingTriggeredActions - ) - }) - test('setTriggeredAction: set undefined', async () => { - const ctx = await getContext() - - await expect(ctx.setTriggeredAction(undefined as any)).rejects.toThrow(/Match error/) - }) - test('setTriggeredAction: set without id', async () => { - const ctx = await getContext() - - await expect( - ctx.setTriggeredAction({ - _rank: 0, - actions: [], - triggers: [], - } as any) - ).rejects.toThrow(/Match error/) - }) - test('setTriggeredAction: set without actions', async () => { - const ctx = await getContext() - - await expect( - ctx.setTriggeredAction({ - _id: 'test1', - _rank: 0, - triggers: [], - } as any) - ).rejects.toThrow(/Match error/) - }) - test('setTriggeredAction: set with null as name', async () => { - const ctx = await getContext() - - await expect( - ctx.setTriggeredAction({ - _id: 'test1', - _rank: 0, - actions: [], - triggers: [], - name: null, - } as any) - ).rejects.toThrow(/Match error/) - }) - test('setTriggeredAction: set non-existing id', async () => { - const ctx = await getContext() - - const blueprintLocalId = 'test0' - - await ctx.setTriggeredAction({ - _id: blueprintLocalId, - _rank: 1001, - actions: { - '0': { - action: ClientActions.shelf, - filterChain: [ - { - object: 'view', - }, - ], - state: 'toggle', - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Digit1', - }, - }, - }) - const insertedTriggeredAction = await ctx.getTriggeredAction(blueprintLocalId) - expect(insertedTriggeredAction).toBeTruthy() - // the actual id in the database should not be the same as the one provided - // in the setTriggeredAction method - expect(insertedTriggeredAction?._id !== blueprintLocalId).toBe(true) - }) - test('setTriggeredAction: set existing id', async () => { - const ctx = await getContext() - - const oldCoreAction = await ctx.getTriggeredAction('mockTriggeredAction_core0') - expect(oldCoreAction).toBeTruthy() - expect(oldCoreAction?.actions[0].action).toBe(PlayoutActions.adlib) - - await ctx.setTriggeredAction({ - _id: 'mockTriggeredAction_core0', - _rank: 0, - actions: { - '0': { - action: PlayoutActions.activateRundownPlaylist, - rehearsal: false, - filterChain: [ - { - object: 'view', - }, - ], - }, - }, - triggers: { - '0': { - type: TriggerType.hotkey, - keys: 'Control+Shift+Enter', - }, - }, - }) - - const newCoreAction = await ctx.getTriggeredAction('mockTriggeredAction_core0') - expect(newCoreAction).toBeTruthy() - expect(newCoreAction?.actions[0].action).toBe(PlayoutActions.activateRundownPlaylist) - }) - test('removeTriggeredAction: remove empty id', async () => { - const ctx = await getContext() - - await expect(ctx.removeTriggeredAction('')).rejects.toThrowMeteor( - 500, - 'Triggered actions Id "" is invalid' - ) - }) - test('removeTriggeredAction: remove existing id', async () => { - const ctx = await getContext() - - const oldCoreAction = await ctx.getTriggeredAction('mockTriggeredAction_core0') - expect(oldCoreAction).toBeTruthy() - - await ctx.removeTriggeredAction('mockTriggeredAction_core0') - expect(await ctx.getTriggeredAction('mockTriggeredAction_core0')).toBeFalsy() - }) - }) - }) -}) diff --git a/meteor/server/api/blueprints/http.ts b/meteor/server/api/blueprints/http.ts index 394cd1e02c9..29b1a6bc719 100644 --- a/meteor/server/api/blueprints/http.ts +++ b/meteor/server/api/blueprints/http.ts @@ -179,7 +179,7 @@ blueprintsRouter.post( } ) -blueprintsRouter.get('/assets/(.*)', async (ctx) => { +blueprintsRouter.get('/assets/*splat', async (ctx) => { logger.debug(`Blueprint Asset: ${ctx.socket.remoteAddress} GET "${ctx.url}"`) // TODO - some sort of user verification // for now just check it's a png to prevent snapshots being downloaded diff --git a/meteor/server/api/blueprints/migrationContext.ts b/meteor/server/api/blueprints/migrationContext.ts deleted file mode 100644 index 2615ad6a908..00000000000 --- a/meteor/server/api/blueprints/migrationContext.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { getHash, clone, Complete } from '@sofie-automation/corelib/dist/lib' -import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' -import { Meteor } from 'meteor/meteor' -import { - MigrationContextSystem as IMigrationContextSystem, - IBlueprintTriggeredActions, -} from '@sofie-automation/blueprints-integration' -import { check } from '../../lib/check' -import { TriggeredActionsObj } from '@sofie-automation/meteor-lib/dist/collections/TriggeredActions' -import { Match } from 'meteor/check' -import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { ShowStyleBaseId, TriggeredActionId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { TriggeredActions } from '../../collections' - -function convertTriggeredActionToBlueprints(triggeredAction: TriggeredActionsObj): IBlueprintTriggeredActions { - const obj: Complete = { - _id: unprotectString(triggeredAction._id), - _rank: triggeredAction._rank, - name: triggeredAction.name, - triggers: clone(triggeredAction.triggersWithOverrides.defaults), - actions: clone(triggeredAction.actionsWithOverrides.defaults), - styleClassNames: triggeredAction.styleClassNames, - } - - return obj -} - -class AbstractMigrationContextWithTriggeredActions { - protected showStyleBaseId: ShowStyleBaseId | null = null - getTriggeredActionId(triggeredActionId: string): string { - return getHash((this.showStyleBaseId ?? 'core') + '_' + triggeredActionId) - } - private getProtectedTriggeredActionId(triggeredActionId: string): TriggeredActionId { - return protectString(this.getTriggeredActionId(triggeredActionId)) - } - async getAllTriggeredActions(): Promise { - return ( - await TriggeredActions.findFetchAsync({ - showStyleBaseId: this.showStyleBaseId, - }) - ).map(convertTriggeredActionToBlueprints) - } - private async getTriggeredActionFromDb(triggeredActionId: string): Promise { - const triggeredAction = await TriggeredActions.findOneAsync({ - showStyleBaseId: this.showStyleBaseId, - _id: this.getProtectedTriggeredActionId(triggeredActionId), - }) - if (triggeredAction) return triggeredAction - - // Assume we were given the full id - return TriggeredActions.findOneAsync({ - showStyleBaseId: this.showStyleBaseId, - _id: protectString(triggeredActionId), - }) - } - async getTriggeredAction(triggeredActionId: string): Promise { - check(triggeredActionId, String) - if (!triggeredActionId) { - throw new Meteor.Error(500, `Triggered actions Id "${triggeredActionId}" is invalid`) - } - - const obj = await this.getTriggeredActionFromDb(triggeredActionId) - return obj ? convertTriggeredActionToBlueprints(obj) : undefined - } - async setTriggeredAction(triggeredActions: IBlueprintTriggeredActions): Promise { - check(triggeredActions, Object) - check(triggeredActions._id, String) - check(triggeredActions._rank, Number) - check(triggeredActions.actions, Object) - check(triggeredActions.triggers, Object) - check(triggeredActions.name, Match.OneOf(Match.Optional(Object), Match.Optional(String))) - if (!triggeredActions) { - throw new Meteor.Error(500, `Triggered Actions object is invalid`) - } - - const newObj: Omit = { - // _rundownVersionHash: '', - // _id: this.getProtectedTriggeredActionId(triggeredActions._id), - _rank: triggeredActions._rank, - name: triggeredActions.name, - triggersWithOverrides: wrapDefaultObject(triggeredActions.triggers), - actionsWithOverrides: wrapDefaultObject(triggeredActions.actions), - blueprintUniqueId: triggeredActions._id, - } - - const currentTriggeredAction = await this.getTriggeredActionFromDb(triggeredActions._id) - if (!currentTriggeredAction) { - await TriggeredActions.insertAsync({ - ...newObj, - showStyleBaseId: this.showStyleBaseId, - _id: this.getProtectedTriggeredActionId(triggeredActions._id), - }) - } else { - await TriggeredActions.updateAsync( - { - _id: currentTriggeredAction._id, - }, - { - $set: newObj, - }, - { multi: true } - ) - } - } - async removeTriggeredAction(triggeredActionId: string): Promise { - check(triggeredActionId, String) - if (!triggeredActionId) { - throw new Meteor.Error(500, `Triggered actions Id "${triggeredActionId}" is invalid`) - } - - const currentTriggeredAction = await this.getTriggeredActionFromDb(triggeredActionId) - if (currentTriggeredAction) { - await TriggeredActions.removeAsync({ - _id: currentTriggeredAction._id, - showStyleBaseId: this.showStyleBaseId, - }) - } - } -} - -export class MigrationContextSystem - extends AbstractMigrationContextWithTriggeredActions - implements IMigrationContextSystem {} diff --git a/meteor/server/api/deviceTriggers/StudioObserver.ts b/meteor/server/api/deviceTriggers/StudioObserver.ts index 43adfdace19..c98a154b468 100644 --- a/meteor/server/api/deviceTriggers/StudioObserver.ts +++ b/meteor/server/api/deviceTriggers/StudioObserver.ts @@ -29,7 +29,7 @@ type PieceInstancesChangeHandler = (showStyleBaseId: ShowStyleBaseId, cache: Pie const REACTIVITY_DEBOUNCE = 20 -type RundownPlaylistFields = '_id' | 'nextPartInfo' | 'currentPartInfo' | 'activationId' +type RundownPlaylistFields = '_id' | 'nextPartInfo' | 'currentPartInfo' | 'activationId' | 'rehearsal' | 'studioId' const rundownPlaylistFieldSpecifier = literal< MongoFieldSpecifierOnesStrict> >({ @@ -37,6 +37,8 @@ const rundownPlaylistFieldSpecifier = literal< activationId: 1, currentPartInfo: 1, nextPartInfo: 1, + rehearsal: 1, + studioId: 1, }) type RundownFields = '_id' | 'showStyleBaseId' diff --git a/meteor/server/api/deviceTriggers/reactiveContentCache.ts b/meteor/server/api/deviceTriggers/reactiveContentCache.ts index c61d96fe976..5c01abb7ec4 100644 --- a/meteor/server/api/deviceTriggers/reactiveContentCache.ts +++ b/meteor/server/api/deviceTriggers/reactiveContentCache.ts @@ -14,7 +14,14 @@ import { MongoFieldSpecifierOnesStrict } from '@sofie-automation/corelib/dist/mo import { literal } from '@sofie-automation/corelib/dist/lib' import { ReactiveCacheCollection } from '../../publications/lib/ReactiveCacheCollection' -export type RundownPlaylistFields = '_id' | 'name' | 'activationId' | 'currentPartInfo' | 'nextPartInfo' +export type RundownPlaylistFields = + | '_id' + | 'name' + | 'activationId' + | 'currentPartInfo' + | 'nextPartInfo' + | 'studioId' + | 'rehearsal' export const rundownPlaylistFieldSpecifier = literal< MongoFieldSpecifierOnesStrict> >({ @@ -23,6 +30,8 @@ export const rundownPlaylistFieldSpecifier = literal< activationId: 1, currentPartInfo: 1, nextPartInfo: 1, + studioId: 1, + rehearsal: 1, }) export type SegmentFields = '_id' | '_rank' | 'isHidden' | 'name' | 'rundownId' | 'identifier' diff --git a/meteor/server/api/deviceTriggers/triggersContext.ts b/meteor/server/api/deviceTriggers/triggersContext.ts index bbe5df5c51d..a93d5da362a 100644 --- a/meteor/server/api/deviceTriggers/triggersContext.ts +++ b/meteor/server/api/deviceTriggers/triggersContext.ts @@ -6,7 +6,7 @@ import { import { SINGLE_USE_TOKEN_SALT } from '@sofie-automation/meteor-lib/dist/api/userActions' import { assertNever, getHash } from '@sofie-automation/corelib/dist/lib' import type { Time } from '@sofie-automation/shared-lib/dist/lib/lib' -import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' +import { ProtectedString, protectString } from '@sofie-automation/corelib/dist/protectedString' import { getCurrentTime } from '../../lib/lib' import { MeteorCall } from '../methods' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' @@ -38,9 +38,9 @@ export function hashSingleUseToken(token: string): string { return getHash(SINGLE_USE_TOKEN_SALT + token) } -class MeteorTriggersCollectionWrapper }> - implements TriggersAsyncCollection -{ +class MeteorTriggersCollectionWrapper< + DBInterface extends { _id: ProtectedString }, +> implements TriggersAsyncCollection { readonly #collection: AsyncOnlyReadOnlyMongoCollection constructor(collection: AsyncOnlyReadOnlyMongoCollection) { @@ -209,10 +209,13 @@ async function rundownPlaylistFilter( case 'studioId': selector['$and']?.push({ studioId: { - $regex: link.value as any, + $eq: protectString(link.value), }, }) break + case 'rehearsal': + selector['rehearsal'] = link.value + break default: assertNever(link) break diff --git a/meteor/server/api/ingest/actions.ts b/meteor/server/api/ingest/actions.ts index 6a6cf852bf3..29fc700312a 100644 --- a/meteor/server/api/ingest/actions.ts +++ b/meteor/server/api/ingest/actions.ts @@ -7,6 +7,7 @@ import { PeripheralDeviceType } from '@sofie-automation/corelib/dist/dataModel/P import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { assertNever } from '@sofie-automation/corelib/dist/lib' import { VerifiedRundownForUserAction } from '../../security/check' +import { logger } from '../../logging' /* This file contains actions that can be performed on an ingest-device @@ -27,6 +28,26 @@ export namespace IngestActions { return TriggerReloadDataResponse.COMPLETED } + case 'restApi': { + const resyncUrl = rundown.source.resyncUrl + fetch(resyncUrl, { method: 'POST' }) + .then(() => { + logger.info(`Reload rundown: resync request sent to "${resyncUrl}"`) + }) + .catch((error) => { + if (error.cause.code === 'ECONNREFUSED' || error.cause.code === 'ENOTFOUND') { + logger.error( + `Reload rundown: could not establish connection with "${resyncUrl}" (${error.cause.code})` + ) + return + } + logger.error( + `Reload rundown: error occured while sending resync request to "${resyncUrl}", message: ${error.message}, cause: ${JSON.stringify(error.cause)}` + ) + }) + + return TriggerReloadDataResponse.WORKING + } case 'testing': { await runIngestOperation(rundown.studioId, IngestJobs.CreateAdlibTestingRundownForShowStyleVariant, { showStyleVariantId: rundown.showStyleVariantId, diff --git a/meteor/server/api/rest/koa.ts b/meteor/server/api/rest/koa.ts index ec57fc79cec..7e6f8420aaa 100644 --- a/meteor/server/api/rest/koa.ts +++ b/meteor/server/api/rest/koa.ts @@ -196,11 +196,7 @@ function getClientAddrFromForwarded(forwardedVal: string | undefined): string | } export const makeMeteorConnectionFromKoa = ( - ctx: Koa.ParameterizedContext< - Koa.DefaultState, - Koa.DefaultContext & KoaRouter.RouterParamContext, - unknown - > + ctx: Koa.ParameterizedContext ): Meteor.Connection => { return { id: getRandomString(), diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index 87e8bdf1cf7..ab2908aab57 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -19,17 +19,12 @@ import { registerRoutes as registerStudiosRoutes } from './studios' import { registerRoutes as registerSystemRoutes } from './system' import { registerRoutes as registerBucketsRoutes } from './buckets' import { registerRoutes as registerSnapshotRoutes } from './snapshots' +import { registerRoutes as registerIngestRoutes } from './ingest' import { APIFactory, ServerAPIContext } from './types' import { getSystemStatus } from '../../../systemStatus/systemStatus' import { Component, ExternalStatus } from '@sofie-automation/meteor-lib/dist/api/systemStatus' -function restAPIUserEvent( - ctx: Koa.ParameterizedContext< - Koa.DefaultState, - Koa.DefaultContext & KoaRouter.RouterParamContext, - unknown - > -): string { +function restAPIUserEvent(ctx: Koa.ParameterizedContext): string { // the ctx.URL.pathname will contain `/v1.0`, but will not contain `/api` return `REST API: ${ctx.method} /api${ctx.URL.pathname} ${ctx.URL.origin}` } @@ -122,6 +117,7 @@ interface APIRequestError { status: number message: string details?: string[] + additionalInfo?: Record } function sofieAPIRequest( @@ -138,6 +134,7 @@ function sofieAPIRequest( ) => Promise> ) { koaRouter[method](route, async (ctx, next) => { + let responseAdditionalInfo: Record | undefined try { const context = new APIContext() const serverAPI = serverAPIFactory.createServerAPI(context) @@ -149,6 +146,7 @@ function sofieAPIRequest( ctx.request.body as unknown as Body ) if (ClientAPI.isClientResponseError(response)) { + responseAdditionalInfo = response.additionalInfo throw UserError.fromSerialized(response.error) } ctx.body = JSON.stringify({ status: response.success, result: response.result }) @@ -181,7 +179,8 @@ function sofieAPIRequest( ctx.type = 'application/json' const bodyObj: APIRequestError = { status: errCode, message: errMsg } const details = extractErrorDetails(e) - if (details) bodyObj['details'] = details + if (details) bodyObj.details = details + if (responseAdditionalInfo) bodyObj.additionalInfo = responseAdditionalInfo ctx.body = JSON.stringify(bodyObj) ctx.status = errCode } @@ -296,3 +295,4 @@ registerStudiosRoutes(sofieAPIRequest) registerSystemRoutes(sofieAPIRequest) registerBucketsRoutes(sofieAPIRequest) registerSnapshotRoutes(sofieAPIRequest) +registerIngestRoutes(sofieAPIRequest) diff --git a/meteor/server/api/rest/v1/ingest.ts b/meteor/server/api/rest/v1/ingest.ts new file mode 100644 index 00000000000..33a6e2213a9 --- /dev/null +++ b/meteor/server/api/rest/v1/ingest.ts @@ -0,0 +1,1710 @@ +import { IngestPart, IngestRundown, IngestSegment } from '@sofie-automation/blueprints-integration' +import { + BlueprintId, + PartId, + RundownId, + RundownPlaylistId, + SegmentId, + StudioId, +} from '@sofie-automation/corelib/dist/dataModel/Ids' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { getRundownNrcsName, Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' +import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' +import { Meteor } from 'meteor/meteor' +import { Parts, RundownPlaylists, Rundowns, Segments, Studios } from '../../../collections' +import { check } from '../../../lib/check' +import { + IngestRestAPI, + PartResponse, + PlaylistResponse, + RestApiIngestRundown, + RundownResponse, + SegmentResponse, +} from '../../../lib/rest/v1/ingest' +import { logger } from '../../../logging' +import { runIngestOperation } from '../../ingest/lib' +import { validateAPIPartPayload, validateAPIRundownPayload, validateAPISegmentPayload } from './typeConversion' +import { APIFactory, APIRegisterHook, ServerAPIContext } from './types' + +class IngestServerAPI implements IngestRestAPI { + private async validateAPIPayloadsForRundown( + blueprintId: BlueprintId | undefined, + rundown: IngestRundown, + indexes?: { + rundown?: number + } + ) { + const validationResult = await validateAPIRundownPayload(blueprintId, rundown.payload) + const errorMessage = this.formatPayloadValidationErrors('Rundown', validationResult, indexes) + + if (errorMessage) { + logger.error(`${errorMessage} with errors: ${validationResult}`) + throw new Meteor.Error(409, errorMessage, JSON.stringify(validationResult)) + } + + return Promise.all( + rundown.segments.map(async (segment, index) => { + return this.validateAPIPayloadsForSegment(blueprintId, segment, { + ...indexes, + segment: index, + }) + }) + ) + } + + private async validateAPIPayloadsForSegment( + blueprintId: BlueprintId | undefined, + segment: IngestRundown['segments'][number], + indexes?: { + rundown?: number + segment?: number + } + ) { + const validationResult = await validateAPISegmentPayload(blueprintId, segment.payload) + const errorMessage = this.formatPayloadValidationErrors('Segment', validationResult, indexes) + + if (errorMessage) { + logger.error(`${errorMessage} with errors: ${validationResult}`) + throw new Meteor.Error(409, errorMessage, JSON.stringify(validationResult)) + } + + return Promise.all( + segment.parts.map(async (part, index) => { + return this.validateAPIPayloadsForPart(blueprintId, part, { ...indexes, part: index }) + }) + ) + } + + private async validateAPIPayloadsForPart( + blueprintId: BlueprintId | undefined, + part: IngestRundown['segments'][number]['parts'][number], + indexes?: { + rundown?: number + segment?: number + part?: number + } + ) { + const validationResult = await validateAPIPartPayload(blueprintId, part.payload) + const errorMessage = this.formatPayloadValidationErrors('Part', validationResult, indexes) + + if (errorMessage) { + logger.error(`${errorMessage} with errors: ${validationResult}`) + throw new Meteor.Error(409, errorMessage, JSON.stringify(validationResult)) + } + } + + private formatPayloadValidationErrors( + type: 'Rundown' | 'Segment' | 'Part', + validationResult: string[] | undefined, + indexes?: { + rundown?: number + segment?: number + part?: number + } + ) { + if (!validationResult || validationResult.length === 0) { + return + } + + const messageParts = [] + if (indexes?.rundown !== undefined) messageParts.push(`rundowns[${indexes.rundown}]`) + if (indexes?.segment !== undefined) messageParts.push(`segments[${indexes.segment}]`) + if (indexes?.part !== undefined) messageParts.push(`parts[${indexes.part}]`) + let message = `${type} payload validation failed` + if (messageParts.length > 0) message += ` for ${messageParts.join('.')}` + return message + } + + private validateRundown(ingestRundown: RestApiIngestRundown) { + check(ingestRundown, Object) + check(ingestRundown.externalId, String) + check(ingestRundown.name, String) + check(ingestRundown.type, String) + check(ingestRundown.segments, Array) + check(ingestRundown.resyncUrl, String) + + ingestRundown.segments.forEach((ingestSegment) => this.validateSegment(ingestSegment)) + } + + private validateSegment(ingestSegment: IngestSegment) { + check(ingestSegment, Object) + check(ingestSegment.externalId, String) + check(ingestSegment.name, String) + check(ingestSegment.rank, Number) + check(ingestSegment.parts, Array) + + ingestSegment.parts.forEach((ingestPart) => this.validatePart(ingestPart)) + } + + private validatePart(ingestPart: IngestPart) { + check(ingestPart, Object) + check(ingestPart.externalId, String) + check(ingestPart.name, String) + check(ingestPart.rank, Number) + } + + private adaptPlaylist(rawPlaylist: DBRundownPlaylist): PlaylistResponse { + return { + id: unprotectString(rawPlaylist._id), + externalId: rawPlaylist.externalId, + rundownIds: rawPlaylist.rundownIdsInOrder.map((id) => unprotectString(id)), + studioId: unprotectString(rawPlaylist.studioId), + } + } + + private adaptRundown(rawRundown: Rundown): RundownResponse { + return { + id: unprotectString(rawRundown._id), + externalId: rawRundown.externalId, + playlistId: unprotectString(rawRundown.playlistId), + playlistExternalId: rawRundown.playlistExternalId, + studioId: unprotectString(rawRundown.studioId), + name: rawRundown.name, + } + } + + private adaptSegment(rawSegment: DBSegment): SegmentResponse { + return { + id: unprotectString(rawSegment._id), + externalId: rawSegment.externalId, + name: rawSegment.name, + rank: rawSegment._rank, + rundownId: unprotectString(rawSegment.rundownId), + isHidden: rawSegment.isHidden, + } + } + + private adaptPart(rawPart: DBPart): PartResponse { + return { + id: unprotectString(rawPart._id), + externalId: rawPart.externalId, + name: rawPart.title, + rank: rawPart._rank, + rundownId: unprotectString(rawPart.rundownId), + autoNext: rawPart.autoNext, + expectedDuration: rawPart.expectedDuration, + segmentId: unprotectString(rawPart.segmentId), + } + } + + private async findPlaylist(studioId: StudioId, playlistId: string) { + const playlist = await RundownPlaylists.findOneAsync({ + $or: [ + { _id: protectString(playlistId), studioId }, + { externalId: playlistId, studioId }, + ], + }) + if (!playlist) { + throw new Meteor.Error(404, `Playlist ID '${playlistId}' was not found`) + } + return playlist + } + + private async findRundown(studioId: StudioId, playlistId: RundownPlaylistId, rundownId: string) { + const rundown = await Rundowns.findOneAsync({ + $or: [ + { + _id: protectString(rundownId), + playlistId, + studioId, + }, + { + externalId: rundownId, + playlistId, + studioId, + }, + ], + }) + if (!rundown) { + throw new Meteor.Error(404, `Rundown ID '${rundownId}' was not found`) + } + return rundown + } + + private async findRundowns(studioId: StudioId, playlistId: RundownPlaylistId) { + const rundowns = await Rundowns.findFetchAsync({ + $or: [ + { + playlistId, + studioId, + }, + ], + }) + + return rundowns + } + + private async softFindSegment(rundownId: RundownId, segmentId: string) { + const segment = await Segments.findOneAsync({ + $or: [ + { + _id: protectString(segmentId), + rundownId: rundownId, + }, + { + externalId: segmentId, + rundownId: rundownId, + }, + ], + }) + return segment + } + + private async findSegment(rundownId: RundownId, segmentId: string) { + const segment = await this.softFindSegment(rundownId, segmentId) + if (!segment) { + throw new Meteor.Error(404, `Segment ID '${segmentId}' was not found`) + } + return segment + } + + private async findSegments(rundownId: RundownId) { + const segments = await Segments.findFetchAsync({ + $or: [ + { + rundownId: rundownId, + }, + ], + }) + return segments + } + + private async softFindPart(segmentId: SegmentId, partId: string) { + const part = await Parts.findOneAsync({ + $or: [ + { _id: protectString(partId), segmentId }, + { + externalId: partId, + segmentId, + }, + ], + }) + return part + } + + private async findPart(segmentId: SegmentId, partId: string) { + const part = await this.softFindPart(segmentId, partId) + if (!part) { + throw new Meteor.Error(404, `Part ID '${partId}' was not found`) + } + return part + } + + private async findParts(segmentId: SegmentId) { + const parts = await Parts.findFetchAsync({ + $or: [{ segmentId }], + }) + return parts + } + + private async findStudio(studioId: StudioId) { + const studio = await Studios.findOneAsync({ _id: studioId }) + if (!studio) { + throw new Meteor.Error(500, `Studio '${studioId}' does not exist`) + } + + return studio + } + + private checkRundownSource(rundown: Rundown | undefined) { + if (rundown && rundown.source.type !== 'restApi') { + throw new Meteor.Error( + 403, + `Cannot replace existing rundown from source '${getRundownNrcsName( + rundown + )}' with new data from 'restApi' source` + ) + } + } + + // Playlists + + async getPlaylists( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId + ): Promise>> { + check(studioId, String) + + const studio = await this.findStudio(studioId) + const rawPlaylists = await RundownPlaylists.findFetchAsync({ studioId: studio._id }) + const playlists = rawPlaylists.map((rawPlaylist) => this.adaptPlaylist(rawPlaylist)) + + return ClientAPI.responseSuccess(playlists) + } + + async getPlaylist( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string + ): Promise> { + check(studioId, String) + check(playlistId, String) + + const studio = await this.findStudio(studioId) + const rawPlaylist = await this.findPlaylist(studio._id, playlistId) + const playlist = this.adaptPlaylist(rawPlaylist) + + return ClientAPI.responseSuccess(playlist) + } + + async deletePlaylists( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId + ): Promise> { + check(studioId, String) + + const rundowns = await Rundowns.findFetchAsync({}) + const studio = await this.findStudio(studioId) + + await Promise.all( + rundowns.map(async (rundown) => + runIngestOperation(studio._id, IngestJobs.RemoveRundown, { + rundownExternalId: rundown.externalId, + }) + ) + ) + + return ClientAPI.responseSuccess(undefined) + } + + async deletePlaylist( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string + ): Promise> { + check(studioId, String) + check(playlistId, String) + + const studio = await this.findStudio(studioId) + await this.findPlaylist(studio._id, playlistId) + + const rundowns = await Rundowns.findFetchAsync({ + $or: [{ playlistId: protectString(playlistId) }, { playlistExternalId: playlistId }], + }) + + await Promise.all( + rundowns.map(async (rundown) => + runIngestOperation(studio._id, IngestJobs.RemoveRundown, { + rundownExternalId: rundown.externalId, + }) + ) + ) + + return ClientAPI.responseSuccess(undefined) + } + + // Rundowns + + async getRundowns( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string + ): Promise>> { + check(studioId, String) + check(playlistId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rawRundowns = await this.findRundowns(studio._id, playlist._id) + const rundowns = rawRundowns.map((rawRundown) => this.adaptRundown(rawRundown)) + + return ClientAPI.responseSuccess(rundowns) + } + + async getRundown( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rawRundown = await this.findRundown(studio._id, playlist._id, rundownId) + const rundown = this.adaptRundown(rawRundown) + + return ClientAPI.responseSuccess(rundown) + } + + async postRundown( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + ingestRundown: RestApiIngestRundown + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(ingestRundown, Object) + + const studio = await this.findStudio(studioId) + + this.validateRundown(ingestRundown) + await this.validateAPIPayloadsForRundown(studio.blueprintId, ingestRundown) + + const existingRundown = await Rundowns.findOneAsync({ + $or: [ + { + _id: protectString(ingestRundown.externalId), + playlistId: protectString(playlistId), + studioId: studio._id, + }, + { + externalId: ingestRundown.externalId, + playlistExternalId: playlistId, + studioId: studio._id, + }, + ], + }) + if (existingRundown) { + throw new Meteor.Error(400, `Rundown '${ingestRundown.externalId}' already exists`) + } + + await runIngestOperation(studio._id, IngestJobs.UpdateRundown, { + rundownExternalId: ingestRundown.externalId, + ingestRundown: { ...ingestRundown, playlistExternalId: playlistId }, + isCreateAction: true, + rundownSource: { + type: 'restApi', + resyncUrl: ingestRundown.resyncUrl, + }, + }) + + return ClientAPI.responseSuccess(undefined) + } + + async putRundowns( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + ingestRundowns: RestApiIngestRundown[] + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(ingestRundowns, Array) + + const studio = await this.findStudio(studioId) + + await Promise.all( + ingestRundowns.map(async (ingestRundown, index) => { + this.validateRundown(ingestRundown) + return this.validateAPIPayloadsForRundown(studio.blueprintId, ingestRundown, { rundown: index }) + }) + ) + + const playlist = await this.findPlaylist(studio._id, playlistId) + + await Promise.all( + ingestRundowns.map(async (ingestRundown) => { + const rundownExternalId = ingestRundown.externalId + const existingRundown = await this.findRundown(studio._id, playlist._id, rundownExternalId) + if (!existingRundown) { + return + } + + this.checkRundownSource(existingRundown) + + return runIngestOperation(studio._id, IngestJobs.UpdateRundown, { + rundownExternalId: ingestRundown.externalId, + ingestRundown: { ...ingestRundown, playlistExternalId: playlist.externalId }, + isCreateAction: true, + rundownSource: { + type: 'restApi', + resyncUrl: ingestRundown.resyncUrl, + }, + }) + }) + ) + + return ClientAPI.responseSuccess(undefined) + } + + async putRundown( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + ingestRundown: RestApiIngestRundown + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(ingestRundown, Object) + + const studio = await this.findStudio(studioId) + + this.validateRundown(ingestRundown) + await this.validateAPIPayloadsForRundown(studio.blueprintId, ingestRundown) + + const playlist = await this.findPlaylist(studio._id, playlistId) + const existingRundown = await this.findRundown(studio._id, playlist._id, rundownId) + if (!existingRundown) { + throw new Meteor.Error(400, `Rundown '${rundownId}' does not exist`) + } + this.checkRundownSource(existingRundown) + + await runIngestOperation(studio._id, IngestJobs.UpdateRundown, { + rundownExternalId: existingRundown.externalId, + ingestRundown: { ...ingestRundown, playlistExternalId: playlist.externalId }, + isCreateAction: true, + rundownSource: { + type: 'restApi', + resyncUrl: ingestRundown.resyncUrl, + }, + }) + + return ClientAPI.responseSuccess(undefined) + } + + async deleteRundowns( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string + ): Promise> { + check(studioId, String) + check(playlistId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundowns = await this.findRundowns(studio._id, playlist._id) + + await Promise.all( + rundowns.map(async (rundown) => { + this.checkRundownSource(rundown) + return runIngestOperation(studio._id, IngestJobs.RemoveRundown, { + rundownExternalId: rundown.externalId, + }) + }) + ) + + return ClientAPI.responseSuccess(undefined) + } + + async deleteRundown( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + + await runIngestOperation(studio._id, IngestJobs.RemoveRundown, { + rundownExternalId: rundown.externalId, + }) + + return ClientAPI.responseSuccess(undefined) + } + + // Segments + + async getSegments( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string + ): Promise>> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const rawSegments = await this.findSegments(rundown._id) + const segments = rawSegments.map((rawSegment) => this.adaptSegment(rawSegment)) + + return ClientAPI.responseSuccess(segments) + } + + async getSegment( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(segmentId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const rawSegment = await this.findSegment(rundown._id, segmentId) + const segment = this.adaptSegment(rawSegment) + + return ClientAPI.responseSuccess(segment) + } + + async postSegment( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + ingestSegment: IngestSegment + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(ingestSegment, Object) + + const studio = await this.findStudio(studioId) + + this.validateSegment(ingestSegment) + await this.validateAPIPayloadsForSegment(studio.blueprintId, ingestSegment) + + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const existingSegment = await this.softFindSegment(rundown._id, ingestSegment.externalId) + if (existingSegment) { + throw new Meteor.Error(400, `Segment '${ingestSegment.externalId}' already exists`) + } + + await runIngestOperation(studio._id, IngestJobs.UpdateSegment, { + rundownExternalId: rundown.externalId, + isCreateAction: true, + ingestSegment, + }) + + return ClientAPI.responseSuccess(undefined) + } + + async putSegments( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + ingestSegments: IngestSegment[] + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(ingestSegments, Array) + + const studio = await this.findStudio(studioId) + + await Promise.all( + ingestSegments.map(async (ingestSegment, index) => { + this.validateSegment(ingestSegment) + return await this.validateAPIPayloadsForSegment(studio.blueprintId, ingestSegment, { + segment: index, + }) + }) + ) + + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + + await Promise.all( + ingestSegments.map(async (ingestSegment) => { + const segment = await this.findSegment(rundown._id, ingestSegment.externalId) + if (!segment) { + return + } + + const parts = await this.findParts(segment._id) + return Promise.all( + parts.map(async (part) => + runIngestOperation(studio._id, IngestJobs.RemovePart, { + partExternalId: part.externalId, + rundownExternalId: rundown.externalId, + segmentExternalId: segment.externalId, + }) + ) + ) + }) + ) + + await Promise.all( + ingestSegments.map(async (ingestSegment) => { + const existingSegment = await this.softFindSegment(rundown._id, ingestSegment.externalId) + if (!existingSegment) { + return null + } + + return runIngestOperation(studio._id, IngestJobs.UpdateSegment, { + rundownExternalId: rundown.externalId, + isCreateAction: true, + ingestSegment, + }) + }) + ) + + return ClientAPI.responseSuccess(undefined) + } + + async putSegment( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + ingestSegment: IngestSegment + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(segmentId, String) + check(ingestSegment, Object) + + const studio = await this.findStudio(studioId) + + this.validateSegment(ingestSegment) + await this.validateAPIPayloadsForSegment(studio.blueprintId, ingestSegment) + + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const segment = await this.softFindSegment(rundown._id, segmentId) + if (!segment) { + throw new Meteor.Error(400, `Segment '${segmentId}' does not exist`) + } + const parts = await this.findParts(segment._id) + + await Promise.all( + parts.map(async (part) => + runIngestOperation(studio._id, IngestJobs.RemovePart, { + partExternalId: part.externalId, + rundownExternalId: rundown.externalId, + segmentExternalId: segment.externalId, + }) + ) + ) + + await runIngestOperation(studio._id, IngestJobs.UpdateSegment, { + rundownExternalId: rundown.externalId, + isCreateAction: true, + ingestSegment, + }) + + return ClientAPI.responseSuccess(undefined) + } + + async deleteSegments( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + const segments = await this.findSegments(rundown._id) + + await Promise.all( + segments.map(async (segment) => + // This also removes linked Parts + runIngestOperation(studio._id, IngestJobs.RemoveSegment, { + rundownExternalId: rundown.externalId, + segmentExternalId: segment.externalId, + }) + ) + ) + + return ClientAPI.responseSuccess(undefined) + } + + async deleteSegment( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(segmentId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const segment = await this.findSegment(rundown._id, segmentId) + + // This also removes linked Parts + await runIngestOperation(studio._id, IngestJobs.RemoveSegment, { + segmentExternalId: segment.externalId, + rundownExternalId: rundown.externalId, + }) + + return ClientAPI.responseSuccess(undefined) + } + + // Parts + + async getParts( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string + ): Promise>> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(segmentId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const segment = await this.findSegment(rundown._id, segmentId) + const rawParts = await this.findParts(segment._id) + const parts = rawParts.map((rawPart) => this.adaptPart(rawPart)) + + return ClientAPI.responseSuccess(parts) + } + + async getPart( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + partId: string + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(segmentId, String) + check(partId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const segment = await this.findSegment(rundown._id, segmentId) + const rawPart = await this.findPart(segment._id, partId) + const part = this.adaptPart(rawPart) + + return ClientAPI.responseSuccess(part) + } + + async postPart( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + ingestPart: IngestPart + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(segmentId, String) + check(ingestPart, Object) + + const studio = await this.findStudio(studioId) + + this.validatePart(ingestPart) + await this.validateAPIPayloadsForPart(studio.blueprintId, ingestPart) + + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const segment = await this.findSegment(rundown._id, segmentId) + const existingPart = await this.softFindPart(segment._id, ingestPart.externalId) + if (existingPart) { + throw new Meteor.Error(400, `Part '${ingestPart.externalId}' already exists`) + } + + await runIngestOperation(studio._id, IngestJobs.UpdatePart, { + rundownExternalId: rundown.externalId, + segmentExternalId: segment.externalId, + isCreateAction: true, + ingestPart, + }) + + return ClientAPI.responseSuccess(undefined) + } + + async putParts( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + ingestParts: IngestPart[] + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(segmentId, String) + check(ingestParts, Array) + + const studio = await this.findStudio(studioId) + + await Promise.all( + ingestParts.map(async (ingestPart, index) => { + this.validatePart(ingestPart) + return this.validateAPIPayloadsForPart(studio.blueprintId, ingestPart, { part: index }) + }) + ) + + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const segment = await this.findSegment(rundown._id, segmentId) + + await Promise.all( + ingestParts.map(async (ingestPart) => { + const existingPart = await this.findPart(segment._id, ingestPart.externalId) + if (!existingPart) { + return + } + + return runIngestOperation(studio._id, IngestJobs.UpdatePart, { + segmentExternalId: segment.externalId, + rundownExternalId: rundown.externalId, + isCreateAction: true, + ingestPart, + }) + }) + ) + + return ClientAPI.responseSuccess(undefined) + } + + async putPart( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + partId: string, + ingestPart: IngestPart + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(segmentId, String) + check(partId, String) + check(ingestPart, Object) + + const studio = await this.findStudio(studioId) + + this.validatePart(ingestPart) + await this.validateAPIPayloadsForPart(studio.blueprintId, ingestPart) + + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const segment = await this.findSegment(rundown._id, segmentId) + const existingPart = await this.findPart(segment._id, partId) + if (!existingPart) { + throw new Meteor.Error(400, `Part '${partId}' does not exists`) + } + + await runIngestOperation(studio._id, IngestJobs.UpdatePart, { + rundownExternalId: rundown.externalId, + segmentExternalId: segment.externalId, + isCreateAction: true, + ingestPart, + }) + + return ClientAPI.responseSuccess(undefined) + } + + async deleteParts( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(segmentId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const segment = await this.findSegment(rundown._id, segmentId) + const parts = await this.findParts(segment._id) + + await Promise.all( + parts.map(async (part) => + runIngestOperation(studio._id, IngestJobs.RemovePart, { + rundownExternalId: rundown.externalId, + segmentExternalId: segment.externalId, + partExternalId: part.externalId, + }) + ) + ) + + return ClientAPI.responseSuccess(undefined) + } + + async deletePart( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + partId: string + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(segmentId, String) + check(partId, String) + + const studio = await this.findStudio(studioId) + const playlist = await this.findPlaylist(studio._id, playlistId) + const rundown = await this.findRundown(studio._id, playlist._id, rundownId) + this.checkRundownSource(rundown) + const segment = await this.findSegment(rundown._id, segmentId) + const part = await this.findPart(segment._id, partId) + + await runIngestOperation(studio._id, IngestJobs.RemovePart, { + rundownExternalId: rundown.externalId, + segmentExternalId: segment.externalId, + partExternalId: part.externalId, + }) + + return ClientAPI.responseSuccess(undefined) + } +} + +class IngestAPIFactory implements APIFactory { + createServerAPI(_context: ServerAPIContext): IngestRestAPI { + return new IngestServerAPI() + } +} + +export function registerRoutes(registerRoute: APIRegisterHook): void { + const ingestAPIFactory = new IngestAPIFactory() + + // Playlists + + // Get all playlists + registerRoute<{ studioId: string }, never, Array>( + 'get', + '/ingest/:studioId/playlists', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API GET: Playlists`) + + const studioId = protectString(params.studioId) + check(studioId, String) + + return await serverAPI.getPlaylists(connection, event, studioId) + } + ) + + // Get playlist + registerRoute<{ studioId: string; playlistId: string }, never, PlaylistResponse>( + 'get', + '/ingest/:studioId/playlists/:playlistId', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API GET: Playlist`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + + return await serverAPI.getPlaylist(connection, event, studioId, playlistId) + } + ) + + // Delete all playlists + registerRoute<{ studioId: string }, never, void>( + 'delete', + '/ingest/:studioId/playlists', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API DELETE: Playlists`) + + const studioId = protectString(params.studioId) + check(studioId, String) + + return await serverAPI.deletePlaylists(connection, event, studioId) + } + ) + + // Delete playlist + registerRoute<{ studioId: string; playlistId: string }, never, void>( + 'delete', + '/ingest/:studioId/playlists/:playlistId', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API DELETE: Playlist`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + + return await serverAPI.deletePlaylist(connection, event, studioId, playlistId) + } + ) + + // Rundowns + + // Get all rundowns + registerRoute<{ studioId: string; playlistId: string }, never, RundownResponse[]>( + 'get', + '/ingest/:studioId/playlists/:playlistId/rundowns', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API GET: Rundowns`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + + return await serverAPI.getRundowns(connection, event, studioId, playlistId) + } + ) + + // Get rundown + registerRoute<{ studioId: string; playlistId: string; rundownId: string }, never, RundownResponse>( + 'get', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API GET: Rundown`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + + return await serverAPI.getRundown(connection, event, studioId, playlistId, rundownId) + } + ) + + // Create rundown + registerRoute<{ studioId: string; playlistId: string; rundownId: string }, never, void>( + 'post', + '/ingest/:studioId/playlists/:playlistId/rundowns', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, body) => { + logger.info(`INGEST API POST: Rundowns`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + + const ingestRundown = body as RestApiIngestRundown + if (!ingestRundown) throw new Meteor.Error(400, 'Upload rundown: Missing request body') + if (typeof ingestRundown !== 'object') throw new Meteor.Error(400, 'Upload rundown: Invalid request body') + + return await serverAPI.postRundown(connection, event, studioId, playlistId, ingestRundown) + } + ) + + // Update rundowns + registerRoute<{ studioId: string; playlistId: string }, never, void>( + 'put', + '/ingest/:studioId/playlists/:playlistId/rundowns', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, body) => { + logger.info(`INGEST API PUT: Rundowns`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + + const ingestRundowns = body as RestApiIngestRundown[] + if (!ingestRundowns) throw new Meteor.Error(400, 'Upload rundown: Missing request body') + if (typeof ingestRundowns !== 'object') throw new Meteor.Error(400, 'Upload rundown: Invalid request body') + + return await serverAPI.putRundowns(connection, event, studioId, playlistId, ingestRundowns) + } + ) + + // Update rundown + registerRoute<{ studioId: string; playlistId: string; rundownId: string }, never, void>( + 'put', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, body) => { + logger.info(`INGEST API PUT: Rundown`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + + const ingestRundown = body as RestApiIngestRundown + if (!ingestRundown) throw new Meteor.Error(400, 'Upload rundown: Missing request body') + if (typeof ingestRundown !== 'object') throw new Meteor.Error(400, 'Upload rundown: Invalid request body') + + return await serverAPI.putRundown(connection, event, studioId, playlistId, rundownId, ingestRundown) + } + ) + + // Delete rundowns + registerRoute<{ studioId: string; playlistId: string }, never, void>( + 'delete', + '/ingest/:studioId/playlists/:playlistId/rundowns', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API DELETE: Rundowns`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + + return await serverAPI.deleteRundowns(connection, event, studioId, playlistId) + } + ) + + // Delete rundown + registerRoute<{ studioId: string; playlistId: string; rundownId: string }, never, void>( + 'delete', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API DELETE: Rundown`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + + return await serverAPI.deleteRundown(connection, event, studioId, playlistId, rundownId) + } + ) + + // Segments + + // Get all segments + registerRoute<{ studioId: string; playlistId: string; rundownId: string }, never, SegmentResponse[]>( + 'get', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API GET: Segments`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + + return await serverAPI.getSegments(connection, event, studioId, playlistId, rundownId) + } + ) + + // Get segment + registerRoute< + { studioId: string; playlistId: string; rundownId: string; segmentId: string }, + never, + SegmentResponse + >( + 'get', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments/:segmentId', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API GET: Segment`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + const segmentId = params.segmentId + check(segmentId, String) + + return await serverAPI.getSegment(connection, event, studioId, playlistId, rundownId, segmentId) + } + ) + + // Create segment + registerRoute<{ studioId: string; playlistId: string; rundownId: string }, never, void>( + 'post', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, body) => { + logger.info(`INGEST API POST: Segments`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + + const ingestSegment = body as IngestSegment + if (!ingestSegment) throw new Meteor.Error(400, 'Upload rundown: Missing request body') + + return await serverAPI.postSegment(connection, event, studioId, playlistId, rundownId, ingestSegment) + } + ) + + // Update segments + registerRoute<{ studioId: string; playlistId: string; rundownId: string }, never, void>( + 'put', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, body) => { + logger.info(`INGEST API PUT: Segments`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + + const ingestSegments = body as IngestSegment[] + if (!ingestSegments) throw new Meteor.Error(400, 'Upload rundown: Missing request body') + if (!Array.isArray(ingestSegments)) throw new Meteor.Error(400, 'Upload rundown: Invalid request body') + + return await serverAPI.putSegments(connection, event, studioId, playlistId, rundownId, ingestSegments) + } + ) + + // Update segment + registerRoute<{ studioId: string; playlistId: string; rundownId: string; segmentId: string }, never, void>( + 'put', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments/:segmentId', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, body) => { + logger.info(`INGEST API PUT: Segment`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + const segmentId = params.segmentId + check(segmentId, String) + + const ingestSegment = body as IngestSegment + if (!ingestSegment) throw new Meteor.Error(400, 'Upload rundown: Missing request body') + + return await serverAPI.putSegment( + connection, + event, + studioId, + playlistId, + rundownId, + segmentId, + ingestSegment + ) + } + ) + + // Delete segments + registerRoute<{ studioId: string; playlistId: string; rundownId: string }, never, void>( + 'delete', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API DELETE: Segments`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + + return await serverAPI.deleteSegments(connection, event, studioId, playlistId, rundownId) + } + ) + + // Delete segment + registerRoute<{ studioId: string; playlistId: string; rundownId: string; segmentId: string }, never, void>( + 'delete', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments/:segmentId', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API DELETE: Segment`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + const segmentId = params.segmentId + check(segmentId, String) + + return await serverAPI.deleteSegment(connection, event, studioId, playlistId, rundownId, segmentId) + } + ) + + // Parts + + // Get all parts + registerRoute< + { studioId: string; playlistId: string; rundownId: string; segmentId: string }, + never, + PartResponse[] + >( + 'get', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments/:segmentId/parts', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API GET: Parts`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + const segmentId = params.segmentId + check(segmentId, String) + + return await serverAPI.getParts(connection, event, studioId, playlistId, rundownId, segmentId) + } + ) + + // Get part + registerRoute< + { studioId: string; playlistId: string; rundownId: string; segmentId: string; partId: string }, + never, + PartResponse + >( + 'get', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments/:segmentId/parts/:partId', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API GET: Part`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + const segmentId = params.segmentId + check(segmentId, String) + const partId = params.partId + check(partId, String) + + return await serverAPI.getPart(connection, event, studioId, playlistId, rundownId, segmentId, partId) + } + ) + + // Create part + registerRoute<{ studioId: string; playlistId: string; rundownId: string; segmentId: string }, never, void>( + 'post', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments/:segmentId/parts', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, body) => { + logger.info(`INGEST API POST: Parts`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + const segmentId = params.segmentId + check(segmentId, String) + + const ingestPart = body as IngestPart + if (!ingestPart) throw new Meteor.Error(400, 'Upload rundown: Missing request body') + + return await serverAPI.postPart(connection, event, studioId, playlistId, rundownId, segmentId, ingestPart) + } + ) + + // Update parts + registerRoute<{ studioId: string; playlistId: string; rundownId: string; segmentId: string }, never, void>( + 'put', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments/:segmentId/parts', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, body) => { + logger.info(`INGEST API PUT: Parts`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + const segmentId = params.segmentId + check(segmentId, String) + + const ingestParts = body as IngestPart[] + if (!ingestParts) throw new Meteor.Error(400, 'Upload rundown: Missing request body') + if (!Array.isArray(ingestParts)) throw new Meteor.Error(400, 'Upload rundown: Invalid request body') + + return await serverAPI.putParts(connection, event, studioId, playlistId, rundownId, segmentId, ingestParts) + } + ) + + // Update part + registerRoute< + { studioId: string; playlistId: string; rundownId: string; segmentId: string; partId: string }, + never, + void + >( + 'put', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments/:segmentId/parts/:partId', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, body) => { + logger.info(`INGEST API PUT: Part`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + const segmentId = params.segmentId + check(segmentId, String) + const partId = params.partId + check(partId, String) + + const ingestPart = body as IngestPart + if (!ingestPart) throw new Meteor.Error(400, 'Upload rundown: Missing request body') + + return await serverAPI.putPart( + connection, + event, + studioId, + playlistId, + rundownId, + segmentId, + partId, + ingestPart + ) + } + ) + + // Delete parts + registerRoute<{ studioId: string; playlistId: string; rundownId: string; segmentId: string }, never, void>( + 'delete', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments/:segmentId/parts', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API DELETE: Parts`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + const segmentId = params.segmentId + check(segmentId, String) + + return await serverAPI.deleteParts(connection, event, studioId, playlistId, rundownId, segmentId) + } + ) + + // Delete part + registerRoute< + { studioId: string; playlistId: string; rundownId: string; segmentId: string; partId: string }, + never, + void + >( + 'delete', + '/ingest/:studioId/playlists/:playlistId/rundowns/:rundownId/segments/:segmentId/parts/:partId', + new Map(), + ingestAPIFactory, + async (serverAPI, connection, event, params, _) => { + logger.info(`INGEST API DELETE: Part`) + + const studioId = protectString(params.studioId) + check(studioId, String) + const playlistId = params.playlistId + check(playlistId, String) + const rundownId = params.rundownId + check(rundownId, String) + const segmentId = params.segmentId + check(segmentId, String) + const partId = params.partId + check(partId, String) + + return await serverAPI.deletePart(connection, event, studioId, playlistId, rundownId, segmentId, partId) + } + ) +} diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 2616f2e6f93..cdd3e69174b 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -10,6 +10,7 @@ import { PartInstanceId, PieceId, RundownBaselineAdLibActionId, + RundownId, RundownPlaylistId, SegmentId, } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -23,13 +24,15 @@ import { BucketAdLibActions, BucketAdLibs, Buckets, + Parts, RundownBaselineAdLibActions, RundownBaselineAdLibPieces, RundownPlaylists, + Segments, } from '../../../collections' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { ServerClientAPI } from '../../client' -import { QueueNextSegmentResult, StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' +import { QueueNextSegmentResult, StudioJobs, TakeNextPartResult } from '@sofie-automation/corelib/dist/worker/studio' import { getCurrentTime } from '../../../lib/lib' import { TriggerReloadDataResponse } from '@sofie-automation/meteor-lib/dist/api/userActions' import { ServerRundownAPI } from '../../rundown' @@ -38,6 +41,48 @@ import { triggerWriteAccess } from '../../../security/securityVerify' class PlaylistsServerAPI implements PlaylistsRestAPI { constructor(private context: ServerAPIContext) {} + private async findPlaylist(playlistId: RundownPlaylistId) { + const playlist = await RundownPlaylists.findOneAsync({ + $or: [{ _id: playlistId }, { externalId: playlistId }], + }) + if (!playlist) { + throw new Meteor.Error(404, `Playlist ID '${playlistId}' was not found`) + } + return playlist + } + + private async findSegment(segmentId: SegmentId) { + const segment = await Segments.findOneAsync({ + $or: [ + { + _id: segmentId, + }, + { + externalId: segmentId, + }, + ], + }) + if (!segment) { + throw new Meteor.Error(404, `Segment ID '${segmentId}' was not found`) + } + return segment + } + + private async findPart(partId: PartId) { + const part = await Parts.findOneAsync({ + $or: [ + { _id: partId }, + { + externalId: partId, + }, + ], + }) + if (!part) { + throw new Meteor.Error(404, `Part ID '${partId}' was not found`) + } + return part + } + async getAllRundownPlaylists( _connection: Meteor.Connection, _event: string @@ -60,26 +105,30 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { rundownPlaylistId: RundownPlaylistId, rehearsal: boolean ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) + check(playlist._id, String) check(rehearsal, Boolean) }, StudioJobs.ActivateRundownPlaylist, { - playlistId: rundownPlaylistId, + playlistId: playlist._id, rehearsal, } ) } - async deactivate( + + async activateAdLibTesting( connection: Meteor.Connection, event: string, - rundownPlaylistId: RundownPlaylistId + rundownPlaylistId: RundownPlaylistId, + rundownId: RundownId ): Promise> { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), @@ -88,13 +137,38 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { rundownPlaylistId, () => { check(rundownPlaylistId, String) + check(rundownId, String) }, - StudioJobs.DeactivateRundownPlaylist, + StudioJobs.ActivateAdlibTesting, { playlistId: rundownPlaylistId, + rundownId: rundownId, } ) } + + async deactivate( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId + ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + playlist._id, + () => { + check(playlist._id, String) + }, + StudioJobs.DeactivateRundownPlaylist, + { + playlistId: playlist._id, + } + ) + } + async executeAdLib( connection: Meteor.Connection, event: string, @@ -126,9 +200,12 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { if (regularAdLibDoc) { // This is an AdLib Piece const pieceType = baselineAdLibDoc ? 'baseline' : segmentAdLibDoc ? 'normal' : 'bucket' - const rundownPlaylist = await RundownPlaylists.findOneAsync(rundownPlaylistId, { - projection: { currentPartInfo: 1 }, - }) + const rundownPlaylist = await RundownPlaylists.findOneAsync( + { $or: [{ _id: rundownPlaylistId }, { externalId: rundownPlaylistId }] }, + { + projection: { currentPartInfo: 1 }, + } + ) if (!rundownPlaylist) return ClientAPI.responseError( UserError.from( @@ -152,14 +229,14 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + rundownPlaylist._id, () => { - check(rundownPlaylistId, String) + check(rundownPlaylist._id, String) check(adLibId, Match.OneOf(String, null)) }, StudioJobs.AdlibPieceStart, { - playlistId: rundownPlaylistId, + playlistId: rundownPlaylist._id, adLibPieceId: regularAdLibDoc._id, partInstanceId: rundownPlaylist.currentPartInfo.partInstanceId, pieceType, @@ -169,9 +246,12 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { return ClientAPI.responseSuccess({}) } else if (adLibActionDoc) { // This is an AdLib Action - const rundownPlaylist = await RundownPlaylists.findOneAsync(rundownPlaylistId, { - projection: { currentPartInfo: 1, activationId: 1 }, - }) + const rundownPlaylist = await RundownPlaylists.findOneAsync( + { $or: [{ _id: rundownPlaylistId }, { externalId: rundownPlaylistId }] }, + { + projection: { currentPartInfo: 1, activationId: 1 }, + } + ) if (!rundownPlaylist) return ClientAPI.responseError( @@ -205,14 +285,14 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + rundownPlaylist._id, () => { - check(rundownPlaylistId, String) + check(rundownPlaylist._id, String) check(adLibId, Match.OneOf(String, null)) }, StudioJobs.ExecuteAction, { - playlistId: rundownPlaylistId, + playlistId: rundownPlaylist._id, actionDocId: adLibActionDoc._id, actionId: adLibActionDoc.actionId, userData: adLibActionDoc.userData, @@ -226,6 +306,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { ) } } + async executeBucketAdLib( connection: Meteor.Connection, event: string, @@ -234,6 +315,8 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { externalId: string, triggerMode?: string | null ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + const bucketPromise = Buckets.findOneAsync(bucketId, { projection: { _id: 1 } }) const bucketAdlibPromise = BucketAdLibs.findOneAsync({ bucketId, externalId }, { projection: { _id: 1 } }) const bucketAdlibActionPromise = BucketAdLibActions.findOneAsync( @@ -272,21 +355,22 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) + check(playlist._id, String) check(bucketId, String) check(externalId, String) }, StudioJobs.ExecuteBucketAdLibOrAction, { - playlistId: rundownPlaylistId, + playlistId: playlist._id, bucketId, externalId, triggerMode: triggerMode ?? undefined, } ) } + async moveNextPart( connection: Meteor.Connection, event: string, @@ -294,42 +378,47 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { delta: number, ignoreQuickLoop?: boolean ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) + check(playlist._id, String) check(delta, Number) }, StudioJobs.MoveNextPart, { - playlistId: rundownPlaylistId, + playlistId: playlist._id, partDelta: delta, segmentDelta: 0, ignoreQuickLoop, } ) } + async moveNextSegment( connection: Meteor.Connection, event: string, rundownPlaylistId: RundownPlaylistId, delta: number ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) + check(playlist._id, String) check(delta, Number) }, StudioJobs.MoveNextPart, { - playlistId: rundownPlaylistId, + playlistId: playlist._id, partDelta: 0, segmentDelta: delta, } @@ -341,16 +430,18 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { event: string, rundownPlaylistId: RundownPlaylistId ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + return ServerClientAPI.runUserActionInLogForPlaylist( this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) + check(playlist._id, String) }, 'reloadPlaylist', - { rundownPlaylistId }, + { rundownPlaylistId: playlist._id }, async (access) => { const reloadResponse = await ServerRundownAPI.resyncRundownPlaylist(access) const success = !reloadResponse.rundownsResponses.reduce((missing, rundownsResponse) => { @@ -358,7 +449,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { }, false) if (!success) throw UserError.from( - new Error(`Failed to reload playlist ${rundownPlaylistId}`), + new Error(`Failed to reload playlist ${playlist._id}`), UserErrorMessage.InternalError ) } @@ -370,17 +461,19 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { event: string, rundownPlaylistId: RundownPlaylistId ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) + check(playlist._id, String) }, StudioJobs.ResetRundownPlaylist, { - playlistId: rundownPlaylistId, + playlistId: playlist._id, } ) } @@ -390,19 +483,22 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { rundownPlaylistId: RundownPlaylistId, segmentId: SegmentId ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + const segment = await this.findSegment(segmentId) + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) - check(segmentId, String) + check(playlist._id, String) + check(segment._id, String) }, StudioJobs.SetNextSegment, { - playlistId: rundownPlaylistId, - nextSegmentId: segmentId, + playlistId: playlist._id, + nextSegmentId: segment._id, } ) } @@ -412,19 +508,22 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { rundownPlaylistId: RundownPlaylistId, partId: PartId ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + const part = await this.findPart(partId) + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) - check(partId, String) + check(playlist._id, String) + check(part._id, String) }, StudioJobs.SetNextPart, { - playlistId: rundownPlaylistId, - nextPartId: partId, + playlistId: playlist._id, + nextPartId: part._id, } ) } @@ -435,19 +534,22 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { rundownPlaylistId: RundownPlaylistId, segmentId: SegmentId ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + const segment = await this.findSegment(segmentId) + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) - check(segmentId, String) + check(playlist._id, String) + check(segment._id, String) }, StudioJobs.QueueNextSegment, { - playlistId: rundownPlaylistId, - queuedSegmentId: segmentId, + playlistId: playlist._id, + queuedSegmentId: segment._id, } ) } @@ -457,23 +559,22 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { event: string, rundownPlaylistId: RundownPlaylistId, fromPartInstanceId: PartInstanceId | undefined - ): Promise> { + ): Promise> { triggerWriteAccess() - const rundownPlaylist = await RundownPlaylists.findOneAsync(rundownPlaylistId) - if (!rundownPlaylist) throw new Error(`Rundown playlist ${rundownPlaylistId} does not exist`) + const playlist = await this.findPlaylist(rundownPlaylistId) return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) + check(playlist._id, String) }, StudioJobs.TakeNextPart, { - playlistId: rundownPlaylistId, - fromPartInstanceId: fromPartInstanceId ?? rundownPlaylist.currentPartInfo?.partInstanceId ?? null, + playlistId: playlist._id, + fromPartInstanceId: fromPartInstanceId ?? playlist.currentPartInfo?.partInstanceId ?? null, } ) } @@ -484,8 +585,8 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { rundownPlaylistId: RundownPlaylistId, sourceLayerIds: string[] ): Promise> { - const rundownPlaylist = await RundownPlaylists.findOneAsync(rundownPlaylistId) - if (!rundownPlaylist) + const playlist = await this.findPlaylist(rundownPlaylistId) + if (!playlist) return ClientAPI.responseError( UserError.from( Error(`Rundown playlist ${rundownPlaylistId} does not exist`), @@ -494,7 +595,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { 412 ) ) - if (!rundownPlaylist.currentPartInfo?.partInstanceId || !rundownPlaylist.activationId) + if (!playlist.currentPartInfo?.partInstanceId || !playlist.activationId) return ClientAPI.responseError( UserError.from( new Error(`Rundown playlist ${rundownPlaylistId} is not currently active`), @@ -508,15 +609,15 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) + check(playlist._id, String) check(sourceLayerIds, [String]) }, StudioJobs.StopPiecesOnSourceLayers, { - playlistId: rundownPlaylistId, - partInstanceId: rundownPlaylist.currentPartInfo.partInstanceId, + playlistId: playlist._id, + partInstanceId: playlist.currentPartInfo.partInstanceId, sourceLayerIds: sourceLayerIds, } ) @@ -528,18 +629,20 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { rundownPlaylistId: RundownPlaylistId, sourceLayerId: string ): Promise> { + const playlist = await this.findPlaylist(rundownPlaylistId) + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this.context.getMethodContext(connection), event, getCurrentTime(), - rundownPlaylistId, + playlist._id, () => { - check(rundownPlaylistId, String) + check(playlist._id, String) check(sourceLayerId, String) }, StudioJobs.StartStickyPieceOnSourceLayer, { - playlistId: rundownPlaylistId, + playlistId: playlist._id, sourceLayerId, } ) @@ -584,6 +687,25 @@ export function registerRoutes(registerRoute: APIRegisterHook) } ) + registerRoute<{ playlistId: string; rundownId: string }, { rehearsal: boolean }, void>( + 'put', + '/playlists/:playlistId/rundowns/:rundownId/activate-adlib-testing', + new Map([ + [404, [UserErrorMessage.RundownPlaylistNotFound]], + [412, [UserErrorMessage.RundownAlreadyActive]], + ]), + playlistsAPIFactory, + async (serverAPI, connection, event, params, _body) => { + const rundownPlaylistId = protectString(params.playlistId) + const rundownId = protectString(params.rundownId) + logger.info(`API PUT: activate AdLib testing mode, playlist ${rundownPlaylistId}, rundown ${rundownId}`) + + check(rundownPlaylistId, String) + check(rundownId, String) + return await serverAPI.activateAdLibTesting(connection, event, rundownPlaylistId, rundownId) + } + ) + registerRoute<{ playlistId: string }, never, void>( 'put', '/playlists/:playlistId/deactivate', @@ -801,7 +923,7 @@ export function registerRoutes(registerRoute: APIRegisterHook) } ) - registerRoute<{ playlistId: string }, { fromPartInstanceId?: string }, void>( + registerRoute<{ playlistId: string }, { fromPartInstanceId?: string }, TakeNextPartResult>( 'post', '/playlists/:playlistId/take', new Map([ diff --git a/meteor/server/api/rest/v1/system.ts b/meteor/server/api/rest/v1/system.ts index 99c2609cf90..4a84b91fbf8 100644 --- a/meteor/server/api/rest/v1/system.ts +++ b/meteor/server/api/rest/v1/system.ts @@ -8,7 +8,6 @@ import { BlueprintId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { Meteor } from 'meteor/meteor' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { MeteorCall } from '../../methods' -import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration' class SystemServerAPI implements SystemRestAPI { async assignSystemBlueprint( @@ -33,36 +32,20 @@ class SystemServerAPI implements SystemRestAPI { const migrationStatus = await MeteorCall.migration.getMigrationStatus() if (!migrationStatus.migrationNeeded) return ClientAPI.responseSuccess({ inputs: [] }) - const requiredInputs: PendingMigrations = [] - for (const migration of migrationStatus.migration.manualInputs) { - if (migration.stepId && migration.attribute) { - requiredInputs.push({ - stepId: migration.stepId, - attributeId: migration.attribute, - }) - } - } - - return ClientAPI.responseSuccess({ inputs: requiredInputs }) + // Inputs are no longer supported, but need to be preserved for api compatibility + return ClientAPI.responseSuccess({ inputs: [] }) } async applyPendingMigrations( _connection: Meteor.Connection, - _event: string, - inputs: MigrationData + _event: string ): Promise> { const migrationStatus = await MeteorCall.migration.getMigrationStatus() if (!migrationStatus.migrationNeeded) throw new Error(`Migration does not need to be applied`) - const migrationData: MigrationStepInputResult[] = inputs.map((input) => ({ - stepId: input.stepId, - attribute: input.attributeId, - value: input.migrationValue, - })) const result = await MeteorCall.migration.runMigration( migrationStatus.migration.chunks, - migrationStatus.migration.hash, - migrationData + migrationStatus.migration.hash ) if (result.migrationCompleted) return ClientAPI.responseSuccess(undefined) throw new Error(`Unknown error occurred`) @@ -95,12 +78,10 @@ export function registerRoutes(registerRoute: APIRegisterHook): v '/system/migrations', new Map([[400, [UserErrorMessage.NoMigrationsToApply]]]), systemAPIFactory, - async (serverAPI, connection, event, _params, body) => { - const inputs = body.inputs + async (serverAPI, connection, event, _params, _body) => { logger.info(`API POST: System migrations`) - check(inputs, Array) - return await serverAPI.applyPendingMigrations(connection, event, inputs) + return await serverAPI.applyPendingMigrations(connection, event) } ) diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index 9742c5815ba..6d7f3b61888 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -359,8 +359,10 @@ export async function buildStudioFromResolved({ _rundownVersionHash: '', routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], + packageContainerSettingsWithOverrides: wrapDefaultObject({ + previewContainerIds: [], + thumbnailContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), @@ -755,3 +757,54 @@ export function playlistSnapshotOptionsFrom(options: APIPlaylistSnapshotOptions) withTimeline: !!options.withTimeline, } } + +export async function validateAPIRundownPayload( + blueprintId: BlueprintId | undefined, + rundownPayload: unknown +): Promise { + const blueprint = await getBlueprint(blueprintId, BlueprintManifestType.STUDIO) + const blueprintManifest = evalBlueprint(blueprint) as StudioBlueprintManifest + + if (typeof blueprintManifest.validateRundownPayloadFromAPI !== 'function') { + logger.info(`Blueprint ${blueprintManifest.blueprintId} does not support rundown payload validation`) + return [] + } + + const blueprintContext = new CommonContext('validateAPIRundownPayload', `blueprint:${blueprint._id}`) + + return blueprintManifest.validateRundownPayloadFromAPI(blueprintContext, rundownPayload) +} + +export async function validateAPISegmentPayload( + blueprintId: BlueprintId | undefined, + segmentPayload: unknown +): Promise { + const blueprint = await getBlueprint(blueprintId, BlueprintManifestType.STUDIO) + const blueprintManifest = evalBlueprint(blueprint) as StudioBlueprintManifest + + if (typeof blueprintManifest.validateSegmentPayloadFromAPI !== 'function') { + logger.info(`Blueprint ${blueprintManifest.blueprintId} does not support segment payload validation`) + return [] + } + + const blueprintContext = new CommonContext('validateAPISegmentPayload', `blueprint:${blueprint._id}`) + + return blueprintManifest.validateSegmentPayloadFromAPI(blueprintContext, segmentPayload) +} + +export async function validateAPIPartPayload( + blueprintId: BlueprintId | undefined, + partPayload: unknown +): Promise { + const blueprint = await getBlueprint(blueprintId, BlueprintManifestType.STUDIO) + const blueprintManifest = evalBlueprint(blueprint) as StudioBlueprintManifest + + if (typeof blueprintManifest.validatePartPayloadFromAPI !== 'function') { + logger.info(`Blueprint ${blueprintManifest.blueprintId} does not support part payload validation`) + return [] + } + + const blueprintContext = new CommonContext('validateAPIPartPayload', `blueprint:${blueprint._id}`) + + return blueprintManifest.validatePartPayloadFromAPI(blueprintContext, partPayload) +} diff --git a/meteor/server/api/snapshot.ts b/meteor/server/api/snapshot.ts index fb1131133db..ce366e2a8ce 100644 --- a/meteor/server/api/snapshot.ts +++ b/meteor/server/api/snapshot.ts @@ -229,7 +229,8 @@ async function createSystemSnapshot(options: SystemSnapshotOptions): Promise { _id: snapshotId, type: SnapshotType.DEBUG, created: getCurrentTime(), - name: `Debug_${studioId}_${formatDateTime(getCurrentTime())}`, + name: `Debug: ${studioId}`, + longname: `Debug_${studioId}_${formatDateTime(getCurrentTime())}`, version: CURRENT_SYSTEM_VERSION, }, system: systemSnapshot, @@ -411,7 +413,8 @@ async function createRundownPlaylistSnapshot( type: SnapshotType.RUNDOWNPLAYLIST, playlistId: playlist._id, studioId: playlist.studioId, - name: `Rundown_${playlist.name}_${playlist._id}_${formatDateTime(getCurrentTime())}`, + name: playlist.name, + longname: `Rundown_${playlist.name}_${playlist._id}_${formatDateTime(getCurrentTime())}`, version: CURRENT_SYSTEM_VERSION, }, @@ -426,7 +429,7 @@ async function createRundownPlaylistSnapshot( async function storeSnaphot(snapshot: { snapshot: SnapshotBase }, comment: string): Promise { const storePath = getSystemStorePath() - const fileName = fixValidPath(snapshot.snapshot.name) + '.json' + const fileName = fixValidPath(snapshot.snapshot.longname) + '.json' const filePath = Path.join(storePath, fileName) const str = JSON.stringify(snapshot) @@ -444,6 +447,7 @@ async function storeSnaphot(snapshot: { snapshot: SnapshotBase }, comment: strin type: snapshot.snapshot.type, created: snapshot.snapshot.created, name: snapshot.snapshot.name, + longname: snapshot.snapshot.longname, description: snapshot.snapshot.description, version: CURRENT_SYSTEM_VERSION, comment: comment, @@ -780,7 +784,7 @@ async function handleKoaResponse( const snapshot = await snapshotFcn() ctx.response.type = 'application/json' - ctx.response.attachment(`${snapshot.snapshot.name}.json`) + ctx.response.attachment(`${snapshot.snapshot.longname || snapshot.snapshot.name}.json`) ctx.response.status = 200 ctx.response.body = JSON.stringify(snapshot, null, 4) } catch (e) { diff --git a/meteor/server/api/studio/api.ts b/meteor/server/api/studio/api.ts index 0772e382fed..5e9a3d94c30 100644 --- a/meteor/server/api/studio/api.ts +++ b/meteor/server/api/studio/api.ts @@ -69,8 +69,10 @@ export async function insertStudioInner(newId?: StudioId): Promise { routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - thumbnailContainerIds: [], - previewContainerIds: [], + packageContainerSettingsWithOverrides: wrapDefaultObject({ + thumbnailContainerIds: [], + previewContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 96b3a66a6c6..ef467b48c60 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -49,6 +49,7 @@ import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { UserPermissions } from '@sofie-automation/meteor-lib/dist/userPermissions' import { assertConnectionHasOneOfPermissions } from '../security/auth' import { checkAccessToRundown } from '../security/check' +import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' const PERMISSIONS_FOR_PLAYOUT_USERACTION: Array = ['studio'] const PERMISSIONS_FOR_BUCKET_MODIFICATION: Array = ['studio'] @@ -121,8 +122,9 @@ class ServerUserActionAPI userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId, - nextPartId: PartId, - timeOffset: number | null + nextPartOrInstanceId: PartId | PartInstanceId, + timeOffset: number | null, + isInstance: boolean | null ) { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( this, @@ -131,12 +133,15 @@ class ServerUserActionAPI rundownPlaylistId, () => { check(rundownPlaylistId, String) - check(nextPartId, String) + check(nextPartOrInstanceId, String) }, StudioJobs.SetNextPart, { playlistId: rundownPlaylistId, - nextPartId, + nextPartId: isInstance ? undefined : protectString(unprotectString(nextPartOrInstanceId)), + nextPartInstanceId: isInstance + ? protectString(unprotectString(nextPartOrInstanceId)) + : undefined, setManually: true, nextTimeOffset: timeOffset ?? undefined, } @@ -399,9 +404,9 @@ class ServerUserActionAPI userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId, - actionDocId: AdLibActionId | RundownBaselineAdLibActionId | BucketAdLibActionId, + actionDocId: AdLibActionId | RundownBaselineAdLibActionId | BucketAdLibActionId | null, actionId: string, - userData: ActionUserData, + userData: ActionUserData | null, triggerMode: string | null ) { return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( @@ -411,7 +416,7 @@ class ServerUserActionAPI rundownPlaylistId, () => { check(rundownPlaylistId, String) - check(actionDocId, String) + check(actionDocId, Match.Maybe(String)) check(actionId, String) check(userData, Match.Any) check(triggerMode, Match.Maybe(String)) diff --git a/meteor/server/collections/collection.ts b/meteor/server/collections/collection.ts index 632eedbdd90..b677cf37ec0 100644 --- a/meteor/server/collections/collection.ts +++ b/meteor/server/collections/collection.ts @@ -110,8 +110,9 @@ function wrapMeteorCollectionIntoAsyncCollection }> - extends AsyncOnlyReadOnlyMongoCollection { +export interface AsyncOnlyMongoCollection< + DBInterface extends { _id: ProtectedString }, +> extends AsyncOnlyReadOnlyMongoCollection { /** * Insert a document * @param document The document to insert diff --git a/meteor/server/collections/implementations/asyncCollection.ts b/meteor/server/collections/implementations/asyncCollection.ts index f0ba14f90b0..1b92d0e9f92 100644 --- a/meteor/server/collections/implementations/asyncCollection.ts +++ b/meteor/server/collections/implementations/asyncCollection.ts @@ -36,9 +36,9 @@ export type MinimalMeteorMongoCollection } find: (...args: Parameters['find']>) => MinimalMongoCursor } -export class WrappedAsyncMongoCollection }> - implements AsyncOnlyMongoCollection -{ +export class WrappedAsyncMongoCollection< + DBInterface extends { _id: ProtectedString }, +> implements AsyncOnlyMongoCollection { protected readonly _collection: MinimalMeteorMongoCollection public readonly name: string | null diff --git a/meteor/server/collections/implementations/readonlyWrapper.ts b/meteor/server/collections/implementations/readonlyWrapper.ts index a4147afbd57..d2829f07a62 100644 --- a/meteor/server/collections/implementations/readonlyWrapper.ts +++ b/meteor/server/collections/implementations/readonlyWrapper.ts @@ -4,9 +4,9 @@ import type { Collection } from 'mongodb' import type { AsyncOnlyMongoCollection, AsyncOnlyReadOnlyMongoCollection } from '../collection' import type { MinimalMongoCursor } from './asyncCollection' -export class WrappedReadOnlyMongoCollection }> - implements AsyncOnlyReadOnlyMongoCollection -{ +export class WrappedReadOnlyMongoCollection< + DBInterface extends { _id: ProtectedString }, +> implements AsyncOnlyReadOnlyMongoCollection { readonly #mutableCollection: AsyncOnlyMongoCollection constructor(collection: AsyncOnlyMongoCollection) { diff --git a/meteor/server/coreSystem/index.ts b/meteor/server/coreSystem/index.ts index 5ad804e3fc6..f5dadbb8d24 100644 --- a/meteor/server/coreSystem/index.ts +++ b/meteor/server/coreSystem/index.ts @@ -84,9 +84,9 @@ async function initializeCoreSystem() { if (!isRunningInJest()) { // Check what migration has to provide: const migration = await prepareMigration(true) - if (migration.migrationNeeded && migration.manualStepCount === 0 && migration.chunks.length <= 1) { + if (migration.migrationNeeded && migration.chunks.length <= 1) { // Since we've determined that the migration can be done automatically, and we have a fresh system, just do the migration automatically: - await runMigration(migration.chunks, migration.hash, []) + await runMigration(migration.chunks, migration.hash) } } } diff --git a/meteor/server/lib/rest/v1/ingest.ts b/meteor/server/lib/rest/v1/ingest.ts new file mode 100644 index 00000000000..2469541a948 --- /dev/null +++ b/meteor/server/lib/rest/v1/ingest.ts @@ -0,0 +1,273 @@ +import { IngestPart, IngestSegment } from '@sofie-automation/blueprints-integration' +import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { Meteor } from 'meteor/meteor' +import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' +import { IngestRundown } from '@sofie-automation/blueprints-integration' + +/* ************************************************************************* +This file contains types and interfaces that are used by the REST API. +When making changes to these types, you should be aware of any breaking changes +and update packages/openapi accordingly if needed. +************************************************************************* */ + +export interface IngestRestAPI { + // Playlists + + getPlaylists( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId + ): Promise>> + + getPlaylist( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string // Internal or external ID + ): Promise> + + deletePlaylists( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId + ): Promise> + + deletePlaylist( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string // Internal or external ID + ): Promise> + + // Rundowns + + getRundowns( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string + ): Promise>> + + getRundown( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string + ): Promise> + + postRundown( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + ingestRundown: RestApiIngestRundown + ): Promise> + + putRundowns( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + ingestRundowns: RestApiIngestRundown[] + ): Promise> + + putRundown( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + ingestRundown: RestApiIngestRundown + ): Promise> + + deleteRundowns( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string + ): Promise> + + deleteRundown( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string + ): Promise> + + // Segments + + getSegments( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string + ): Promise>> + + getSegment( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string + ): Promise> + + postSegment( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + ingestSegment: IngestSegment + ): Promise> + + putSegments( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + ingestSegments: IngestSegment[] + ): Promise> + + putSegment( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + ingestSegment: IngestSegment + ): Promise> + + deleteSegments( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string + ): Promise> + + deleteSegment( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string + ): Promise> + + // Parts + + getParts( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string + ): Promise>> + + getPart( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + partId: string + ): Promise> + + postPart( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + ingestPart: IngestPart + ): Promise> + + putParts( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + ingestParts: IngestPart[] + ): Promise> + + putPart( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + partId: string, + ingestPart: IngestPart + ): Promise> + + deleteParts( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string + ): Promise> + + deletePart( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + segmentId: string, + partId: string + ): Promise> +} + +export type RestApiIngestRundown = Omit & { + resyncUrl: string +} + +export type PlaylistResponse = { + id: string + externalId: string + rundownIds: string[] + studioId: string +} + +export type RundownResponse = { + id: string + externalId: string + studioId: string + playlistId: string + playlistExternalId?: string + name: string +} + +export type SegmentResponse = { + id: string + externalId: string + rundownId: string + name: string + rank: number + isHidden?: boolean +} + +export type PartResponse = { + id: string + externalId: string + rundownId: string + segmentId: string + name: string + expectedDuration?: number + autoNext?: boolean + rank: number +} diff --git a/meteor/server/lib/rest/v1/playlists.ts b/meteor/server/lib/rest/v1/playlists.ts index 74a60a29762..778ff2b04d2 100644 --- a/meteor/server/lib/rest/v1/playlists.ts +++ b/meteor/server/lib/rest/v1/playlists.ts @@ -7,10 +7,11 @@ import { PartInstanceId, PieceId, RundownBaselineAdLibActionId, + RundownId, RundownPlaylistId, SegmentId, } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { QueueNextSegmentResult } from '@sofie-automation/corelib/dist/worker/studio' +import { QueueNextSegmentResult, TakeNextPartResult } from '@sofie-automation/corelib/dist/worker/studio' import { Meteor } from 'meteor/meteor' /* ************************************************************************* @@ -44,6 +45,15 @@ export interface PlaylistsRestAPI { rundownPlaylistId: RundownPlaylistId, rehearsal: boolean ): Promise> + /** + * Activates AdLibs testing mode. + */ + activateAdLibTesting( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + rundownId: RundownId + ): Promise> /** * Deactivates a Playlist. * @@ -228,7 +238,7 @@ export interface PlaylistsRestAPI { event: string, rundownPlaylistId: RundownPlaylistId, fromPartInstanceId: PartInstanceId | undefined - ): Promise> + ): Promise> /** * Clears the specified SourceLayers. * diff --git a/meteor/server/lib/rest/v1/system.ts b/meteor/server/lib/rest/v1/system.ts index 9b86fda7c2d..6ad4aa4184f 100644 --- a/meteor/server/lib/rest/v1/system.ts +++ b/meteor/server/lib/rest/v1/system.ts @@ -48,11 +48,7 @@ export interface SystemRestAPI { * @param event User event string * @param inputs Migration data to apply */ - applyPendingMigrations( - connection: Meteor.Connection, - event: string, - inputs: MigrationData - ): Promise> + applyPendingMigrations(connection: Meteor.Connection, event: string): Promise> } export interface PendingMigrationStep { diff --git a/meteor/server/migration/0_1_0.ts b/meteor/server/migration/0_1_0.ts index 8b5e4e58542..70449b205aa 100644 --- a/meteor/server/migration/0_1_0.ts +++ b/meteor/server/migration/0_1_0.ts @@ -44,8 +44,10 @@ export const addSteps = addMigrationSteps('0.1.0', [ routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - thumbnailContainerIds: [], - previewContainerIds: [], + packageContainerSettingsWithOverrides: wrapDefaultObject({ + thumbnailContainerIds: [], + previewContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), diff --git a/meteor/server/migration/26_03.ts b/meteor/server/migration/26_03.ts new file mode 100644 index 00000000000..2f1740e12ee --- /dev/null +++ b/meteor/server/migration/26_03.ts @@ -0,0 +1,191 @@ +import { addMigrationSteps } from './databaseMigration' +import { MongoInternals } from 'meteor/mongo' +import { Studios } from '../collections' +import { ExpectedPackages } from '../collections' +import * as PackagesPreR53 from '@sofie-automation/corelib/dist/dataModel/Old/ExpectedPackagesR52' +import { + ExpectedPackageDB, + ExpectedPackageIngestSource, +} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { BucketId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { assertNever, Complete } from '@sofie-automation/corelib/dist/lib' + +// Release 52 + +export const addSteps = addMigrationSteps('26.3.0', [ + { + id: `Drop media manager collections`, + canBeRunAutomatically: true, + validate: async () => { + // If MongoInternals is not available, we are in a test environment + if (!MongoInternals) return false + + const existingCollections = await MongoInternals.defaultRemoteCollectionDriver() + .mongo.db.listCollections() + .toArray() + const collectionsToDrop = existingCollections.filter((c) => + ['expectedMediaItems', 'mediaWorkFlows', 'mediaWorkFlowSteps'].includes(c.name) + ) + if (collectionsToDrop.length > 0) { + return `There are ${collectionsToDrop.length} obsolete collections to be removed: ${collectionsToDrop + .map((c) => c.name) + .join(', ')}` + } + + return false + }, + migrate: async () => { + const existingCollections = await MongoInternals.defaultRemoteCollectionDriver() + .mongo.db.listCollections() + .toArray() + const collectionsToDrop = existingCollections.filter((c) => + ['expectedMediaItems', 'mediaWorkFlows', 'mediaWorkFlowSteps'].includes(c.name) + ) + for (const c of collectionsToDrop) { + await MongoInternals.defaultRemoteCollectionDriver().mongo.db.dropCollection(c.name) + } + }, + }, + + { + id: 'Ensure a single studio', + canBeRunAutomatically: true, + validate: async () => { + const studioCount = await Studios.countDocuments() + if (studioCount === 0) return `No studios found` + if (studioCount > 1) return `There are ${studioCount} studios, but only one is supported` + return false + }, + migrate: async () => { + // Do nothing, the user will have to resolve this manually + }, + }, + { + id: `convert ExpectedPackages to new format`, + canBeRunAutomatically: true, + validate: async () => { + const packages = await ExpectedPackages.findFetchAsync({ + fromPieceType: { $exists: true }, + }) + + if (packages.length > 0) { + return 'ExpectedPackages must be converted to new format' + } + + return false + }, + migrate: async () => { + const packages = (await ExpectedPackages.findFetchAsync({ + fromPieceType: { $exists: true }, + })) as unknown as PackagesPreR53.ExpectedPackageDB[] + + for (const pkg of packages) { + let rundownId: RundownId | null = null + let bucketId: BucketId | null = null + let ingestSource: ExpectedPackageIngestSource | undefined + + switch (pkg.fromPieceType) { + case PackagesPreR53.ExpectedPackageDBType.PIECE: + case PackagesPreR53.ExpectedPackageDBType.ADLIB_PIECE: + rundownId = pkg.rundownId + ingestSource = { + fromPieceType: pkg.fromPieceType, + pieceId: pkg.pieceId, + partId: pkg.partId, + segmentId: pkg.segmentId, + blueprintPackageId: pkg.blueprintPackageId, + listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, + } + break + case PackagesPreR53.ExpectedPackageDBType.ADLIB_ACTION: + rundownId = pkg.rundownId + ingestSource = { + fromPieceType: pkg.fromPieceType, + pieceId: pkg.pieceId, + partId: pkg.partId, + segmentId: pkg.segmentId, + blueprintPackageId: pkg.blueprintPackageId, + listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, + } + break + case PackagesPreR53.ExpectedPackageDBType.BASELINE_ADLIB_PIECE: + rundownId = pkg.rundownId + ingestSource = { + fromPieceType: pkg.fromPieceType, + pieceId: pkg.pieceId, + blueprintPackageId: pkg.blueprintPackageId, + listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, + } + break + case PackagesPreR53.ExpectedPackageDBType.BASELINE_ADLIB_ACTION: + rundownId = pkg.rundownId + ingestSource = { + fromPieceType: pkg.fromPieceType, + pieceId: pkg.pieceId, + blueprintPackageId: pkg.blueprintPackageId, + listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, + } + break + case PackagesPreR53.ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS: + rundownId = pkg.rundownId + ingestSource = { + fromPieceType: pkg.fromPieceType, + blueprintPackageId: pkg.blueprintPackageId, + listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, + } + break + case PackagesPreR53.ExpectedPackageDBType.BUCKET_ADLIB: + bucketId = pkg.bucketId + ingestSource = { + fromPieceType: pkg.fromPieceType, + pieceId: pkg.pieceId, + pieceExternalId: pkg.pieceExternalId, + blueprintPackageId: pkg.blueprintPackageId, + listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, + } + break + case PackagesPreR53.ExpectedPackageDBType.BUCKET_ADLIB_ACTION: + bucketId = pkg.bucketId + ingestSource = { + fromPieceType: pkg.fromPieceType, + pieceId: pkg.pieceId, + pieceExternalId: pkg.pieceExternalId, + blueprintPackageId: pkg.blueprintPackageId, + listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, + } + break + case PackagesPreR53.ExpectedPackageDBType.STUDIO_BASELINE_OBJECTS: + ingestSource = { + fromPieceType: pkg.fromPieceType, + blueprintPackageId: pkg.blueprintPackageId, + listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, + } + break + default: + assertNever(pkg) + break + } + + await ExpectedPackages.mutableCollection.removeAsync(pkg._id) + + if (ingestSource) { + await ExpectedPackages.mutableCollection.insertAsync({ + _id: pkg._id, // Preserve the old id to ensure references aren't broken. This will be 'corrected' upon first ingest operation + studioId: pkg.studioId, + rundownId: rundownId, + bucketId: bucketId, + package: { + ...(pkg as any), // Some fields should be pruned off this, but this is fine + _id: pkg.blueprintPackageId, + }, + created: pkg.created, + ingestSources: [ingestSource], + playoutSources: { + pieceInstanceIds: [], + }, + } satisfies Complete) + } + } + }, + }, +]) diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 30a74d769e1..31712163a52 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -1,15 +1,7 @@ import { addMigrationSteps } from './databaseMigration' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' -import { MongoInternals } from 'meteor/mongo' -import { Studios } from '../collections' -import { ExpectedPackages } from '../collections' -import * as PackagesPreR53 from '@sofie-automation/corelib/dist/dataModel/Old/ExpectedPackagesR52' -import { - ExpectedPackageDB, - ExpectedPackageIngestSource, -} from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' -import { BucketId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { assertNever, Complete } from '@sofie-automation/corelib/dist/lib' +import { RundownPlaylists } from '../collections' +import { ContainerIdsToObjectWithOverridesMigrationStep } from './steps/X_X_X/ContainerIdsToObjectWithOverridesMigrationStep' /* * ************************************************************************************** @@ -23,176 +15,73 @@ import { assertNever, Complete } from '@sofie-automation/corelib/dist/lib' export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ { - id: `Drop media manager collections`, + id: `Rename previousPersistentState to privatePlayoutPersistentState`, canBeRunAutomatically: true, validate: async () => { - // If MongoInternals is not available, we are in a test environment - if (!MongoInternals) return false - - const existingCollections = await MongoInternals.defaultRemoteCollectionDriver() - .mongo.db.listCollections() - .toArray() - const collectionsToDrop = existingCollections.filter((c) => - ['expectedMediaItems', 'mediaWorkFlows', 'mediaWorkFlowSteps'].includes(c.name) - ) - if (collectionsToDrop.length > 0) { - return `There are ${collectionsToDrop.length} obsolete collections to be removed: ${collectionsToDrop.map((c) => c.name).join(', ')}` + const playlists = await RundownPlaylists.countDocuments({ + previousPersistentState: { $exists: true }, + privatePlayoutPersistentState: { $exists: false }, + }) + if (playlists > 0) { + return 'One or more Playlists has previousPersistentState field that needs to be renamed to privatePlayoutPersistentState' } return false }, migrate: async () => { - const existingCollections = await MongoInternals.defaultRemoteCollectionDriver() - .mongo.db.listCollections() - .toArray() - const collectionsToDrop = existingCollections.filter((c) => - ['expectedMediaItems', 'mediaWorkFlows', 'mediaWorkFlowSteps'].includes(c.name) + const playlists = await RundownPlaylists.findFetchAsync( + { + previousPersistentState: { $exists: true }, + privatePlayoutPersistentState: { $exists: false }, + }, + { + projection: { + _id: 1, + // @ts-expect-error - This field is being renamed, so it won't exist on the type anymore + previousPersistentState: 1, + }, + } ) - for (const c of collectionsToDrop) { - await MongoInternals.defaultRemoteCollectionDriver().mongo.db.dropCollection(c.name) - } - }, - }, - { - id: 'Ensure a single studio', - canBeRunAutomatically: true, - validate: async () => { - const studioCount = await Studios.countDocuments() - if (studioCount === 0) return `No studios found` - if (studioCount > 1) return `There are ${studioCount} studios, but only one is supported` - return false - }, - migrate: async () => { - // Do nothing, the user will have to resolve this manually + for (const playlist of playlists) { + // @ts-expect-error - This field is being renamed, so it won't exist on the type anymore + const previousPersistentState = playlist.previousPersistentState + + await RundownPlaylists.mutableCollection.updateAsync(playlist._id, { + $set: { + privatePlayoutPersistentState: previousPersistentState, + }, + $unset: { + previousPersistentState: 1, + }, + }) + } }, }, + new ContainerIdsToObjectWithOverridesMigrationStep(), { - id: `convert ExpectedPackages to new format`, + id: 'Add T-timers to RundownPlaylist', canBeRunAutomatically: true, validate: async () => { - const packages = await ExpectedPackages.findFetchAsync({ - fromPieceType: { $exists: true }, - }) - - if (packages.length > 0) { - return 'ExpectedPackages must be converted to new format' - } - + const playlistCount = await RundownPlaylists.countDocuments({ tTimers: { $exists: false } }) + if (playlistCount > 1) return `There are ${playlistCount} RundownPlaylists without T-timers` return false }, migrate: async () => { - const packages = (await ExpectedPackages.findFetchAsync({ - fromPieceType: { $exists: true }, - })) as unknown as PackagesPreR53.ExpectedPackageDB[] - - for (const pkg of packages) { - let rundownId: RundownId | null = null - let bucketId: BucketId | null = null - let ingestSource: ExpectedPackageIngestSource | undefined - - switch (pkg.fromPieceType) { - case PackagesPreR53.ExpectedPackageDBType.PIECE: - case PackagesPreR53.ExpectedPackageDBType.ADLIB_PIECE: - rundownId = pkg.rundownId - ingestSource = { - fromPieceType: pkg.fromPieceType, - pieceId: pkg.pieceId, - partId: pkg.partId, - segmentId: pkg.segmentId, - blueprintPackageId: pkg.blueprintPackageId, - listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, - } - break - case PackagesPreR53.ExpectedPackageDBType.ADLIB_ACTION: - rundownId = pkg.rundownId - ingestSource = { - fromPieceType: pkg.fromPieceType, - pieceId: pkg.pieceId, - partId: pkg.partId, - segmentId: pkg.segmentId, - blueprintPackageId: pkg.blueprintPackageId, - listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, - } - break - case PackagesPreR53.ExpectedPackageDBType.BASELINE_ADLIB_PIECE: - rundownId = pkg.rundownId - ingestSource = { - fromPieceType: pkg.fromPieceType, - pieceId: pkg.pieceId, - blueprintPackageId: pkg.blueprintPackageId, - listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, - } - break - case PackagesPreR53.ExpectedPackageDBType.BASELINE_ADLIB_ACTION: - rundownId = pkg.rundownId - ingestSource = { - fromPieceType: pkg.fromPieceType, - pieceId: pkg.pieceId, - blueprintPackageId: pkg.blueprintPackageId, - listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, - } - break - case PackagesPreR53.ExpectedPackageDBType.RUNDOWN_BASELINE_OBJECTS: - rundownId = pkg.rundownId - ingestSource = { - fromPieceType: pkg.fromPieceType, - blueprintPackageId: pkg.blueprintPackageId, - listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, - } - break - case PackagesPreR53.ExpectedPackageDBType.BUCKET_ADLIB: - bucketId = pkg.bucketId - ingestSource = { - fromPieceType: pkg.fromPieceType, - pieceId: pkg.pieceId, - pieceExternalId: pkg.pieceExternalId, - blueprintPackageId: pkg.blueprintPackageId, - listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, - } - break - case PackagesPreR53.ExpectedPackageDBType.BUCKET_ADLIB_ACTION: - bucketId = pkg.bucketId - ingestSource = { - fromPieceType: pkg.fromPieceType, - pieceId: pkg.pieceId, - pieceExternalId: pkg.pieceExternalId, - blueprintPackageId: pkg.blueprintPackageId, - listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, - } - break - case PackagesPreR53.ExpectedPackageDBType.STUDIO_BASELINE_OBJECTS: - ingestSource = { - fromPieceType: pkg.fromPieceType, - blueprintPackageId: pkg.blueprintPackageId, - listenToPackageInfoUpdates: pkg.listenToPackageInfoUpdates, - } - break - default: - assertNever(pkg) - break - } - - await ExpectedPackages.mutableCollection.removeAsync(pkg._id) - - if (ingestSource) { - await ExpectedPackages.mutableCollection.insertAsync({ - _id: pkg._id, // Preserve the old id to ensure references aren't broken. This will be 'corrected' upon first ingest operation - studioId: pkg.studioId, - rundownId: rundownId, - bucketId: bucketId, - package: { - ...(pkg as any), // Some fields should be pruned off this, but this is fine - _id: pkg.blueprintPackageId, - }, - created: pkg.created, - ingestSources: [ingestSource], - playoutSources: { - pieceInstanceIds: [], - }, - } satisfies Complete) - } - } + await RundownPlaylists.mutableCollection.updateAsync( + { tTimers: { $exists: false } }, + { + $set: { + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], + }, + }, + { multi: true } + ) }, }, + // Add your migration here ]) diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index 5252a280818..2d4319f44b6 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -1,18 +1,16 @@ import _ from 'underscore' -import { setupEmptyEnvironment } from '../../../__mocks__/helpers/database' +import { setupEmptyEnvironment, setupMockStudio } from '../../../__mocks__/helpers/database' import { ICoreSystem, GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { clearMigrationSteps, addMigrationSteps, prepareMigration, PreparedMigration } from '../databaseMigration' import { CURRENT_SYSTEM_VERSION } from '../currentSystemVersion' import { RunMigrationResult, GetMigrationStatusResult } from '@sofie-automation/meteor-lib/dist/api/migration' -import { literal } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' -import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration' +import { MigrationStepCore } from '@sofie-automation/meteor-lib/dist/migrations' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { MeteorCall } from '../../api/methods' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { ShowStyleBases, ShowStyleVariants, Studios } from '../../collections' import { getCoreSystemAsync } from '../../coreSystem/collection' -import { DEFAULT_MINIMUM_TAKE_SPAN } from '@sofie-automation/shared-lib/dist/core/constants' import fs from 'fs' require('../../api/peripheralDevice.ts') // include in order to create the Meteor methods needed @@ -37,22 +35,7 @@ describe('Migrations', () => { async function getSystem() { return (await getCoreSystemAsync()) as ICoreSystem } - function userInput( - migrationStatus: GetMigrationStatusResult, - userValues?: { [key: string]: any } - ): MigrationStepInputResult[] { - return _.compact( - _.map(migrationStatus.migration.manualInputs, (manualInput) => { - if (manualInput.stepId && manualInput.attribute) { - return literal({ - stepId: manualInput.stepId, - attribute: manualInput.attribute, - value: userValues && userValues[manualInput.stepId], - }) - } - }) - ) - } + test('System migrations, initial setup', async () => { expect((await getSystem()).version).toEqual(GENESIS_SYSTEM_VERSION) @@ -64,11 +47,8 @@ describe('Migrations', () => { migrationNeeded: true, migration: { - canDoAutomaticMigration: true, - // manualInputs: [], hash: expect.stringContaining(''), automaticStepCount: expect.any(Number), - manualStepCount: expect.any(Number), ignoredStepCount: expect.any(Number), partialMigration: true, // chunks: expect.any(Array) @@ -77,8 +57,7 @@ describe('Migrations', () => { const migrationResult0: RunMigrationResult = await MeteorCall.migration.runMigration( migrationStatus0.migration.chunks, - migrationStatus0.migration.hash, - userInput(migrationStatus0) + migrationStatus0.migration.hash ) expect(migrationResult0).toMatchObject({ @@ -107,35 +86,8 @@ describe('Migrations', () => { return false }, migrate: async () => { - await Studios.insertAsync({ + await setupMockStudio({ _id: protectString('studioMock2'), - name: 'Default studio', - supportedShowStyleBase: [], - settingsWithOverrides: wrapDefaultObject({ - mediaPreviewsUrl: '', - frameRate: 25, - minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, - allowHold: true, - allowPieceDirectPlay: true, - enableBuckets: true, - enableEvaluationForm: true, - }), - mappingsWithOverrides: wrapDefaultObject({}), - blueprintConfigWithOverrides: wrapDefaultObject({}), - _rundownVersionHash: '', - routeSetsWithOverrides: wrapDefaultObject({}), - routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], - peripheralDeviceSettings: { - deviceSettings: wrapDefaultObject({}), - playoutDevices: wrapDefaultObject({}), - ingestDevices: wrapDefaultObject({}), - inputDevices: wrapDefaultObject({}), - }, - lastBlueprintConfig: undefined, - lastBlueprintFixUpHash: undefined, }) }, }, @@ -149,35 +101,8 @@ describe('Migrations', () => { return false }, migrate: async () => { - await Studios.insertAsync({ + await setupMockStudio({ _id: protectString('studioMock3'), - name: 'Default studio', - supportedShowStyleBase: [], - settingsWithOverrides: wrapDefaultObject({ - mediaPreviewsUrl: '', - frameRate: 25, - minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, - allowHold: true, - allowPieceDirectPlay: true, - enableBuckets: true, - enableEvaluationForm: true, - }), - mappingsWithOverrides: wrapDefaultObject({}), - blueprintConfigWithOverrides: wrapDefaultObject({}), - _rundownVersionHash: '', - routeSetsWithOverrides: wrapDefaultObject({}), - routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], - peripheralDeviceSettings: { - deviceSettings: wrapDefaultObject({}), - playoutDevices: wrapDefaultObject({}), - ingestDevices: wrapDefaultObject({}), - inputDevices: wrapDefaultObject({}), - }, - lastBlueprintConfig: undefined, - lastBlueprintFixUpHash: undefined, }) }, }, @@ -191,35 +116,8 @@ describe('Migrations', () => { return false }, migrate: async () => { - await Studios.insertAsync({ + await setupMockStudio({ _id: protectString('studioMock1'), - name: 'Default studio', - supportedShowStyleBase: [], - settingsWithOverrides: wrapDefaultObject({ - mediaPreviewsUrl: '', - frameRate: 25, - minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN, - allowHold: true, - allowPieceDirectPlay: true, - enableBuckets: true, - enableEvaluationForm: true, - }), - mappingsWithOverrides: wrapDefaultObject({}), - blueprintConfigWithOverrides: wrapDefaultObject({}), - _rundownVersionHash: '', - routeSetsWithOverrides: wrapDefaultObject({}), - routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), - packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], - peripheralDeviceSettings: { - deviceSettings: wrapDefaultObject({}), - playoutDevices: wrapDefaultObject({}), - ingestDevices: wrapDefaultObject({}), - inputDevices: wrapDefaultObject({}), - }, - lastBlueprintConfig: undefined, - lastBlueprintFixUpHash: undefined, }) }, }, @@ -322,4 +220,47 @@ describe('Migrations', () => { expect(steps.indexOf(myShowStyleMockStep3)).toEqual(8) */ }) + + test('Class-based migration steps work with proper binding', async () => { + await MeteorCall.migration.resetDatabaseVersions() + clearMigrationSteps() + + // Create a migration step class that uses instance properties + class TestClassMigrationStep implements Omit { + public readonly id = 'classBasedMigrationTest' + public readonly canBeRunAutomatically = true + public testValue = 'initialized' + + public async validate(): Promise { + // If 'this' is not bound, testValue will be undefined + return this.testValue === 'initialized' ? 'Migration needed' : false + } + + public async migrate(): Promise { + // If 'this' is not bound, this will throw or fail to update the correct instance + this.testValue = 'migrated' + } + } + + // Instantiate the step so we can check it later + const step = new TestClassMigrationStep() + addMigrationSteps('1.0.0', [step])() + + // Prepare migration to ensure it's detected + const migration = await prepareMigration(true) + expect(migration.migrationNeeded).toEqual(true) + expect(_.find(migration.steps, (s) => s.id === 'classBasedMigrationTest')).toBeTruthy() + + // Run the migration to verify that methods are properly bound + const migrationStatus: GetMigrationStatusResult = await MeteorCall.migration.getMigrationStatus() + const migrationResult: RunMigrationResult = await MeteorCall.migration.runMigration( + migrationStatus.migration.chunks, + migrationStatus.migration.hash + ) + + expect(migrationResult.migrationCompleted).toEqual(true) + + // Verify that migrate() was called and 'this' was correctly bound + expect(step.testValue).toEqual('migrated') + }) }) diff --git a/meteor/server/migration/api.ts b/meteor/server/migration/api.ts index 05574802788..c39fc44e85d 100644 --- a/meteor/server/migration/api.ts +++ b/meteor/server/migration/api.ts @@ -7,7 +7,6 @@ import { BlueprintFixUpConfigMessage, } from '@sofie-automation/meteor-lib/dist/api/migration' import * as Migrations from './databaseMigration' -import { MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { MethodContextAPI } from '../api/methodContext' import { fixupConfigForShowStyleBase, @@ -34,20 +33,14 @@ class ServerMigrationAPI extends MethodContextAPI implements NewMigrationAPI { return Migrations.getMigrationStatus() } - async runMigration( - chunks: Array, - hash: string, - inputResults: Array, - isFirstOfPartialMigrations?: boolean | null - ) { + async runMigration(chunks: Array, hash: string, isFirstOfPartialMigrations?: boolean | null) { check(chunks, Array) check(hash, String) - check(inputResults, Array) check(isFirstOfPartialMigrations, Match.Maybe(Boolean)) assertConnectionHasOneOfPermissions(this.connection, ...PERMISSIONS_FOR_MIGRATIONS) - return Migrations.runMigration(chunks, hash, inputResults, isFirstOfPartialMigrations || false) + return Migrations.runMigration(chunks, hash, isFirstOfPartialMigrations || false) } async forceMigration(chunks: Array) { diff --git a/meteor/server/migration/currentSystemVersion.ts b/meteor/server/migration/currentSystemVersion.ts index bb7d1be26c3..f07ea1346f3 100644 --- a/meteor/server/migration/currentSystemVersion.ts +++ b/meteor/server/migration/currentSystemVersion.ts @@ -55,4 +55,4 @@ */ // Note: Only set this to release versions, (ie X.Y.Z), not pre-releases (ie X.Y.Z-0-pre-release) -export const CURRENT_SYSTEM_VERSION = '1.53.0' +export const CURRENT_SYSTEM_VERSION = '26.6.0' diff --git a/meteor/server/migration/databaseMigration.ts b/meteor/server/migration/databaseMigration.ts index eed1dc39482..f33642a8bc4 100644 --- a/meteor/server/migration/databaseMigration.ts +++ b/meteor/server/migration/databaseMigration.ts @@ -1,24 +1,13 @@ import { Meteor } from 'meteor/meteor' import * as semver from 'semver' import { - BlueprintManifestType, - InputFunctionCore, - InputFunctionSystem, MigrateFunctionCore, - MigrationContextSystem as IMigrationContextSystem, MigrationStep, - MigrationStepInput, - MigrationStepInputFilteredResult, - MigrationStepInputResult, - SystemBlueprintManifest, ValidateFunctionCore, - ValidateFunctionSystem, - MigrateFunctionSystem, ValidateFunction, MigrateFunction, - InputFunction, MigrationStepCore, -} from '@sofie-automation/blueprints-integration' +} from '@sofie-automation/meteor-lib/dist/migrations' import _ from 'underscore' import { GetMigrationStatusResult, @@ -30,14 +19,12 @@ import { logger } from '../logging' import { internalStoreSystemSnapshot } from '../api/snapshot' import { parseVersion, Version } from '../systemStatus/semverUtils' import { GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' -import { clone, getHash, omit } from '@sofie-automation/corelib/dist/lib' +import { getHash, omit } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' -import { evalBlueprint } from '../api/blueprints/cache' -import { MigrationContextSystem } from '../api/blueprints/migrationContext' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' import { SnapshotId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { Blueprints, CoreSystem } from '../collections' +import { Blueprints } from '../collections' import { getSystemStorePath } from '../coreSystem' import { getCoreSystemAsync, setCoreSystemVersion } from '../coreSystem/collection' @@ -65,7 +52,7 @@ export function isVersionSupported(version: Version): boolean { return isSupported } -interface MigrationStepInternal extends MigrationStep { +interface MigrationStepInternal extends MigrationStep { chunk: MigrationChunk _rank: number _version: Version // step version @@ -82,10 +69,8 @@ const coreMigrationSteps: Array = [] export function addMigrationSteps(version: string, steps: Array>) { return (): void => { for (const step of steps) { - coreMigrationSteps.push({ - ...step, - version: version, - }) + ;(step as MigrationStepCore).version = version + coreMigrationSteps.push(step as MigrationStepCore) } } } @@ -100,9 +85,7 @@ export interface PreparedMigration { steps: MigrationStepInternal[] migrationNeeded: boolean automaticStepCount: number - manualStepCount: number ignoredStepCount: number - manualInputs: MigrationStepInput[] partialMigration: boolean } export async function prepareMigration(returnAllChunks?: boolean): Promise { @@ -135,10 +118,9 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise { - const chunk: MigrationChunk = { - sourceType: MigrationStepType.SYSTEM, - sourceName: 'Blueprint ' + blueprint.name + ' for system', - sourceId: 'system', - blueprintId: blueprint._id, - _dbVersion: parseVersion(blueprint.databaseVersion.system || '0.0.0'), - _targetVersion: parseVersion(bp.blueprintVersion), - _steps: [], - } - migrationChunks.push(chunk) - // Add core migration steps from blueprint: - for (const step of bp.coreMigrations) { - allMigrationSteps.push( - prefixIdsOnStep('blueprint_' + blueprint._id + '_system_', { - id: step.id, - overrideSteps: step.overrideSteps, - validate: step.validate, - canBeRunAutomatically: step.canBeRunAutomatically, - migrate: step.migrate, - input: step.input, - dependOnResultFrom: step.dependOnResultFrom, - version: step.version, - _version: parseVersion(step.version), - _validateResult: false, // to be set later - _rank: rank++, - chunk: chunk, - }) - ) - } - }) - } else { - // unknown blueprint type - } - } else { - console.log(`blueprint ${blueprint._id} has no code`) - } - } - // Sort, smallest version first: allMigrationSteps.sort((a, b) => { // First, sort by type: if (a.chunk.sourceType === MigrationStepType.CORE && b.chunk.sourceType !== MigrationStepType.CORE) return -1 if (a.chunk.sourceType !== MigrationStepType.CORE && b.chunk.sourceType === MigrationStepType.CORE) return 1 - if (a.chunk.sourceType === MigrationStepType.SYSTEM && b.chunk.sourceType !== MigrationStepType.SYSTEM) - return -1 - if (a.chunk.sourceType !== MigrationStepType.SYSTEM && b.chunk.sourceType === MigrationStepType.SYSTEM) return 1 - // Then, sort by version: if (semver.gt(a._version, b._version)) return 1 if (semver.lt(a._version, b._version)) return -1 @@ -231,7 +147,6 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise = [] const stepsHash: Array = [] for (const step of Object.values(migrationSteps)) { stepsHash.push(step.id) step.chunk._steps.push(step.id) - if (!step.canBeRunAutomatically) { - manualStepCount++ - - if (step.input) { - let input: Array = [] - if (Array.isArray(step.input)) { - input = clone(step.input) - } else if (typeof step.input === 'function') { - if (step.chunk.sourceType === MigrationStepType.CORE) { - const inputFunction = step.input as InputFunctionCore - input = inputFunction() - } else if (step.chunk.sourceType === MigrationStepType.SYSTEM) { - const inputFunction = step.input as InputFunctionSystem - input = inputFunction(getMigrationSystemContext(step.chunk)) - } else throw new Meteor.Error(500, `Unknown step.chunk.sourceType "${step.chunk.sourceType}"`) - } - if (input.length) { - for (const i of input) { - if (i.label && typeof step._validateResult === 'string') { - i.label = (i.label + '').replace(/\$validation/g, step._validateResult) - } - if (i.description && typeof step._validateResult === 'string') { - i.description = (i.description + '').replace(/\$validation/g, step._validateResult) - } - manualInputs.push({ - ...i, - stepId: step.id, - }) - } - } - } - } else { - automaticStepCount++ - } + + automaticStepCount++ } // Only return the chunks which has steps in them: @@ -368,29 +244,14 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise { - return prefix + override - }) - } - if (step.dependOnResultFrom) { - step.dependOnResultFrom = prefix + step.dependOnResultFrom - } - return step -} export async function runMigration( chunks: Array, hash: string, - inputResults: Array, isFirstOfPartialMigrations = true, chunksLeft = 20 ): Promise { @@ -404,19 +265,9 @@ export async function runMigration( // Verify the input: const migration = await prepareMigration(true) - const manualInputsWithUserPrompt = migration.manualInputs.filter((manualInput) => { - return !!(manualInput.stepId && manualInput.attribute) - }) if (migration.hash !== hash) throw new Meteor.Error(500, `Migration input hash differ from expected: "${hash}", "${migration.hash}"`) - if (manualInputsWithUserPrompt.length !== inputResults.length) { - throw new Meteor.Error( - 500, - `Migration manualInput lengths differ from expected: "${inputResults.length}", "${migration.manualInputs.length}"` - ) - } - // Check that chunks match: let unmatchedChunk = migration.chunks.find((migrationChunk) => { return !chunks.find((chunk) => { @@ -463,22 +314,10 @@ export async function runMigration( } } - logger.info( - `Migration: ${migration.automaticStepCount} automatic and ${migration.manualStepCount} manual steps (${migration.ignoredStepCount} ignored).` - ) - - logger.debug(inputResults) + logger.info(`Migration: ${migration.automaticStepCount} steps (${migration.ignoredStepCount} ignored).`) for (const step of migration.steps) { try { - // Prepare input from user - const stepInput: MigrationStepInputFilteredResult = {} - for (const ir of inputResults) { - if (ir.stepId === step.id) { - stepInput[ir.attribute] = ir.value - } - } - // Run the migration script if (step.migrate !== undefined) { @@ -486,10 +325,7 @@ export async function runMigration( if (step.chunk.sourceType === MigrationStepType.CORE) { const migration = step.migrate as MigrateFunctionCore - await migration(stepInput) - } else if (step.chunk.sourceType === MigrationStepType.SYSTEM) { - const migration = step.migrate as MigrateFunctionSystem - await migration(getMigrationSystemContext(step.chunk), stepInput) + await migration() } else throw new Meteor.Error(500, `Unknown step.chunk.sourceType "${step.chunk.sourceType}"`) } @@ -501,9 +337,6 @@ export async function runMigration( if (step.chunk.sourceType === MigrationStepType.CORE) { const validate = step.validate as ValidateFunctionCore validateMessage = await validate(true) - } else if (step.chunk.sourceType === MigrationStepType.SYSTEM) { - const validate = step.validate as ValidateFunctionSystem - validateMessage = await validate(getMigrationSystemContext(step.chunk), true) } else throw new Meteor.Error(500, `Unknown step.chunk.sourceType "${step.chunk.sourceType}"`) // let validate = step.validate as ValidateFunctionCore @@ -524,20 +357,14 @@ export async function runMigration( let migrationCompleted = false - if (migration.manualStepCount === 0 && !warningMessages.length) { + if (!warningMessages.length) { // continue automatically with the next batch logger.info('Migration: Automatically continuing with next batch..') migration.partialMigration = false const s = await getMigrationStatus() - if (s.migration.automaticStepCount > 0 || s.migration.manualStepCount > 0) { + if (s.migration.automaticStepCount > 0) { try { - const res = await runMigration( - s.migration.chunks, - s.migration.hash, - inputResults, - false, - chunksLeft - 1 - ) + const res = await runMigration(s.migration.chunks, s.migration.hash, false, chunksLeft - 1) if (res.migrationCompleted) { return res } @@ -571,22 +398,6 @@ async function completeMigration(chunks: Array) { for (const chunk of chunks) { if (chunk.sourceType === MigrationStepType.CORE) { await setCoreSystemVersion(chunk._targetVersion) - } else if (chunk.sourceType === MigrationStepType.SYSTEM) { - if (!chunk.blueprintId) throw new Meteor.Error(500, `chunk.blueprintId missing!`) - if (!chunk.sourceId) throw new Meteor.Error(500, `chunk.sourceId missing!`) - - const blueprint = await Blueprints.findOneAsync(chunk.blueprintId) - if (!blueprint) throw new Meteor.Error(404, `Blueprint "${chunk.blueprintId}" not found!`) - - const m: any = {} - if (chunk.sourceType === MigrationStepType.SYSTEM) { - logger.info( - `Updating Blueprint "${chunk.sourceName}" version, from "${blueprint.databaseVersion.system}" to "${chunk._targetVersion}".` - ) - m[`databaseVersion.system`] = chunk._targetVersion - } else throw new Meteor.Error(500, `Bad chunk.sourcetype: "${chunk.sourceType}"`) - - await Blueprints.updateAsync(chunk.blueprintId, { $set: m }) } else throw new Meteor.Error(500, `Unknown chunk.sourcetype: "${chunk.sourceType}"`) } } @@ -605,14 +416,10 @@ export async function getMigrationStatus(): Promise { migrationNeeded: migration.migrationNeeded, migration: { - canDoAutomaticMigration: migration.manualStepCount === 0, - - manualInputs: migration.manualInputs, hash: migration.hash, chunks: migration.chunks, automaticStepCount: migration.automaticStepCount, - manualStepCount: migration.manualStepCount, ignoredStepCount: migration.ignoredStepCount, partialMigration: migration.partialMigration, }, @@ -646,11 +453,3 @@ export async function resetDatabaseVersions(): Promise { { multi: true } ) } - -function getMigrationSystemContext(chunk: MigrationChunk): IMigrationContextSystem { - if (chunk.sourceType !== MigrationStepType.SYSTEM) - throw new Meteor.Error(500, `wrong chunk.sourceType "${chunk.sourceType}", expected SYSTEM`) - if (!chunk.sourceId) throw new Meteor.Error(500, `chunk.sourceId missing`) - - return new MigrationContextSystem() -} diff --git a/meteor/server/migration/lib.ts b/meteor/server/migration/lib.ts index 07e1bd4ab51..c12a6b978d3 100644 --- a/meteor/server/migration/lib.ts +++ b/meteor/server/migration/lib.ts @@ -1,5 +1,5 @@ import _ from 'underscore' -import { MigrationStepCore } from '@sofie-automation/blueprints-integration' +import { MigrationStepCore } from '@sofie-automation/meteor-lib/dist/migrations' import { objectPathGet } from '@sofie-automation/corelib/dist/lib' import { ProtectedString } from '@sofie-automation/corelib/dist/protectedString' import { Meteor } from 'meteor/meteor' diff --git a/meteor/server/migration/migrations.ts b/meteor/server/migration/migrations.ts index b0b2bfd516e..d5359df8d89 100644 --- a/meteor/server/migration/migrations.ts +++ b/meteor/server/migration/migrations.ts @@ -40,6 +40,9 @@ addSteps1_51_0() import { addSteps as addSteps1_52_0 } from './1_52_0' addSteps1_52_0() +import { addSteps as addSteps26_03 } from './26_03' +addSteps26_03() + // Migrations for the in-development release: import { addSteps as addStepsX_X_X } from './X_X_X' addStepsX_X_X() diff --git a/meteor/server/migration/steps/X_X_X/ContainerIdsToObjectWithOverridesMigrationStep.ts b/meteor/server/migration/steps/X_X_X/ContainerIdsToObjectWithOverridesMigrationStep.ts new file mode 100644 index 00000000000..45ef67b97bd --- /dev/null +++ b/meteor/server/migration/steps/X_X_X/ContainerIdsToObjectWithOverridesMigrationStep.ts @@ -0,0 +1,54 @@ +import { MigrationStepCore } from '@sofie-automation/meteor-lib/dist/migrations' +import { Studios } from '../../../collections' +import { + convertObjectIntoOverrides, + ObjectWithOverrides, +} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { StudioPackageContainerSettings } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' + +export class ContainerIdsToObjectWithOverridesMigrationStep implements Omit { + public readonly id = `convert previewContainerIds to ObjectWithOverrides` + public readonly canBeRunAutomatically = true + + public async validate(): Promise { + const studios = await this.findStudiosToMigrate() + + if (studios.length) { + return 'previewContainerIds and thumbnailContainerIds must be converted to an ObjectWithOverrides' + } + + return false + } + + public async migrate(): Promise { + const studios = await this.findStudiosToMigrate() + + for (const studio of studios) { + // @ts-expect-error previewContainerIds is typed as string[] + const oldPreviewContainerIds = studio.previewContainerIds + // @ts-expect-error thumbnailContainerIds is typed as string[] + const oldThumbnailContainerIds = studio.thumbnailContainerIds + + const newPackageContainers = convertObjectIntoOverrides({ + previewContainerIds: oldPreviewContainerIds ?? [], + thumbnailContainerIds: oldThumbnailContainerIds ?? [], + } satisfies StudioPackageContainerSettings) as ObjectWithOverrides + + await Studios.updateAsync(studio._id, { + $set: { + packageContainerSettingsWithOverrides: newPackageContainers, + }, + $unset: { + previewContainerIds: 1, + thumbnailContainerIds: 1, + }, + }) + } + } + + private async findStudiosToMigrate() { + return Studios.findFetchAsync({ + packageContainerSettingsWithOverrides: { $exists: false }, + }) + } +} diff --git a/meteor/server/migration/steps/X_X_X/__tests__/ContainerIdsToObjectWithOverridesMigrationStep.test.ts b/meteor/server/migration/steps/X_X_X/__tests__/ContainerIdsToObjectWithOverridesMigrationStep.test.ts new file mode 100644 index 00000000000..3042b10c63e --- /dev/null +++ b/meteor/server/migration/steps/X_X_X/__tests__/ContainerIdsToObjectWithOverridesMigrationStep.test.ts @@ -0,0 +1,73 @@ +import { setupEmptyEnvironment, setupMockStudio } from '../../../../../__mocks__/helpers/database' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { Studios } from '../../../../collections' +import { ContainerIdsToObjectWithOverridesMigrationStep } from '../../X_X_X/ContainerIdsToObjectWithOverridesMigrationStep' + +describe('ContainerIdsToObjectWithOverridesMigrationStep', () => { + beforeEach(async () => { + await setupEmptyEnvironment() + }) + + test('migration is needed when studio is missing packageContainerSettingsWithOverrides', async () => { + await setupMockStudio({ + _id: protectString('studio0'), + // @ts-expect-error + previewContainerIds: ['preview1'], + thumbnailContainerIds: ['thumb1'], + packageContainerSettingsWithOverrides: undefined as any, + }) + + const step = new ContainerIdsToObjectWithOverridesMigrationStep() + const validateResult = await step.validate() + expect(validateResult).toBe( + 'previewContainerIds and thumbnailContainerIds must be converted to an ObjectWithOverrides' + ) + + await step.migrate() + + const studio = await Studios.findOneAsync(protectString('studio0')) + expect(studio).toBeTruthy() + expect(studio?.packageContainerSettingsWithOverrides).toMatchObject({ + defaults: {}, + overrides: [ + { op: 'set', path: 'previewContainerIds', value: ['preview1'] }, + { op: 'set', path: 'thumbnailContainerIds', value: ['thumb1'] }, + ], + }) + // @ts-expect-error + expect(studio?.previewContainerIds).toBeUndefined() + // @ts-expect-error + expect(studio?.thumbnailContainerIds).toBeUndefined() + + const validateResultAfter = await step.validate() + expect(validateResultAfter).toBe(false) + }) + + test('migration handles missing optional old fields', async () => { + await setupMockStudio({ + _id: protectString('studio1'), + packageContainerSettingsWithOverrides: undefined as any, + }) + + const step = new ContainerIdsToObjectWithOverridesMigrationStep() + const validateResult = await step.validate() + expect(validateResult).toBe( + 'previewContainerIds and thumbnailContainerIds must be converted to an ObjectWithOverrides' + ) + + await step.migrate() + + const studio = await Studios.findOneAsync(protectString('studio1')) + expect(studio).toBeTruthy() + expect(studio?.packageContainerSettingsWithOverrides).toMatchObject({ + defaults: {}, + overrides: [ + { op: 'set', path: 'previewContainerIds', value: [] }, + { op: 'set', path: 'thumbnailContainerIds', value: [] }, + ], + }) + + const validateResultAfter = await step.validate() + expect(validateResultAfter).toBe(false) + }) +}) diff --git a/meteor/server/migration/upgrades/lib.ts b/meteor/server/migration/upgrades/lib.ts index ce825f2fd77..8da4dacf89f 100644 --- a/meteor/server/migration/upgrades/lib.ts +++ b/meteor/server/migration/upgrades/lib.ts @@ -33,6 +33,7 @@ export async function updateTriggeredActionsForShowStyleBaseId( $set: { _rank: newTriggeredAction._rank, name: newTriggeredAction.name, + styleClassNames: newTriggeredAction.styleClassNames, 'triggersWithOverrides.defaults': newTriggeredAction.triggers, 'actionsWithOverrides.defaults': newTriggeredAction.actions, }, diff --git a/meteor/server/publications/packageManager/expectedPackages/generate.ts b/meteor/server/publications/packageManager/expectedPackages/generate.ts index 5c815af910f..203d4239ea9 100644 --- a/meteor/server/publications/packageManager/expectedPackages/generate.ts +++ b/meteor/server/publications/packageManager/expectedPackages/generate.ts @@ -3,6 +3,7 @@ import { Accessor, AccessorOnPackage, ExpectedPackage, + StudioPackageContainerSettings, } from '@sofie-automation/blueprints-integration' import { PeripheralDeviceId, ExpectedPackageId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' @@ -15,12 +16,11 @@ import deepExtend from 'deep-extend' import { ReadonlyDeep } from 'type-fest' import _ from 'underscore' import { getSideEffect } from '@sofie-automation/meteor-lib/dist/collections/ExpectedPackages' -import { DBStudio, StudioLight, StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' import { clone, omit } from '@sofie-automation/corelib/dist/lib' import { CustomPublishCollection } from '../../../lib/customPublication' import { logger } from '../../../logging' import { ExpectedPackageDBCompact, ExpectedPackagesContentCache } from './contentCache' -import type { StudioFields } from './publication' /** * Regenerate the output for the provided ExpectedPackage `regenerateIds`, updating the data in `collection` as needed @@ -33,7 +33,7 @@ import type { StudioFields } from './publication' */ export async function updateCollectionForExpectedPackageIds( contentCache: ReadonlyDeep, - studio: Pick, + packageContainerSettings: StudioPackageContainerSettings, layerNameToDeviceIds: Map, packageContainers: Record, collection: CustomPublishCollection, @@ -63,7 +63,12 @@ export async function updateCollectionForExpectedPackageIds( // Filter, keep only the routed mappings for this device: if (filterPlayoutDeviceIds && !filterPlayoutDeviceIds.includes(deviceId)) continue - const routedPackage = generateExpectedPackageForDevice(studio, packageDoc, deviceId, packageContainers) + const routedPackage = generateExpectedPackageForDevice( + packageContainerSettings, + packageDoc, + deviceId, + packageContainers + ) updatedDocIds.add(routedPackage._id) collection.replace(routedPackage) @@ -81,10 +86,7 @@ export async function updateCollectionForExpectedPackageIds( } function generateExpectedPackageForDevice( - studio: Pick< - StudioLight, - '_id' | 'packageContainersWithOverrides' | 'previewContainerIds' | 'thumbnailContainerIds' - >, + packageContainerSettings: StudioPackageContainerSettings, expectedPackage: ExpectedPackageDBCompact, deviceId: PeripheralDeviceId, packageContainers: Record @@ -118,7 +120,7 @@ function generateExpectedPackageForDevice( if (!combinedTargets.length) { logger.warn(`Pub.expectedPackagesForDevice: No targets found for "${expectedPackage._id}"`) } - const packageSideEffect = getSideEffect(expectedPackage.package, studio) + const packageSideEffect = getSideEffect(expectedPackage.package, packageContainerSettings) return { _id: protectString(`${expectedPackage._id}_${deviceId}`), diff --git a/meteor/server/publications/packageManager/expectedPackages/publication.ts b/meteor/server/publications/packageManager/expectedPackages/publication.ts index 46328b5ce8a..2fbd90b9e1e 100644 --- a/meteor/server/publications/packageManager/expectedPackages/publication.ts +++ b/meteor/server/publications/packageManager/expectedPackages/publication.ts @@ -30,6 +30,7 @@ import { PeripheralDevicePubSubCollectionsNames, } from '@sofie-automation/shared-lib/dist/pubsub/peripheralDevice' import { checkAccessAndGetPeripheralDevice } from '../../../security/check' +import { StudioPackageContainerSettings } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' interface ExpectedPackagesPublicationArgs { readonly studioId: StudioId @@ -50,6 +51,7 @@ interface ExpectedPackagesPublicationState { studio: Pick | undefined layerNameToDeviceIds: Map packageContainers: Record + packageContainerSettings: StudioPackageContainerSettings contentCache: ReadonlyDeep } @@ -59,15 +61,13 @@ export type StudioFields = | 'routeSetsWithOverrides' | 'mappingsWithOverrides' | 'packageContainersWithOverrides' - | 'previewContainerIds' - | 'thumbnailContainerIds' + | 'packageContainerSettingsWithOverrides' const studioFieldSpecifier = literal>>({ _id: 1, routeSetsWithOverrides: 1, mappingsWithOverrides: 1, packageContainersWithOverrides: 1, - previewContainerIds: 1, - thumbnailContainerIds: 1, + packageContainerSettingsWithOverrides: 1, }) async function setupExpectedPackagesPublicationObservers( @@ -125,6 +125,8 @@ async function manipulateExpectedPackagesPublicationData( if (!state.layerNameToDeviceIds) state.layerNameToDeviceIds = new Map() if (!state.packageContainers) state.packageContainers = {} + if (!state.packageContainerSettings) + state.packageContainerSettings = { previewContainerIds: [], thumbnailContainerIds: [] } if (invalidateAllItems) { // Everything is invalid, reset everything @@ -145,6 +147,7 @@ async function manipulateExpectedPackagesPublicationData( logger.warn(`Pub.expectedPackagesForDevice: studio "${args.studioId}" not found!`) state.layerNameToDeviceIds = new Map() state.packageContainers = {} + state.packageContainerSettings = { previewContainerIds: [], thumbnailContainerIds: [] } } else { const studioMappings = applyAndValidateOverrides(state.studio.mappingsWithOverrides).obj state.layerNameToDeviceIds = buildMappingsToDeviceIdMap( @@ -152,6 +155,9 @@ async function manipulateExpectedPackagesPublicationData( studioMappings ) state.packageContainers = applyAndValidateOverrides(state.studio.packageContainersWithOverrides).obj + state.packageContainerSettings = applyAndValidateOverrides( + state.studio.packageContainerSettingsWithOverrides + ).obj } } @@ -173,7 +179,7 @@ async function manipulateExpectedPackagesPublicationData( await updateCollectionForExpectedPackageIds( state.contentCache, - state.studio, + state.packageContainerSettings, state.layerNameToDeviceIds, state.packageContainers, collection, diff --git a/meteor/server/publications/packageManager/expectedPackages/util.ts b/meteor/server/publications/packageManager/expectedPackages/util.ts index 08fe01ab560..54316fcf360 100644 --- a/meteor/server/publications/packageManager/expectedPackages/util.ts +++ b/meteor/server/publications/packageManager/expectedPackages/util.ts @@ -3,7 +3,7 @@ import { MappingExt, MappingsExt, StudioRouteSet } from '@sofie-automation/corel import { ReadonlyDeep } from 'type-fest' import { getActiveRoutes, getRoutedMappings } from '@sofie-automation/meteor-lib/dist/collections/Studios' -type MappingExtWithOriginalName = MappingExt & { originalLayerName: string } +type MappingExtWithOriginalName = ReadonlyDeep & { originalLayerName: string } type MappingsExtWithOriginalName = { [layerName: string]: MappingExtWithOriginalName } @@ -13,7 +13,7 @@ export function buildMappingsToDeviceIdMap( ): Map { // Map the expectedPackages onto their specified layer: const mappingsWithPackages: MappingsExtWithOriginalName = {} - for (const [layerName, mapping] of Object.entries(studioMappings)) { + for (const [layerName, mapping] of Object.entries>(studioMappings)) { mappingsWithPackages[layerName] = { ...mapping, originalLayerName: layerName, diff --git a/meteor/server/publications/partsUI/publication.ts b/meteor/server/publications/partsUI/publication.ts index 7e437088687..868956828aa 100644 --- a/meteor/server/publications/partsUI/publication.ts +++ b/meteor/server/publications/partsUI/publication.ts @@ -191,7 +191,7 @@ meteorCustomPublish( MeteorPubSub.uiParts, CustomCollectionName.UIParts, async function (pub, playlistId: RundownPlaylistId | null) { - check(playlistId, Match.Optional(String)) + check(playlistId, Match.Maybe(String)) triggerWriteAccessBecauseNoCheckNecessary() diff --git a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts index f6a8069a8e5..a7573349389 100644 --- a/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts +++ b/meteor/server/publications/pieceContentStatusUI/__tests__/checkPieceContentStatus.test.ts @@ -261,8 +261,10 @@ describe('lib/mediaObjects', () => { const mockStudio: Complete = { _id: mockDefaultStudio._id, settings: mockStudioSettings, - previewContainerIds: ['previews0'], - thumbnailContainerIds: ['thumbnails0'], + packageContainerSettings: { + previewContainerIds: ['previews0'], + thumbnailContainerIds: ['thumbnails0'], + }, routeSets: applyAndValidateOverrides(mockDefaultStudio.routeSetsWithOverrides).obj, mappings: applyAndValidateOverrides(mockDefaultStudio.mappingsWithOverrides).obj, packageContainers: applyAndValidateOverrides(mockDefaultStudio.packageContainersWithOverrides).obj, diff --git a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index f6bf71d0a70..ba2435e8cd3 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -25,7 +25,6 @@ import { } from '@sofie-automation/corelib/dist/dataModel/PackageContainerPackageStatus' import { PieceGeneric, PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { - DBStudio, IStudioSettings, MappingExt, MappingsExt, @@ -57,6 +56,7 @@ import { PieceContentStatusObj } from '@sofie-automation/corelib/dist/dataModel/ import { PieceContentStatusMessageFactory, PieceContentStatusMessageRequiredArgs } from './messageFactory' import { PackageStatusMessage } from '@sofie-automation/shared-lib/dist/packageStatusMessages' import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibPiece' +import { StudioPackageContainerSettings } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' const DEFAULT_MESSAGE_FACTORY = new PieceContentStatusMessageFactory(undefined) @@ -211,8 +211,8 @@ export type PieceContentStatusPiece = Pick< */ previousPieceInstanceId?: PieceInstanceId } -export interface PieceContentStatusStudio - extends Pick { +export interface PieceContentStatusStudio { + _id: StudioId /** Mappings between the physical devices / outputs and logical ones */ mappings: MappingsExt /** Route sets with overrides */ @@ -222,6 +222,8 @@ export interface PieceContentStatusStudio */ packageContainers: Record + packageContainerSettings: StudioPackageContainerSettings + settings: IStudioSettings } @@ -707,7 +709,7 @@ async function checkPieceContentExpectedPackageStatus( } if (!thumbnailUrl) { - const sideEffect = getSideEffect(expectedPackage, studio) + const sideEffect = getSideEffect(expectedPackage, studio.packageContainerSettings) thumbnailUrl = await getAssetUrlFromPackageContainerStatus( studio.packageContainers, @@ -719,7 +721,7 @@ async function checkPieceContentExpectedPackageStatus( } if (!previewUrl) { - const sideEffect = getSideEffect(expectedPackage, studio) + const sideEffect = getSideEffect(expectedPackage, studio.packageContainerSettings) previewUrl = await getAssetUrlFromPackageContainerStatus( studio.packageContainers, @@ -1184,7 +1186,7 @@ function routeExpectedPackage( expectedPackage: ExpectedPackage.Base ): Set { // Collect the relevant mappings - const mappingsWithPackages: MappingsExt = {} + const mappingsWithPackages: { [layerName: string]: ReadonlyDeep } = {} for (const layerName of expectedPackage.layers) { const mapping = studioMappings[layerName] @@ -1199,5 +1201,5 @@ function routeExpectedPackage( const routedMappings = getRoutedMappings(mappingsWithPackages, routes) // Find the referenced deviceIds - return new Set(Object.values(routedMappings).map((mapping) => mapping.deviceId)) + return new Set(Object.values>(routedMappings).map((mapping) => mapping.deviceId)) } diff --git a/meteor/server/publications/pieceContentStatusUI/common.ts b/meteor/server/publications/pieceContentStatusUI/common.ts index f8c7591d535..94a61b1bc24 100644 --- a/meteor/server/publications/pieceContentStatusUI/common.ts +++ b/meteor/server/publications/pieceContentStatusUI/common.ts @@ -15,16 +15,14 @@ export type StudioFields = | '_id' | 'settingsWithOverrides' | 'packageContainersWithOverrides' - | 'previewContainerIds' - | 'thumbnailContainerIds' + | 'packageContainerSettingsWithOverrides' | 'mappingsWithOverrides' | 'routeSetsWithOverrides' export const studioFieldSpecifier = literal>>({ _id: 1, settingsWithOverrides: 1, packageContainersWithOverrides: 1, - previewContainerIds: 1, - thumbnailContainerIds: 1, + packageContainerSettingsWithOverrides: 1, mappingsWithOverrides: 1, routeSetsWithOverrides: 1, }) @@ -113,8 +111,7 @@ export async function fetchStudio(studioId: StudioId): Promise=18.0.0" - axios: "npm:^1.7.8" - checksum: 10/f4a3c7400b2281622eb2a3ed992425e4f777e80876cd69b0d8897fe3d5f5dfac4008131fd9afdd1d7bcb6ba00e5e562c7e6df7236e16bd6447d0c85b25930d23 + axios: "npm:^1.11.0" + checksum: 10/8f8083f9654e590f04731985b337f576842b2034a9261010f85d813c4e262f69d856c142b0dcf2022bfe69c22c2e97cc7d877a79989cd0f7a0cf2554ae0754ed + languageName: node + linkType: hard + +"@so-ric/colorspace@npm:^1.1.6": + version: 1.1.6 + resolution: "@so-ric/colorspace@npm:1.1.6" + dependencies: + color: "npm:^5.0.2" + text-hex: "npm:1.0.x" + checksum: 10/fc3285e5cb9a458d255aa678d9453174ca40689a4c692f1617907996ab8eb78839542439604ced484c4f674a5297f7ba8b0e63fcfe901174f43c3d9c3c881b52 languageName: node linkType: hard @@ -1158,9 +1285,9 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@portal:../packages/blueprints-integration::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" + "@sofie-automation/shared-lib": "npm:26.3.0-2" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" languageName: node linkType: soft @@ -1194,17 +1321,17 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/corelib@portal:../packages/corelib::locator=automation-core%40workspace%3A." dependencies: - "@sofie-automation/blueprints-integration": "npm:1.53.0-in-development" - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" + "@sofie-automation/blueprints-integration": "npm:26.3.0-2" + "@sofie-automation/shared-lib": "npm:26.3.0-2" fast-clone: "npm:^1.5.13" i18next: "npm:^21.10.0" - influx: "npm:^5.9.7" - nanoid: "npm:^3.3.8" + influx: "npm:^5.12.0" + nanoid: "npm:^3.3.11" object-path: "npm:^0.11.8" prom-client: "npm:^15.1.3" timecode: "npm:0.0.4" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" underscore: "npm:^1.13.7" peerDependencies: mongodb: ^6.12.0 @@ -1226,20 +1353,21 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/job-worker@portal:../packages/job-worker::locator=automation-core%40workspace%3A." dependencies: - "@slack/webhook": "npm:^7.0.4" - "@sofie-automation/blueprints-integration": "npm:1.53.0-in-development" - "@sofie-automation/corelib": "npm:1.53.0-in-development" - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" - amqplib: "npm:^0.10.5" + "@slack/webhook": "npm:^7.0.6" + "@sofie-automation/blueprints-integration": "npm:26.3.0-2" + "@sofie-automation/corelib": "npm:26.3.0-2" + "@sofie-automation/shared-lib": "npm:26.3.0-2" + amqplib: "npm:0.10.5" + chrono-node: "npm:^2.9.0" deepmerge: "npm:^4.3.1" - elastic-apm-node: "npm:^4.11.0" - mongodb: "npm:^6.12.0" + elastic-apm-node: "npm:^4.15.0" + mongodb: "npm:^6.21.0" p-lazy: "npm:^3.1.0" p-timeout: "npm:^4.1.0" superfly-timeline: "npm:9.2.0" - threadedclass: "npm:^1.2.2" + threadedclass: "npm:^1.3.0" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" underscore: "npm:^1.13.7" languageName: node linkType: soft @@ -1249,12 +1377,12 @@ __metadata: resolution: "@sofie-automation/meteor-lib@portal:../packages/meteor-lib::locator=automation-core%40workspace%3A." dependencies: "@mos-connection/helper": "npm:^5.0.0-alpha.0" - "@sofie-automation/blueprints-integration": "npm:1.53.0-in-development" - "@sofie-automation/corelib": "npm:1.53.0-in-development" - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" + "@sofie-automation/blueprints-integration": "npm:26.3.0-2" + "@sofie-automation/corelib": "npm:26.3.0-2" + "@sofie-automation/shared-lib": "npm:26.3.0-2" deep-extend: "npm:0.6.0" - semver: "npm:^7.6.3" - type-fest: "npm:^4.33.0" + semver: "npm:^7.7.3" + type-fest: "npm:^4.41.0" underscore: "npm:^1.13.7" peerDependencies: i18next: ^21.10.0 @@ -1268,9 +1396,9 @@ __metadata: dependencies: "@mos-connection/model": "npm:^5.0.0-alpha.0" kairos-lib: "npm:^0.2.3" - timeline-state-resolver-types: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" + timeline-state-resolver-types: "npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" languageName: node linkType: soft @@ -1281,10 +1409,12 @@ __metadata: languageName: node linkType: hard -"@tootallnate/once@npm:2": - version: 2.0.0 - resolution: "@tootallnate/once@npm:2.0.0" - checksum: 10/ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 +"@tybys/wasm-util@npm:^0.10.0": + version: 0.10.1 + resolution: "@tybys/wasm-util@npm:0.10.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/7fe0d239397aebb002ac4855d30c197c06a05ea8df8511350a3a5b1abeefe26167c60eda8a5508337571161e4c4b53d7c1342296123f9607af8705369de9fa7f languageName: node linkType: hard @@ -1297,23 +1427,16 @@ __metadata: languageName: node linkType: hard -"@types/app-root-path@npm:^1.2.8": - version: 1.2.8 - resolution: "@types/app-root-path@npm:1.2.8" - checksum: 10/540640e6408b81632271b878d3aeb911e437b9777903a4b671e5c085f6a244fa6069c7c835a3c2ac278c45e6092edbb450aaf5311c2810552a7426bf186bf56f - languageName: node - linkType: hard - -"@types/babel__core@npm:^7.1.14": - version: 7.20.2 - resolution: "@types/babel__core@npm:7.20.2" +"@types/babel__core@npm:^7.20.5": + version: 7.20.5 + resolution: "@types/babel__core@npm:7.20.5" dependencies: "@babel/parser": "npm:^7.20.7" "@babel/types": "npm:^7.20.7" "@types/babel__generator": "npm:*" "@types/babel__template": "npm:*" "@types/babel__traverse": "npm:*" - checksum: 10/78aede009117ff6c95ef36db19e27ad15ecdcb5cfc9ad57d43caa5d2f44127105691a3e6e8d1806fd305484db8a74fdec5640e88da452c511f6351353f7ac0c8 + checksum: 10/c32838d280b5ab59d62557f9e331d3831f8e547ee10b4f85cb78753d97d521270cebfc73ce501e9fb27fe71884d1ba75e18658692c2f4117543f0fc4e3e118b3 languageName: node linkType: hard @@ -1336,7 +1459,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6": +"@types/babel__traverse@npm:*": version: 7.20.2 resolution: "@types/babel__traverse@npm:7.20.2" dependencies: @@ -1345,20 +1468,13 @@ __metadata: languageName: node linkType: hard -"@types/body-parser@npm:*, @types/body-parser@npm:^1.19.5": - version: 1.19.5 - resolution: "@types/body-parser@npm:1.19.5" +"@types/body-parser@npm:*, @types/body-parser@npm:^1.19.6": + version: 1.19.6 + resolution: "@types/body-parser@npm:1.19.6" dependencies: "@types/connect": "npm:*" "@types/node": "npm:*" - checksum: 10/1e251118c4b2f61029cc43b0dc028495f2d1957fe8ee49a707fb940f86a9bd2f9754230805598278fe99958b49e9b7e66eec8ef6a50ab5c1f6b93e1ba2aaba82 - languageName: node - linkType: hard - -"@types/caseless@npm:*": - version: 0.12.3 - resolution: "@types/caseless@npm:0.12.3" - checksum: 10/ec696955d914493adfe32a2e6f1c7ac066585312fe63a487b7b2e98386842b376d0ca88ca1c802a44298c723c3351c075d3c8f529f543369576aebb1592c4c06 + checksum: 10/33041e88eae00af2cfa0827e951e5f1751eafab2a8b6fce06cd89ef368a988907996436b1325180edaeddd1c0c7d0d0d4c20a6c9ff294a91e0039a9db9e9b658 languageName: node linkType: hard @@ -1428,15 +1544,6 @@ __metadata: languageName: node linkType: hard -"@types/graceful-fs@npm:^4.1.3": - version: 4.1.6 - resolution: "@types/graceful-fs@npm:4.1.6" - dependencies: - "@types/node": "npm:*" - checksum: 10/c3070ccdc9ca0f40df747bced1c96c71a61992d6f7c767e8fd24bb6a3c2de26e8b84135ede000b7e79db530a23e7e88dcd9db60eee6395d0f4ce1dae91369dd4 - languageName: node - linkType: hard - "@types/http-assert@npm:*": version: 1.5.3 resolution: "@types/http-assert@npm:1.5.3" @@ -1444,17 +1551,17 @@ __metadata: languageName: node linkType: hard -"@types/http-errors@npm:*": - version: 2.0.2 - resolution: "@types/http-errors@npm:2.0.2" - checksum: 10/d7f14045240ac4b563725130942b8e5c8080bfabc724c8ff3f166ea928ff7ae02c5194763bc8f6aaf21897e8a44049b0492493b9de3e058247e58fdfe0f86692 +"@types/http-errors@npm:*, @types/http-errors@npm:^2": + version: 2.0.5 + resolution: "@types/http-errors@npm:2.0.5" + checksum: 10/a88da669366bc483e8f3b3eb3d34ada5f8d13eeeef851b1204d77e2ba6fc42aba4566d877cca5c095204a3f4349b87fe397e3e21288837bdd945dd514120755b languageName: node linkType: hard -"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": - version: 2.0.4 - resolution: "@types/istanbul-lib-coverage@npm:2.0.4" - checksum: 10/a25d7589ee65c94d31464c16b72a9dc81dfa0bea9d3e105ae03882d616e2a0712a9c101a599ec482d297c3591e16336962878cb3eb1a0a62d5b76d277a890ce7 +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6": + version: 2.0.6 + resolution: "@types/istanbul-lib-coverage@npm:2.0.6" + checksum: 10/3feac423fd3e5449485afac999dcfcb3d44a37c830af898b689fadc65d26526460bedb889db278e0d4d815a670331796494d073a10ee6e3a6526301fe7415778 languageName: node linkType: hard @@ -1467,22 +1574,22 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-reports@npm:^3.0.0": - version: 3.0.1 - resolution: "@types/istanbul-reports@npm:3.0.1" +"@types/istanbul-reports@npm:^3.0.4": + version: 3.0.4 + resolution: "@types/istanbul-reports@npm:3.0.4" dependencies: "@types/istanbul-lib-report": "npm:*" - checksum: 10/f1ad54bc68f37f60b30c7915886b92f86b847033e597f9b34f2415acdbe5ed742fa559a0a40050d74cdba3b6a63c342cac1f3a64dba5b68b66a6941f4abd7903 + checksum: 10/93eb18835770b3431f68ae9ac1ca91741ab85f7606f310a34b3586b5a34450ec038c3eed7ab19266635499594de52ff73723a54a72a75b9f7d6a956f01edee95 languageName: node linkType: hard -"@types/jest@npm:^29.5.14": - version: 29.5.14 - resolution: "@types/jest@npm:29.5.14" +"@types/jest@npm:^30.0.0": + version: 30.0.0 + resolution: "@types/jest@npm:30.0.0" dependencies: - expect: "npm:^29.0.0" - pretty-format: "npm:^29.0.0" - checksum: 10/59ec7a9c4688aae8ee529316c43853468b6034f453d08a2e1064b281af9c81234cec986be796288f1bbb29efe943bc950e70c8fa8faae1e460d50e3cf9760f9b + expect: "npm:^30.0.0" + pretty-format: "npm:^30.0.0" + checksum: 10/cdeaa924c68b5233d9ff92861a89e7042df2b0f197633729bcf3a31e65bd4e9426e751c5665b5ac2de0b222b33f100a5502da22aefce3d2c62931c715e88f209 languageName: node linkType: hard @@ -1500,12 +1607,12 @@ __metadata: languageName: node linkType: hard -"@types/koa-bodyparser@npm:^4.3.12": - version: 4.3.12 - resolution: "@types/koa-bodyparser@npm:4.3.12" +"@types/koa-bodyparser@npm:^4.3.13": + version: 4.3.13 + resolution: "@types/koa-bodyparser@npm:4.3.13" dependencies: "@types/koa": "npm:*" - checksum: 10/645cc253c6b9b2e98252b1cdc75a4812cd6d3c228e426f9893a755324b7a6936559ec659a0ff288cb2642340b3cc4e2110167f24b84efc8e3b89c04fe67ed883 + checksum: 10/684856d19fd35033f61eb2d99bb94f378cb4f397ddd1a2c4c852105fb5957f0b23be6e2307acd754b3c105e98c88228caadef9785b4a077f014af16afe4bd657 languageName: node linkType: hard @@ -1518,7 +1625,7 @@ __metadata: languageName: node linkType: hard -"@types/koa-mount@npm:^4": +"@types/koa-mount@npm:^4.0.5": version: 4.0.5 resolution: "@types/koa-mount@npm:4.0.5" dependencies: @@ -1546,37 +1653,28 @@ __metadata: languageName: node linkType: hard -"@types/koa@npm:*, @types/koa@npm:^2.15.0": - version: 2.15.0 - resolution: "@types/koa@npm:2.15.0" +"@types/koa@npm:*, @types/koa@npm:^3.0.1": + version: 3.0.1 + resolution: "@types/koa@npm:3.0.1" dependencies: "@types/accepts": "npm:*" "@types/content-disposition": "npm:*" "@types/cookies": "npm:*" "@types/http-assert": "npm:*" - "@types/http-errors": "npm:*" + "@types/http-errors": "npm:^2" "@types/keygrip": "npm:*" "@types/koa-compose": "npm:*" "@types/node": "npm:*" - checksum: 10/2be9dff1ef66bf15b037386c188893761a8fb46390a5e1d2a2031d9e1ba4473e40ddfbd625980a504bd804d7148b3e230c18e240503f33eac3b6e5e830645d30 + checksum: 10/b1581d31d562bb5d9f61bc0148652abffc701c39930eb77a57b7d1f43aaad56ffa1970f6f3d4d6a0a56395a6832e2711a3278850dcc5a6c986ba6ed2cd0f4f1f languageName: node linkType: hard -"@types/koa__cors@npm:^5.0.0": - version: 5.0.0 - resolution: "@types/koa__cors@npm:5.0.0" - dependencies: - "@types/koa": "npm:*" - checksum: 10/ad8e6a482f1bb0e357e0051faec328a75e2978a24065a953032d5dba58ac08edf5ca66b03059551f0faf9e085b15ee7892e6ab03c9500af4be8bd258965479c9 - languageName: node - linkType: hard - -"@types/koa__router@npm:^12.0.4": - version: 12.0.4 - resolution: "@types/koa__router@npm:12.0.4" +"@types/koa__cors@npm:^5.0.1": + version: 5.0.1 + resolution: "@types/koa__cors@npm:5.0.1" dependencies: "@types/koa": "npm:*" - checksum: 10/c01311980bf9a921b77cca5a93cc85522a6d13fe49575e6190fa80407a60237e7351d99a399316dda3119641d498f5d8236b905cd3b4f54fad2c0839ab655dd4 + checksum: 10/552db24607ce394130c2ae0e1bd443237f0174702feaf81fe45afc430ca467c6760084a1a93e096be3a2a18160220b37bc6ae2cf48ed0e198d273f557ee1bf64 languageName: node linkType: hard @@ -1601,12 +1699,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=18.0.0, @types/node@npm:^22.10.10": - version: 22.13.1 - resolution: "@types/node@npm:22.13.1" +"@types/node@npm:*, @types/node@npm:>=18.0.0, @types/node@npm:^22.19.8": + version: 22.19.8 + resolution: "@types/node@npm:22.19.8" dependencies: - undici-types: "npm:~6.20.0" - checksum: 10/d8ba7068b0445643c0fa6e4917cdb7a90e8756a9daff8c8a332689cd5b2eaa01e4cd07de42e3cd7e6a6f465eeda803d5a1363d00b5ab3f6cea7950350a159497 + undici-types: "npm:~6.21.0" + checksum: 10/a61c68d434871d4a13496e3607502b2ff8e2ff69dca7e09228de5bea3bc95eb627d09243a8cff8e0bf9ff1fa13baaf0178531748f59ae81f0569c7a2f053bfa5 languageName: node linkType: hard @@ -1631,22 +1729,10 @@ __metadata: languageName: node linkType: hard -"@types/request@npm:^2.48.12": - version: 2.48.12 - resolution: "@types/request@npm:2.48.12" - dependencies: - "@types/caseless": "npm:*" - "@types/node": "npm:*" - "@types/tough-cookie": "npm:*" - form-data: "npm:^2.5.0" - checksum: 10/a7b3f9f14cacc18fe235bb8e57eff1232a04bd3fa3dad29371f24a5d96db2cd295a0c8b6b34ed7efa3efbbcff845febb02c9635cd68c54811c947ea66ae22090 - languageName: node - linkType: hard - -"@types/semver@npm:^7.5.8": - version: 7.5.8 - resolution: "@types/semver@npm:7.5.8" - checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 +"@types/semver@npm:^7.7.1": + version: 7.7.1 + resolution: "@types/semver@npm:7.7.1" + checksum: 10/8f09e7e6ca3ded67d78ba7a8f7535c8d9cf8ced83c52e7f3ac3c281fe8c689c3fe475d199d94390dc04fc681d51f2358b430bb7b2e21c62de24f2bee2c719068 languageName: node linkType: hard @@ -1671,17 +1757,10 @@ __metadata: languageName: node linkType: hard -"@types/stack-utils@npm:^2.0.0": - version: 2.0.1 - resolution: "@types/stack-utils@npm:2.0.1" - checksum: 10/205fdbe3326b7046d7eaf5e494d8084f2659086a266f3f9cf00bccc549c8e36e407f88168ad4383c8b07099957ad669f75f2532ed4bc70be2b037330f7bae019 - languageName: node - linkType: hard - -"@types/tough-cookie@npm:*": - version: 4.0.3 - resolution: "@types/tough-cookie@npm:4.0.3" - checksum: 10/32d17b50766357b0297762d4ee0e42b430f36f0397eec38559b9ce18120f64f07922cca3ecc6bdb5a097ba75ec71e91879bdb89a7c532de43b5eff6775625334 +"@types/stack-utils@npm:^2.0.3": + version: 2.0.3 + resolution: "@types/stack-utils@npm:2.0.3" + checksum: 10/72576cc1522090fe497337c2b99d9838e320659ac57fa5560fcbdcbafcf5d0216c6b3a0a8a4ee4fdb3b1f5e3420aa4f6223ab57b82fef3578bec3206425c6cf5 languageName: node linkType: hard @@ -1722,12 +1801,12 @@ __metadata: languageName: node linkType: hard -"@types/yargs@npm:^17.0.8": - version: 17.0.24 - resolution: "@types/yargs@npm:17.0.24" +"@types/yargs@npm:^17.0.33": + version: 17.0.35 + resolution: "@types/yargs@npm:17.0.35" dependencies: "@types/yargs-parser": "npm:*" - checksum: 10/03d9a985cb9331b2194a52d57a66aad88bf46aa32b3968a71cc6f39fb05c74f0709f0dd3aa9c0b29099cfe670343e3b1bd2ac6df2abfab596ede4453a616f63f + checksum: 10/47bcd4476a4194ea11617ea71cba8a1eddf5505fc39c44336c1a08d452a0de4486aedbc13f47a017c8efbcb5a8aa358d976880663732ebcbc6dbcbbecadb0581 languageName: node linkType: hard @@ -1818,32 +1897,174 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.23.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.21.0": - version: 8.23.0 - resolution: "@typescript-eslint/utils@npm:8.23.0" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.23.0" - "@typescript-eslint/types": "npm:8.23.0" - "@typescript-eslint/typescript-estree": "npm:8.23.0" - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.8.0" - checksum: 10/72588d617ee5b1fa1020d008a7ff714a4a1e0fc1167aa9ff4b8ae71a37b25f43b2d40bca3380c56bb84d4092b6cac8d5d14d74e290e80217175ccf8237faf22a +"@typescript-eslint/utils@npm:8.23.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.21.0": + version: 8.23.0 + resolution: "@typescript-eslint/utils@npm:8.23.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.23.0" + "@typescript-eslint/types": "npm:8.23.0" + "@typescript-eslint/typescript-estree": "npm:8.23.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ">=4.8.4 <5.8.0" + checksum: 10/72588d617ee5b1fa1020d008a7ff714a4a1e0fc1167aa9ff4b8ae71a37b25f43b2d40bca3380c56bb84d4092b6cac8d5d14d74e290e80217175ccf8237faf22a + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:8.23.0": + version: 8.23.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.23.0" + dependencies: + "@typescript-eslint/types": "npm:8.23.0" + eslint-visitor-keys: "npm:^4.2.0" + checksum: 10/fd473849b85e564e31aec64feb3417a4e16e48bf21f1959fbab56258e19c21ef47bbdb523c64a8921cdc82a5083735418890b6f74b564fd1ece305c977a0f7a6 + languageName: node + linkType: hard + +"@ungap/structured-clone@npm:^1.3.0": + version: 1.3.0 + resolution: "@ungap/structured-clone@npm:1.3.0" + checksum: 10/80d6910946f2b1552a2406650051c91bbd1f24a6bf854354203d84fe2714b3e8ce4618f49cc3410494173a1c1e8e9777372fe68dce74bd45faf0a7a1a6ccf448 + languageName: node + linkType: hard + +"@unrs/resolver-binding-android-arm-eabi@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-android-arm-eabi@npm:1.11.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@unrs/resolver-binding-android-arm64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-android-arm64@npm:1.11.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-darwin-arm64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-darwin-arm64@npm:1.11.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-darwin-x64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-darwin-x64@npm:1.11.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-freebsd-x64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-freebsd-x64@npm:1.11.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.11.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-arm-musleabihf@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm-musleabihf@npm:1.11.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-arm64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm64-gnu@npm:1.11.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-arm64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm64-musl@npm:1.11.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-ppc64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-ppc64-gnu@npm:1.11.1" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-riscv64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-riscv64-gnu@npm:1.11.1" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-riscv64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-riscv64-musl@npm:1.11.1" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-s390x-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-s390x-gnu@npm:1.11.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-x64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-x64-gnu@npm:1.11.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-x64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-x64-musl@npm:1.11.1" + conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.23.0": - version: 8.23.0 - resolution: "@typescript-eslint/visitor-keys@npm:8.23.0" +"@unrs/resolver-binding-wasm32-wasi@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-wasm32-wasi@npm:1.11.1" dependencies: - "@typescript-eslint/types": "npm:8.23.0" - eslint-visitor-keys: "npm:^4.2.0" - checksum: 10/fd473849b85e564e31aec64feb3417a4e16e48bf21f1959fbab56258e19c21ef47bbdb523c64a8921cdc82a5083735418890b6f74b564fd1ece305c977a0f7a6 + "@napi-rs/wasm-runtime": "npm:^0.2.11" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@unrs/resolver-binding-win32-arm64-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-arm64-msvc@npm:1.11.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-win32-ia32-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-ia32-msvc@npm:1.11.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@unrs/resolver-binding-win32-x64-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-x64-msvc@npm:1.11.1" + conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"JSONStream@npm:^1.0.4": +"JSONStream@npm:^1.3.5": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" dependencies: @@ -1855,13 +2076,20 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:1, abbrev@npm:^1.0.0": +"abbrev@npm:1": version: 1.1.1 resolution: "abbrev@npm:1.1.1" checksum: 10/2d882941183c66aa665118bafdab82b7a177e9add5eb2776c33e960a4f3c89cff88a1b38aba13a456de01d0dd9d66a8bea7c903268b21ea91dd1097e1e2e8243 languageName: node linkType: hard +"abbrev@npm:^4.0.0": + version: 4.0.0 + resolution: "abbrev@npm:4.0.0" + checksum: 10/e2f0c6a6708ad738b3e8f50233f4800de31ad41a6cdc50e0cbe51b76fed69fd0213516d92c15ce1a9985fca71a14606a9be22bf00f8475a58987b9bfb671c582 + languageName: node + linkType: hard + "abort-controller@npm:^3.0.0": version: 3.0.0 resolution: "abort-controller@npm:3.0.0" @@ -1871,7 +2099,7 @@ __metadata: languageName: node linkType: hard -"accepts@npm:^1.3.5, accepts@npm:^1.3.7": +"accepts@npm:^1.3.5, accepts@npm:^1.3.7, accepts@npm:^1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" dependencies: @@ -1961,7 +2189,16 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.4, acorn@npm:^8.15.0, acorn@npm:^8.8.2": +"acorn@npm:^8.0.4, acorn@npm:^8.14.0": + version: 8.14.0 + resolution: "acorn@npm:8.14.0" + bin: + acorn: bin/acorn + checksum: 10/6df29c35556782ca9e632db461a7f97947772c6c1d5438a81f0c873a3da3a792487e83e404d1c6c25f70513e91aa18745f6eafb1fcc3a43ecd1920b21dd173d2 + languageName: node + linkType: hard + +"acorn@npm:^8.15.0": version: 8.15.0 resolution: "acorn@npm:8.15.0" bin: @@ -1984,12 +2221,10 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:6, agent-base@npm:^6.0.2": - version: 6.0.2 - resolution: "agent-base@npm:6.0.2" - dependencies: - debug: "npm:4" - checksum: 10/21fb903e0917e5cb16591b4d0ef6a028a54b83ac30cd1fca58dece3d4e0990512a8723f9f83130d88a41e2af8b1f7be1386fda3ea2d181bb1a62155e75e95e23 +"agent-base@npm:^7.1.0, agent-base@npm:^7.1.2": + version: 7.1.4 + resolution: "agent-base@npm:7.1.4" + checksum: 10/79bef167247789f955aaba113bae74bf64aa1e1acca4b1d6bb444bdf91d82c3e07e9451ef6a6e2e35e8f71a6f97ce33e3d855a5328eb9fad1bc3cc4cfd031ed8 languageName: node linkType: hard @@ -2002,29 +2237,19 @@ __metadata: languageName: node linkType: hard -"aggregate-error@npm:^3.0.0": - version: 3.1.0 - resolution: "aggregate-error@npm:3.1.0" - dependencies: - clean-stack: "npm:^2.0.0" - indent-string: "npm:^4.0.0" - checksum: 10/1101a33f21baa27a2fa8e04b698271e64616b886795fd43c31068c07533c7b3facfcaf4e9e0cab3624bd88f729a592f1c901a1a229c9e490eafce411a8644b79 - languageName: node - linkType: hard - "ajv@npm:^6.12.4": - version: 6.12.6 - resolution: "ajv@npm:6.12.6" + version: 6.14.0 + resolution: "ajv@npm:6.14.0" dependencies: fast-deep-equal: "npm:^3.1.1" fast-json-stable-stringify: "npm:^2.0.0" json-schema-traverse: "npm:^0.4.1" uri-js: "npm:^4.2.2" - checksum: 10/48d6ad21138d12eb4d16d878d630079a2bda25a04e745c07846a4ad768319533031e28872a9b3c5790fa1ec41aabdf2abed30a56e5a03ebc2cf92184b8ee306c + checksum: 10/c71f14dd2b6f2535d043f74019c8169f7aeb1106bafbb741af96f34fdbf932255c919ddd46344043d03b62ea0ccb319f83667ec5eedf612393f29054fe5ce4a5 languageName: node linkType: hard -"amqplib@npm:^0.10.5": +"amqplib@npm:0.10.5": version: 0.10.5 resolution: "amqplib@npm:0.10.5" dependencies: @@ -2035,7 +2260,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1": +"ansi-escapes@npm:^4.3.2": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -2052,9 +2277,9 @@ __metadata: linkType: hard "ansi-regex@npm:^6.0.1": - version: 6.0.1 - resolution: "ansi-regex@npm:6.0.1" - checksum: 10/1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 + version: 6.2.2 + resolution: "ansi-regex@npm:6.2.2" + checksum: 10/9b17ce2c6daecc75bcd5966b9ad672c23b184dc3ed9bf3c98a0702f0d2f736c15c10d461913568f2cf527a5e64291c7473358885dd493305c84a1cfed66ba94f languageName: node linkType: hard @@ -2076,7 +2301,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^5.0.0": +"ansi-styles@npm:^5.2.0": version: 5.2.0 resolution: "ansi-styles@npm:5.2.0" checksum: 10/d7f4e97ce0623aea6bc0d90dcd28881ee04cba06c570b97fd3391bd7a268eedfd9d5e2dd4fdcbdd82b8105df5faf6f24aaedc08eaf3da898e702db5948f63469 @@ -2084,13 +2309,13 @@ __metadata: linkType: hard "ansi-styles@npm:^6.1.0": - version: 6.2.1 - resolution: "ansi-styles@npm:6.2.1" - checksum: 10/70fdf883b704d17a5dfc9cde206e698c16bcd74e7f196ab821511651aee4f9f76c9514bdfa6ca3a27b5e49138b89cb222a28caf3afe4567570139577f991df32 + version: 6.2.3 + resolution: "ansi-styles@npm:6.2.3" + checksum: 10/c49dad7639f3e48859bd51824c93b9eb0db628afc243c51c3dd2410c4a15ede1a83881c6c7341aa2b159c4f90c11befb38f2ba848c07c66c9f9de4bcd7cb9f30 languageName: node linkType: hard -"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3": +"anymatch@npm:^3.1.3": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -2100,40 +2325,6 @@ __metadata: languageName: node linkType: hard -"app-root-path@npm:^3.1.0": - version: 3.1.0 - resolution: "app-root-path@npm:3.1.0" - checksum: 10/b4cdab5f7e51ec43fa04c97eca2adedf8e18d6c3dd21cd775b70457c5e71f0441c692a49dcceb426f192640b7393dcd41d85c36ef98ecb7c785a53159c912def - languageName: node - linkType: hard - -"aproba@npm:^1.0.3 || ^2.0.0": - version: 2.0.0 - resolution: "aproba@npm:2.0.0" - checksum: 10/c2b9a631298e8d6f3797547e866db642f68493808f5b37cd61da778d5f6ada890d16f668285f7d60bd4fc3b03889bd590ffe62cf81b700e9bb353431238a0a7b - languageName: node - linkType: hard - -"are-we-there-yet@npm:^2.0.0": - version: 2.0.0 - resolution: "are-we-there-yet@npm:2.0.0" - dependencies: - delegates: "npm:^1.0.0" - readable-stream: "npm:^3.6.0" - checksum: 10/ea6f47d14fc33ae9cbea3e686eeca021d9d7b9db83a306010dd04ad5f2c8b7675291b127d3fcbfcbd8fec26e47b3324ad5b469a6cc3733a582f2fe4e12fc6756 - languageName: node - linkType: hard - -"are-we-there-yet@npm:^3.0.0": - version: 3.0.1 - resolution: "are-we-there-yet@npm:3.0.1" - dependencies: - delegates: "npm:^1.0.0" - readable-stream: "npm:^3.6.0" - checksum: 10/390731720e1bf9ed5d0efc635ea7df8cbc4c90308b0645a932f06e8495a0bf1ecc7987d3b97e805f62a17d6c4b634074b25200aa4d149be2a7b17250b9744bc4 - languageName: node - linkType: hard - "argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -2289,74 +2480,70 @@ __metadata: version: 0.0.0-use.local resolution: "automation-core@workspace:." dependencies: - "@babel/core": "npm:^7.26.7" - "@babel/plugin-transform-modules-commonjs": "npm:^7.26.3" - "@babel/runtime": "npm:^7.26.7" + "@babel/core": "npm:^7.29.0" + "@babel/plugin-transform-modules-commonjs": "npm:^7.28.6" + "@babel/runtime": "npm:^7.28.6" "@koa/cors": "npm:^5.0.0" - "@koa/router": "npm:^13.1.0" + "@koa/router": "npm:^15.3.0" "@mos-connection/helper": "npm:^5.0.0-alpha.0" "@shopify/jest-koa-mocks": "npm:^5.3.1" - "@slack/webhook": "npm:^7.0.4" + "@slack/webhook": "npm:^7.0.6" "@sofie-automation/blueprints-integration": "portal:../packages/blueprints-integration" "@sofie-automation/code-standard-preset": "npm:^3.0.0" "@sofie-automation/corelib": "portal:../packages/corelib" "@sofie-automation/job-worker": "portal:../packages/job-worker" "@sofie-automation/meteor-lib": "portal:../packages/meteor-lib" "@sofie-automation/shared-lib": "portal:../packages/shared-lib" - "@types/app-root-path": "npm:^1.2.8" - "@types/body-parser": "npm:^1.19.5" + "@types/body-parser": "npm:^1.19.6" "@types/deep-extend": "npm:^0.6.2" - "@types/jest": "npm:^29.5.14" - "@types/koa": "npm:^2.15.0" - "@types/koa-bodyparser": "npm:^4.3.12" - "@types/koa-mount": "npm:^4" + "@types/jest": "npm:^30.0.0" + "@types/koa": "npm:^3.0.1" + "@types/koa-bodyparser": "npm:^4.3.13" + "@types/koa-mount": "npm:^4.0.5" "@types/koa-static": "npm:^4.0.4" - "@types/koa__cors": "npm:^5.0.0" - "@types/koa__router": "npm:^12.0.4" - "@types/node": "npm:^22.10.10" - "@types/request": "npm:^2.48.12" - "@types/semver": "npm:^7.5.8" + "@types/koa__cors": "npm:^5.0.1" + "@types/node": "npm:^22.19.8" + "@types/semver": "npm:^7.7.1" "@types/underscore": "npm:^1.13.0" - app-root-path: "npm:^3.1.0" - babel-jest: "npm:^29.7.0" - bcrypt: "npm:^5.1.1" - body-parser: "npm:^1.20.3" + babel-jest: "npm:^30.2.0" + bcrypt: "npm:^6.0.0" + body-parser: "npm:^1.20.4" + commit-and-tag-version: "npm:^12.6.1" deep-extend: "npm:0.6.0" deepmerge: "npm:^4.3.1" ejson: "npm:^2.2.3" - elastic-apm-node: "npm:^4.11.0" - eslint: "npm:^9.18.0" + elastic-apm-node: "npm:^4.15.0" + eslint: "npm:^9.39.2" fast-clone: "npm:^1.5.13" - glob: "npm:^11.0.1" + glob: "npm:^13.0.1" i18next: "npm:^21.10.0" i18next-conv: "npm:^10.2.0" i18next-scanner: "npm:^4.6.0" indexof: "npm:0.0.1" - jest: "npm:^29.7.0" - koa: "npm:^2.15.3" + jest: "npm:^30.2.0" + jest-util: "npm:^30.2.0" + koa: "npm:^3.1.1" koa-bodyparser: "npm:^4.4.1" - koa-mount: "npm:^4.0.0" + koa-mount: "npm:^4.2.0" koa-static: "npm:^5.0.0" legally: "npm:^3.5.10" - meteor-node-stubs: "npm:^1.2.12" + meteor-node-stubs: "npm:^1.2.25" moment: "npm:^2.30.1" - nanoid: "npm:^3.3.8" - node-gyp: "npm:^9.4.1" + nanoid: "npm:^3.3.11" ntp-client: "npm:^0.5.3" object-path: "npm:^0.11.8" open-cli: "npm:^8.0.0" p-lazy: "npm:^3.1.0" - prettier: "npm:^3.4.2" - semver: "npm:^7.6.3" - standard-version: "npm:^9.5.0" + prettier: "npm:^3.8.1" + semver: "npm:^7.7.3" superfly-timeline: "npm:9.2.0" - threadedclass: "npm:^1.2.2" + threadedclass: "npm:^1.3.0" timecode: "npm:0.0.4" - ts-jest: "npm:^29.2.5" - type-fest: "npm:^4.33.0" + ts-jest: "npm:^29.4.6" + type-fest: "npm:^4.41.0" typescript: "npm:~5.7.3" underscore: "npm:^1.13.7" - winston: "npm:^3.17.0" + winston: "npm:^3.19.0" yargs: "npm:^17.7.2" languageName: unknown linkType: soft @@ -2370,90 +2557,90 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.7.8": - version: 1.13.3 - resolution: "axios@npm:1.13.3" +"axios@npm:^1.11.0": + version: 1.13.6 + resolution: "axios@npm:1.13.6" dependencies: - follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.4" + follow-redirects: "npm:^1.15.11" + form-data: "npm:^4.0.5" proxy-from-env: "npm:^1.1.0" - checksum: 10/2ceca9215671f9c2bcd5d8a0a1a667e9a35f9f7cfae88f25bba773ed9612de6cac50b2bf8be5e6918cbd2db601b4431ca87a00bffd9682939a8b85da9c89345a + checksum: 10/a7ed83c2af3ef21d64609df0f85e76893a915a864c5934df69241001d0578082d6521a0c730bf37518ee458821b5695957cb10db9fc705f2a8996c8686ea7a89 languageName: node linkType: hard -"babel-jest@npm:^29.7.0": - version: 29.7.0 - resolution: "babel-jest@npm:29.7.0" +"babel-jest@npm:30.2.0, babel-jest@npm:^30.2.0": + version: 30.2.0 + resolution: "babel-jest@npm:30.2.0" dependencies: - "@jest/transform": "npm:^29.7.0" - "@types/babel__core": "npm:^7.1.14" - babel-plugin-istanbul: "npm:^6.1.1" - babel-preset-jest: "npm:^29.6.3" - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" + "@jest/transform": "npm:30.2.0" + "@types/babel__core": "npm:^7.20.5" + babel-plugin-istanbul: "npm:^7.0.1" + babel-preset-jest: "npm:30.2.0" + chalk: "npm:^4.1.2" + graceful-fs: "npm:^4.2.11" slash: "npm:^3.0.0" peerDependencies: - "@babel/core": ^7.8.0 - checksum: 10/8a0953bd813b3a8926008f7351611055548869e9a53dd36d6e7e96679001f71e65fd7dbfe253265c3ba6a4e630dc7c845cf3e78b17d758ef1880313ce8fba258 + "@babel/core": ^7.11.0 || ^8.0.0-0 + checksum: 10/4c7351a366cf8ac2b8a2e4e438867693eb9d83ed24c29c648da4576f700767aaf72a5d14337fc3f92c50b069f5025b26c7b89e3b7b867914b7cf8997fc15f095 languageName: node linkType: hard -"babel-plugin-istanbul@npm:^6.1.1": - version: 6.1.1 - resolution: "babel-plugin-istanbul@npm:6.1.1" +"babel-plugin-istanbul@npm:^7.0.1": + version: 7.0.1 + resolution: "babel-plugin-istanbul@npm:7.0.1" dependencies: "@babel/helper-plugin-utils": "npm:^7.0.0" "@istanbuljs/load-nyc-config": "npm:^1.0.0" - "@istanbuljs/schema": "npm:^0.1.2" - istanbul-lib-instrument: "npm:^5.0.4" + "@istanbuljs/schema": "npm:^0.1.3" + istanbul-lib-instrument: "npm:^6.0.2" test-exclude: "npm:^6.0.0" - checksum: 10/ffd436bb2a77bbe1942a33245d770506ab2262d9c1b3c1f1da7f0592f78ee7445a95bc2efafe619dd9c1b6ee52c10033d6c7d29ddefe6f5383568e60f31dfe8d + checksum: 10/fe9f865f975aaa7a033de9ccb2b63fdcca7817266c5e98d3e02ac7ffd774c695093d215302796cb3770a71ef4574e7a9b298504c3c0c104cf4b48c8eda67b2a6 languageName: node linkType: hard -"babel-plugin-jest-hoist@npm:^29.6.3": - version: 29.6.3 - resolution: "babel-plugin-jest-hoist@npm:29.6.3" +"babel-plugin-jest-hoist@npm:30.2.0": + version: 30.2.0 + resolution: "babel-plugin-jest-hoist@npm:30.2.0" dependencies: - "@babel/template": "npm:^7.3.3" - "@babel/types": "npm:^7.3.3" - "@types/babel__core": "npm:^7.1.14" - "@types/babel__traverse": "npm:^7.0.6" - checksum: 10/9bfa86ec4170bd805ab8ca5001ae50d8afcb30554d236ba4a7ffc156c1a92452e220e4acbd98daefc12bf0216fccd092d0a2efed49e7e384ec59e0597a926d65 + "@types/babel__core": "npm:^7.20.5" + checksum: 10/360e87a9aa35f4cf208a10ba79e1821ea906f9e3399db2a9762cbc5076fd59f808e571d88b5b1106738d22e23f9ddefbb8137b2780b2abd401c8573b85c8a2f5 languageName: node linkType: hard -"babel-preset-current-node-syntax@npm:^1.0.0": - version: 1.0.1 - resolution: "babel-preset-current-node-syntax@npm:1.0.1" +"babel-preset-current-node-syntax@npm:^1.2.0": + version: 1.2.0 + resolution: "babel-preset-current-node-syntax@npm:1.2.0" dependencies: "@babel/plugin-syntax-async-generators": "npm:^7.8.4" "@babel/plugin-syntax-bigint": "npm:^7.8.3" - "@babel/plugin-syntax-class-properties": "npm:^7.8.3" - "@babel/plugin-syntax-import-meta": "npm:^7.8.3" + "@babel/plugin-syntax-class-properties": "npm:^7.12.13" + "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" + "@babel/plugin-syntax-import-attributes": "npm:^7.24.7" + "@babel/plugin-syntax-import-meta": "npm:^7.10.4" "@babel/plugin-syntax-json-strings": "npm:^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" - "@babel/plugin-syntax-numeric-separator": "npm:^7.8.3" + "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" - "@babel/plugin-syntax-top-level-await": "npm:^7.8.3" + "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" + "@babel/plugin-syntax-top-level-await": "npm:^7.14.5" peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10/94561959cb12bfa80867c9eeeace7c3d48d61707d33e55b4c3fdbe82fc745913eb2dbfafca62aef297421b38aadcb58550e5943f50fbcebbeefd70ce2bed4b74 + "@babel/core": ^7.0.0 || ^8.0.0-0 + checksum: 10/3608fa671cfa46364ea6ec704b8fcdd7514b7b70e6ec09b1199e13ae73ed346c51d5ce2cb6d4d5b295f6a3f2cad1fdeec2308aa9e037002dd7c929194cc838ea languageName: node linkType: hard -"babel-preset-jest@npm:^29.6.3": - version: 29.6.3 - resolution: "babel-preset-jest@npm:29.6.3" +"babel-preset-jest@npm:30.2.0": + version: 30.2.0 + resolution: "babel-preset-jest@npm:30.2.0" dependencies: - babel-plugin-jest-hoist: "npm:^29.6.3" - babel-preset-current-node-syntax: "npm:^1.0.0" + babel-plugin-jest-hoist: "npm:30.2.0" + babel-preset-current-node-syntax: "npm:^1.2.0" peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10/aa4ff2a8a728d9d698ed521e3461a109a1e66202b13d3494e41eea30729a5e7cc03b3a2d56c594423a135429c37bf63a9fa8b0b9ce275298be3095a88c69f6fb + "@babel/core": ^7.11.0 || ^8.0.0-beta.1 + checksum: 10/f75e155a8cf63ea1c5ca942bf757b934427630a1eeafdf861e9117879b3367931fc521da3c41fd52f8d59d705d1093ffb46c9474b3fd4d765d194bea5659d7d9 languageName: node linkType: hard @@ -2464,6 +2651,13 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 + languageName: node + linkType: hard + "base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -2480,13 +2674,14 @@ __metadata: languageName: node linkType: hard -"bcrypt@npm:^5.1.1": - version: 5.1.1 - resolution: "bcrypt@npm:5.1.1" +"bcrypt@npm:^6.0.0": + version: 6.0.0 + resolution: "bcrypt@npm:6.0.0" dependencies: - "@mapbox/node-pre-gyp": "npm:^1.0.11" - node-addon-api: "npm:^5.0.0" - checksum: 10/be6af3a93d90a0071c3b4412e8b82e2f319e26cb4e6cb14a1790cfe7c164792fa8add3ac9f30278a017d7d332ee8852601ce81a69737e9bfb9f10c878dd3d0dd + node-addon-api: "npm:^8.3.0" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.8.4" + checksum: 10/24dc552828435f2346fe0a27eb2b23e4fdcc4f139d069db0dbee6e3b37fcf8e88ffbd6473a138e1d594a4b9df91e9b71994d15cf9fc6f5c3ff68f3d851fd973a languageName: node linkType: hard @@ -2523,20 +2718,20 @@ __metadata: linkType: hard "bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9": - version: 4.12.0 - resolution: "bn.js@npm:4.12.0" - checksum: 10/10f8db196d3da5adfc3207d35d0a42aa29033eb33685f20ba2c36cadfe2de63dad05df0a20ab5aae01b418d1c4b3d4d205273085262fa020d17e93ff32b67527 + version: 4.12.3 + resolution: "bn.js@npm:4.12.3" + checksum: 10/57ed5a055f946f3e009f1589c45a5242db07f3dddfc72e4506f0dd9d8b145f0dbee4edabc2499288f3fc338eb712fb96a1c623a2ed2bcd49781df1a64db64dd1 languageName: node linkType: hard "bn.js@npm:^5.2.1": - version: 5.2.1 - resolution: "bn.js@npm:5.2.1" - checksum: 10/7a7e8764d7a6e9708b8b9841b2b3d6019cc154d2fc23716d0efecfe1e16921b7533c6f7361fb05471eab47986c4aa310c270f88e3507172104632ac8df2cfd84 + version: 5.2.3 + resolution: "bn.js@npm:5.2.3" + checksum: 10/dfb3927e0d531e6ec4f191597ce6f7f7665310c356fef5f968ada676b8058027f959af42eaa37b5f5c63617e819d3741813025ab15dd71a90f2e74698df0b58e languageName: node linkType: hard -"body-parser@npm:^1.20.3": +"body-parser@npm:^1.20.4": version: 1.20.4 resolution: "body-parser@npm:1.20.4" dependencies: @@ -2566,7 +2761,7 @@ __metadata: languageName: node linkType: hard -"brace-expansion@npm:^2.0.1": +"brace-expansion@npm:^2.0.2": version: 2.0.2 resolution: "brace-expansion@npm:2.0.2" dependencies: @@ -2575,6 +2770,15 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^5.0.2": + version: 5.0.4 + resolution: "brace-expansion@npm:5.0.4" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10/cfd57e20d8ded9578149e47ae4d3fff2b2f78d06b54a32a73057bddff65c8e9b930613f0cbcfefedf12dd117151e19d4da16367d5127c54f3bff02d8a4479bb2 + languageName: node + linkType: hard + "braces@npm:^3.0.3": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -2648,24 +2852,6 @@ __metadata: languageName: node linkType: hard -"browserify-sign@npm:^4.2.3": - version: 4.2.3 - resolution: "browserify-sign@npm:4.2.3" - dependencies: - bn.js: "npm:^5.2.1" - browserify-rsa: "npm:^4.1.0" - create-hash: "npm:^1.2.0" - create-hmac: "npm:^1.1.7" - elliptic: "npm:^6.5.5" - hash-base: "npm:~3.0" - inherits: "npm:^2.0.4" - parse-asn1: "npm:^5.1.7" - readable-stream: "npm:^2.3.8" - safe-buffer: "npm:^5.2.1" - checksum: 10/403a8061d229ae31266670345b4a7c00051266761d2c9bbeb68b1a9bcb05f68143b16110cf23a171a5d6716396a1f41296282b3e73eeec0a1871c77f0ff4ee6b - languageName: node - linkType: hard - "browserify-zlib@npm:^0.2.0": version: 0.2.0 resolution: "browserify-zlib@npm:0.2.0" @@ -2707,10 +2893,10 @@ __metadata: languageName: node linkType: hard -"bson@npm:^6.10.1": - version: 6.10.2 - resolution: "bson@npm:6.10.2" - checksum: 10/c729cf609bf96ee3ab8edbd1c5117bfc2f7ea33eb45a49aeeda8144a9d5616bfee6ad78d4b591757151acddaedcf11dc82c0ad6c0712270221cf340da4006962 +"bson@npm:^6.10.4": + version: 6.10.4 + resolution: "bson@npm:6.10.4" + checksum: 10/8a79a452219a13898358a5abc93e32bc3805236334f962661da121ce15bd5cade27718ba3310ee2a143ff508489b08467eed172ecb2a658cb8d2e94fdb76b215 languageName: node linkType: hard @@ -2778,49 +2964,22 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^16.1.0": - version: 16.1.3 - resolution: "cacache@npm:16.1.3" - dependencies: - "@npmcli/fs": "npm:^2.1.0" - "@npmcli/move-file": "npm:^2.0.0" - chownr: "npm:^2.0.0" - fs-minipass: "npm:^2.1.0" - glob: "npm:^8.0.1" - infer-owner: "npm:^1.0.4" - lru-cache: "npm:^7.7.1" - minipass: "npm:^3.1.6" - minipass-collect: "npm:^1.0.2" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - mkdirp: "npm:^1.0.4" - p-map: "npm:^4.0.0" - promise-inflight: "npm:^1.0.1" - rimraf: "npm:^3.0.2" - ssri: "npm:^9.0.0" - tar: "npm:^6.1.11" - unique-filename: "npm:^2.0.0" - checksum: 10/a14524d90e377ee691d63a81173b33c473f8bc66eb299c64290b58e1d41b28842397f8d6c15a01b4c57ca340afcec019ae112a45c2f67a79f76130d326472e92 - languageName: node - linkType: hard - -"cacache@npm:^17.0.0": - version: 17.1.4 - resolution: "cacache@npm:17.1.4" +"cacache@npm:^20.0.1": + version: 20.0.3 + resolution: "cacache@npm:20.0.3" dependencies: - "@npmcli/fs": "npm:^3.1.0" + "@npmcli/fs": "npm:^5.0.0" fs-minipass: "npm:^3.0.0" - glob: "npm:^10.2.2" - lru-cache: "npm:^7.7.1" + glob: "npm:^13.0.0" + lru-cache: "npm:^11.1.0" minipass: "npm:^7.0.3" - minipass-collect: "npm:^1.0.2" + minipass-collect: "npm:^2.0.1" minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" - p-map: "npm:^4.0.0" - ssri: "npm:^10.0.0" - tar: "npm:^6.1.11" - unique-filename: "npm:^3.0.0" - checksum: 10/6e26c788bc6a18ff42f4d4f97db30d5c60a5dfac8e7c10a03b0307a92cf1b647570547cf3cd96463976c051eb9c7258629863f156e224c82018862c1a8ad0e70 + p-map: "npm:^7.0.2" + ssri: "npm:^13.0.0" + unique-filename: "npm:^5.0.0" + checksum: 10/388a0169970df9d051da30437f93f81b7e91efb570ad0ff2b8fde33279fbe726c1bc8e8e2b9c05053ffb4f563854c73db395e8712e3b62347a1bc4f7fb8899ff languageName: node linkType: hard @@ -2834,7 +2993,7 @@ __metadata: languageName: node linkType: hard -"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": +"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.2": version: 1.0.2 resolution: "call-bind-apply-helpers@npm:1.0.2" dependencies: @@ -2844,6 +3003,16 @@ __metadata: languageName: node linkType: hard +"call-bind-apply-helpers@npm:^1.0.1": + version: 1.0.1 + resolution: "call-bind-apply-helpers@npm:1.0.1" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10/6e30c621170e45f1fd6735e84d02ee8e02a3ab95cb109499d5308cbe5d1e84d0cd0e10b48cc43c76aa61450ae1b03a7f89c37c10fc0de8d4998b42aab0f268cc + languageName: node + linkType: hard + "call-bind@npm:^1.0.0, call-bind@npm:^1.0.2, call-bind@npm:^1.0.8": version: 1.0.8 resolution: "call-bind@npm:1.0.8" @@ -2891,7 +3060,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.2.0": +"camelcase@npm:^6.3.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 10/8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d @@ -2916,7 +3085,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, chalk@npm:^4.0.2, chalk@npm:^4.1.0": +"chalk@npm:^4.0.0, chalk@npm:^4.1.0, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -2933,17 +3102,24 @@ __metadata: languageName: node linkType: hard -"chownr@npm:^2.0.0": - version: 2.0.0 - resolution: "chownr@npm:2.0.0" - checksum: 10/c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f +"chownr@npm:^3.0.0": + version: 3.0.0 + resolution: "chownr@npm:3.0.0" + checksum: 10/b63cb1f73d171d140a2ed8154ee6566c8ab775d3196b0e03a2a94b5f6a0ce7777ee5685ca56849403c8d17bd457a6540672f9a60696a6137c7a409097495b82c + languageName: node + linkType: hard + +"chrono-node@npm:^2.9.0": + version: 2.9.0 + resolution: "chrono-node@npm:2.9.0" + checksum: 10/a30bbaa67f9a127e711db6e694ee4c89292d8f533dbfdc3d7cb34f479728e02e377f682e75ad84dd4b6a16016c248a5e85fb453943b96f93f5993f5ccddc6d08 languageName: node linkType: hard -"ci-info@npm:^3.2.0": - version: 3.8.0 - resolution: "ci-info@npm:3.8.0" - checksum: 10/b00e9313c1f7042ca8b1297c157c920d6d69f0fbad7b867910235676df228c4b4f4df33d06cacae37f9efba7a160b0a167c6be85492b419ef71d85660e60606b +"ci-info@npm:^4.2.0": + version: 4.4.0 + resolution: "ci-info@npm:4.4.0" + checksum: 10/dfded0c630267d89660c8abb988ac8395a382bdfefedcc03e3e2858523312c5207db777c239c34774e3fcff11f015477c19d2ac8a58ea58aa476614a2e64f434 languageName: node linkType: hard @@ -2958,17 +3134,17 @@ __metadata: languageName: node linkType: hard -"cjs-module-lexer@npm:^1.0.0, cjs-module-lexer@npm:^1.2.2": +"cjs-module-lexer@npm:^1.2.2": version: 1.2.3 resolution: "cjs-module-lexer@npm:1.2.3" checksum: 10/f96a5118b0a012627a2b1c13bd2fcb92509778422aaa825c5da72300d6dcadfb47134dd2e9d97dfa31acd674891dd91642742772d19a09a8adc3e56bd2f5928c languageName: node linkType: hard -"clean-stack@npm:^2.0.0": +"cjs-module-lexer@npm:^2.1.0": version: 2.2.0 - resolution: "clean-stack@npm:2.2.0" - checksum: 10/2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 + resolution: "cjs-module-lexer@npm:2.2.0" + checksum: 10/fc8eb5c1919504366d8260a150d93c4e857740e770467dc59ca0cc34de4b66c93075559a5af65618f359187866b1be40e036f4e1a1bab2f1e06001c216415f74 languageName: node linkType: hard @@ -3038,14 +3214,14 @@ __metadata: languageName: node linkType: hard -"collect-v8-coverage@npm:^1.0.0": - version: 1.0.2 - resolution: "collect-v8-coverage@npm:1.0.2" - checksum: 10/30ea7d5c9ee51f2fdba4901d4186c5b7114a088ef98fd53eda3979da77eed96758a2cae81cc6d97e239aaea6065868cf908b24980663f7b7e96aa291b3e12fa4 +"collect-v8-coverage@npm:^1.0.2": + version: 1.0.3 + resolution: "collect-v8-coverage@npm:1.0.3" + checksum: 10/656443261fb7b79cf79e89cba4b55622b07c1d4976c630829d7c5c585c73cda1c2ff101f316bfb19bb9e2c58d724c7db1f70a21e213dcd14099227c5e6019860 languageName: node linkType: hard -"color-convert@npm:^1.9.0, color-convert@npm:^1.9.3": +"color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" dependencies: @@ -3063,6 +3239,15 @@ __metadata: languageName: node linkType: hard +"color-convert@npm:^3.1.3": + version: 3.1.3 + resolution: "color-convert@npm:3.1.3" + dependencies: + color-name: "npm:^2.0.0" + checksum: 10/36b9b99c138f90eb11a28d1ad911054a9facd6cffde4f00dc49a34ebde7cae28454b2285ede64f273b6a8df9c3228b80e4352f4471978fa8b5005fe91341a67b + languageName: node + linkType: hard + "color-name@npm:1.1.3": version: 1.1.3 resolution: "color-name@npm:1.1.3" @@ -3070,49 +3255,36 @@ __metadata: languageName: node linkType: hard -"color-name@npm:^1.0.0, color-name@npm:~1.1.4": - version: 1.1.4 - resolution: "color-name@npm:1.1.4" - checksum: 10/b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 - languageName: node - linkType: hard - -"color-string@npm:^1.6.0": - version: 1.9.1 - resolution: "color-string@npm:1.9.1" - dependencies: - color-name: "npm:^1.0.0" - simple-swizzle: "npm:^0.2.2" - checksum: 10/72aa0b81ee71b3f4fb1ac9cd839cdbd7a011a7d318ef58e6cb13b3708dca75c7e45029697260488709f1b1c7ac4e35489a87e528156c1e365917d1c4ccb9b9cd +"color-name@npm:^2.0.0": + version: 2.1.0 + resolution: "color-name@npm:2.1.0" + checksum: 10/eb014f71d87408e318e95d3f554f188370d354ba8e0ffa4341d0fd19de391bfe2bc96e563d4f6614644d676bc24f475560dffee3fe310c2d6865d007410a9a2b languageName: node linkType: hard -"color-support@npm:^1.1.2, color-support@npm:^1.1.3": - version: 1.1.3 - resolution: "color-support@npm:1.1.3" - bin: - color-support: bin.js - checksum: 10/4bcfe30eea1498fe1cabc852bbda6c9770f230ea0e4faf4611c5858b1b9e4dde3730ac485e65f54ca182f4c50b626c1bea7c8441ceda47367a54a818c248aa7a +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: 10/b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 languageName: node linkType: hard -"color@npm:^3.1.3": - version: 3.2.1 - resolution: "color@npm:3.2.1" +"color-string@npm:^2.1.3": + version: 2.1.4 + resolution: "color-string@npm:2.1.4" dependencies: - color-convert: "npm:^1.9.3" - color-string: "npm:^1.6.0" - checksum: 10/bf70438e0192f4f62f4bfbb303e7231289e8cc0d15ff6b6cbdb722d51f680049f38d4fdfc057a99cb641895cf5e350478c61d98586400b060043afc44285e7ae + color-name: "npm:^2.0.0" + checksum: 10/689a8688ac3cd55247792c83a9db9bfe675343c7412fedba1eb748ac6a8867dd2bb3d406e309ebfe90336809ee5067c7f2cccfbd10133c5cc9ef1dba5aad58f2 languageName: node linkType: hard -"colorspace@npm:1.1.x": - version: 1.1.4 - resolution: "colorspace@npm:1.1.4" +"color@npm:^5.0.2": + version: 5.0.3 + resolution: "color@npm:5.0.3" dependencies: - color: "npm:^3.1.3" - text-hex: "npm:1.0.x" - checksum: 10/bb3934ef3c417e961e6d03d7ca60ea6e175947029bfadfcdb65109b01881a1c0ecf9c2b0b59abcd0ee4a0d7c1eae93beed01b0e65848936472270a0b341ebce8 + color-convert: "npm:^3.1.3" + color-string: "npm:^2.1.3" + checksum: 10/88063ee058b995e5738092b5aa58888666275d1e967333f3814ff4fa334ce9a9e71de78a16fb1838f17c80793ea87f4878c20192037662809fe14eab2d474fd9 languageName: node linkType: hard @@ -3139,6 +3311,31 @@ __metadata: languageName: node linkType: hard +"commit-and-tag-version@npm:^12.6.1": + version: 12.6.1 + resolution: "commit-and-tag-version@npm:12.6.1" + dependencies: + chalk: "npm:^2.4.2" + conventional-changelog: "npm:4.0.0" + conventional-changelog-config-spec: "npm:2.1.0" + conventional-changelog-conventionalcommits: "npm:6.1.0" + conventional-recommended-bump: "npm:7.0.1" + detect-indent: "npm:^6.1.0" + detect-newline: "npm:^3.1.0" + dotgitignore: "npm:^2.1.0" + fast-xml-parser: "npm:^5.2.5" + figures: "npm:^3.2.0" + find-up: "npm:^5.0.0" + git-semver-tags: "npm:^5.0.1" + semver: "npm:^7.7.2" + yaml: "npm:^2.6.0" + yargs: "npm:^17.7.2" + bin: + commit-and-tag-version: bin/cli.js + checksum: 10/a7fc46f4e9b50a9071986ea7743839af7a73609bcecbb73ba3a2de15dd2d89f10174a05e8e1af2ffa1a435acf11093589d338e73408def6705278a6f8d7c91b3 + languageName: node + linkType: hard + "compare-func@npm:^2.0.0": version: 2.0.0 resolution: "compare-func@npm:2.0.0" @@ -3175,13 +3372,6 @@ __metadata: languageName: node linkType: hard -"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0": - version: 1.1.0 - resolution: "console-control-strings@npm:1.1.0" - checksum: 10/27b5fa302bc8e9ae9e98c03c66d76ca289ad0c61ce2fe20ab288d288bee875d217512d2edb2363fc83165e88f1c405180cf3f5413a46e51b4fe1a004840c6cdb - languageName: node - linkType: hard - "console-log-level@npm:^1.4.1": version: 1.4.1 resolution: "console-log-level@npm:1.4.1" @@ -3205,38 +3395,40 @@ __metadata: languageName: node linkType: hard -"content-type@npm:^1.0.4, content-type@npm:~1.0.5": +"content-disposition@npm:~1.0.1": + version: 1.0.1 + resolution: "content-disposition@npm:1.0.1" + checksum: 10/0718d861dfec56f532fd9acd714f173782ce5257b243344fecab5196621746cf8623bf1c833441612f1ac84559c546b59277cf0e91c3a646b0712a806decb1c8 + languageName: node + linkType: hard + +"content-type@npm:^1.0.4, content-type@npm:^1.0.5, content-type@npm:~1.0.5": version: 1.0.5 resolution: "content-type@npm:1.0.5" checksum: 10/585847d98dc7fb8035c02ae2cb76c7a9bd7b25f84c447e5ed55c45c2175e83617c8813871b4ee22f368126af6b2b167df655829007b21aa10302873ea9c62662 languageName: node linkType: hard -"conventional-changelog-angular@npm:^5.0.12": - version: 5.0.13 - resolution: "conventional-changelog-angular@npm:5.0.13" +"conventional-changelog-angular@npm:^6.0.0": + version: 6.0.0 + resolution: "conventional-changelog-angular@npm:6.0.0" dependencies: compare-func: "npm:^2.0.0" - q: "npm:^1.5.1" - checksum: 10/e7ee31ac703bc139552a735185f330d1b2e53d7c1ff40a78bf43339e563d95c290a4f57e68b76bb223345524702d80bf18dc955417cd0852d9457595c04ad8ce + checksum: 10/ddc59ead53a45b817d83208200967f5340866782b8362d5e2e34105fdfa3d3a31585ebbdec7750bdb9de53da869f847e8ca96634a9801f51e27ecf4e7ffe2bad languageName: node linkType: hard -"conventional-changelog-atom@npm:^2.0.8": - version: 2.0.8 - resolution: "conventional-changelog-atom@npm:2.0.8" - dependencies: - q: "npm:^1.5.1" - checksum: 10/53ae65ef33913538085f4cdda4904384a7b17374342efc2f34ad697569cb2011b2327d744ef5750ea651d27bfd401a166f9b6b5c2dc8564b38346910593dfae0 +"conventional-changelog-atom@npm:^3.0.0": + version: 3.0.0 + resolution: "conventional-changelog-atom@npm:3.0.0" + checksum: 10/9b8c2667e4a263f5e49d7415d16acf5cb240787fe7b604b5a49ef9b63adabf01297dc1e72f0700b98423f99f47e665dccf37e914e12f432b7bf155d0973e054f languageName: node linkType: hard -"conventional-changelog-codemirror@npm:^2.0.8": - version: 2.0.8 - resolution: "conventional-changelog-codemirror@npm:2.0.8" - dependencies: - q: "npm:^1.5.1" - checksum: 10/45183dcb16fa19fe8bc6cc1affc34ea856150e826fe83579f52b5b934f83fe71df64094a8061ccdb2890b94c9dc01a97d04618c88fa6ee58a1ac7f82067cad11 +"conventional-changelog-codemirror@npm:^3.0.0": + version: 3.0.0 + resolution: "conventional-changelog-codemirror@npm:3.0.0" + checksum: 10/9d7ae60d8e4185502e89e27e46ae69253a4e26ce519fd8475f4695af1472f923711669d44ea2dcee3ce92f78c07ed6ac080eafb07f22b4010ab78139201aa0b0 languageName: node linkType: hard @@ -3247,171 +3439,152 @@ __metadata: languageName: node linkType: hard -"conventional-changelog-conventionalcommits@npm:4.6.3, conventional-changelog-conventionalcommits@npm:^4.5.0": - version: 4.6.3 - resolution: "conventional-changelog-conventionalcommits@npm:4.6.3" +"conventional-changelog-conventionalcommits@npm:6.1.0, conventional-changelog-conventionalcommits@npm:^6.0.0": + version: 6.1.0 + resolution: "conventional-changelog-conventionalcommits@npm:6.1.0" dependencies: compare-func: "npm:^2.0.0" - lodash: "npm:^4.17.15" - q: "npm:^1.5.1" - checksum: 10/70b9ba65a72d57d40aeea7e787cd200cd8350430ad959892a6cc2cb8b9c3874ba8e331d355c2565549c0a28881c114c5a8f1d4dab61fd8607f29d7e2174e181b + checksum: 10/7e5caef7d65b381a0b302534058acff83adc7a907094c85379ef138c35f2aa043cf8e7a3bef30f42078dcc4bff0e8bc763b179c007dd732d92856fae0607a4bc languageName: node linkType: hard -"conventional-changelog-core@npm:^4.2.1": - version: 4.2.4 - resolution: "conventional-changelog-core@npm:4.2.4" +"conventional-changelog-core@npm:^5.0.0": + version: 5.0.2 + resolution: "conventional-changelog-core@npm:5.0.2" dependencies: add-stream: "npm:^1.0.0" - conventional-changelog-writer: "npm:^5.0.0" - conventional-commits-parser: "npm:^3.2.0" - dateformat: "npm:^3.0.0" - get-pkg-repo: "npm:^4.0.0" - git-raw-commits: "npm:^2.0.8" + conventional-changelog-writer: "npm:^6.0.0" + conventional-commits-parser: "npm:^4.0.0" + dateformat: "npm:^3.0.3" + get-pkg-repo: "npm:^4.2.1" + git-raw-commits: "npm:^3.0.0" git-remote-origin-url: "npm:^2.0.0" - git-semver-tags: "npm:^4.1.1" - lodash: "npm:^4.17.15" - normalize-package-data: "npm:^3.0.0" - q: "npm:^1.5.1" + git-semver-tags: "npm:^5.0.0" + normalize-package-data: "npm:^3.0.3" read-pkg: "npm:^3.0.0" read-pkg-up: "npm:^3.0.0" - through2: "npm:^4.0.0" - checksum: 10/c8104986724ec384baa559425485bd7834bb94a12e5d52b71b4829eddf664895be4c6269504a83788179959e60e40ba2fcbdb474cc70606ba7ce06b61e016726 + checksum: 10/eceb8ddbe226768dad326f5cea3b4281b073e51e1af70591280e476c0883f61bc816a5ef4b53debeadf9d98acff1a07aaa3cc0cfff0dfb5b7c56d4f786029d51 languageName: node linkType: hard -"conventional-changelog-ember@npm:^2.0.9": - version: 2.0.9 - resolution: "conventional-changelog-ember@npm:2.0.9" - dependencies: - q: "npm:^1.5.1" - checksum: 10/87faf4223079a8089c8377fc77a01a567c6f58b46e9699143cc3125301ae520a69cd132a847d26b218871e7a0e074303764ee2da03d019c691f498a0abcfd32c +"conventional-changelog-ember@npm:^3.0.0": + version: 3.0.0 + resolution: "conventional-changelog-ember@npm:3.0.0" + checksum: 10/7c32f6dd0c560cfc148767c1b8e3eee7e6498b279e7d4fab8c0f254839a8f2d32a5b43f5fdc32cc869f4f20544daea9c7852a0795a2ef3bc059d5293044510ce languageName: node linkType: hard -"conventional-changelog-eslint@npm:^3.0.9": - version: 3.0.9 - resolution: "conventional-changelog-eslint@npm:3.0.9" - dependencies: - q: "npm:^1.5.1" - checksum: 10/f12f82adaeb6353fa04ab7ff4c245373edefdead215b901ac7c15b51dc6c3fb00ea8fbbaa1a393803aba9d3bdf89fd5125167850ccc3f42260f403e6b2f0cde8 +"conventional-changelog-eslint@npm:^4.0.0": + version: 4.0.0 + resolution: "conventional-changelog-eslint@npm:4.0.0" + checksum: 10/98fa5da097196f5b66456a06cc27395b7d41faeb83789f9a3c31a75b27349f586ce74c8d6fb41377764d7c9ff931ee3ce20e74f8e4ce15dbec436ba769a1639b languageName: node linkType: hard -"conventional-changelog-express@npm:^2.0.6": - version: 2.0.6 - resolution: "conventional-changelog-express@npm:2.0.6" - dependencies: - q: "npm:^1.5.1" - checksum: 10/08db048159e9bd140a4c607c17023d37ab29aeb5f31bd62388cb8e7c647e39c6e44d181e1cfb8ef7c36ea0ec240aa9a1bf0e8400c872ae654a0d8d1f4e8caccb +"conventional-changelog-express@npm:^3.0.0": + version: 3.0.0 + resolution: "conventional-changelog-express@npm:3.0.0" + checksum: 10/bac94ebd8d759db78eb3d410061a57c4007c0f1dcafad174bff6b38ca8a2de16ac368bc149eafc6ef7eba408e9b04731605a23458c634ae70b3f0bdc5635cf1e languageName: node linkType: hard -"conventional-changelog-jquery@npm:^3.0.11": - version: 3.0.11 - resolution: "conventional-changelog-jquery@npm:3.0.11" - dependencies: - q: "npm:^1.5.1" - checksum: 10/18720ee26785aa0e31b0098b0b85779f4e7410d6eb3c7a7cfb0ea5c5125b970e11ac18a2d5b414806286fc389047c8592d792cbe47ed17a49e4661bd9aac1c74 +"conventional-changelog-jquery@npm:^4.0.0": + version: 4.0.0 + resolution: "conventional-changelog-jquery@npm:4.0.0" + checksum: 10/141b6c7b147bb5e706d0fa784817b02d04ed3a5cfc0cb0c99ccea41d890266e239ff8a32c05e128258bb415ceec3305cf16ff2f5baf12049d1dee58b18915bc4 languageName: node linkType: hard -"conventional-changelog-jshint@npm:^2.0.9": - version: 2.0.9 - resolution: "conventional-changelog-jshint@npm:2.0.9" +"conventional-changelog-jshint@npm:^3.0.0": + version: 3.0.0 + resolution: "conventional-changelog-jshint@npm:3.0.0" dependencies: compare-func: "npm:^2.0.0" - q: "npm:^1.5.1" - checksum: 10/42e16d0e41464619c68eefa00efdb9787a2be4923c33a1d607e5e281c3326491cc3674a67191ba8bd3cbdbe2a820de532622a8c6c9a10eae1639c48da458ab01 + checksum: 10/4ee044c9cf6c960f40dfd8b80b67ef5989d0b9489a4b94ba711ca35d82a98095882c138f2f823de0d444409d71c8b65b12025c79e23ac946b97e344b57476bf9 languageName: node linkType: hard -"conventional-changelog-preset-loader@npm:^2.3.4": - version: 2.3.4 - resolution: "conventional-changelog-preset-loader@npm:2.3.4" - checksum: 10/23a889b7fcf6fe7653e61f32a048877b2f954dcc1e0daa2848c5422eb908e6f24c78372f8d0d2130b5ed941c02e7010c599dccf44b8552602c6c8db9cb227453 +"conventional-changelog-preset-loader@npm:^3.0.0": + version: 3.0.0 + resolution: "conventional-changelog-preset-loader@npm:3.0.0" + checksum: 10/199c4730c5151f243d35c24585114900c2a7091eab5832cfeb49067a18a2b77d5c9a86b779e6e18b49278a1ff83c011c1d9bb6da95bd1f78d9e36d4d379216d5 languageName: node linkType: hard -"conventional-changelog-writer@npm:^5.0.0": - version: 5.0.1 - resolution: "conventional-changelog-writer@npm:5.0.1" +"conventional-changelog-writer@npm:^6.0.0": + version: 6.0.1 + resolution: "conventional-changelog-writer@npm:6.0.1" dependencies: - conventional-commits-filter: "npm:^2.0.7" - dateformat: "npm:^3.0.0" + conventional-commits-filter: "npm:^3.0.0" + dateformat: "npm:^3.0.3" handlebars: "npm:^4.7.7" json-stringify-safe: "npm:^5.0.1" - lodash: "npm:^4.17.15" - meow: "npm:^8.0.0" - semver: "npm:^6.0.0" - split: "npm:^1.0.0" - through2: "npm:^4.0.0" + meow: "npm:^8.1.2" + semver: "npm:^7.0.0" + split: "npm:^1.0.1" bin: conventional-changelog-writer: cli.js - checksum: 10/09703c3fcea24753ac79dd408fad391f64b7e48c6b3813d0429e6ed25b72aec5235400cf9f182400520ad193598983a81345ad817ca9c37ae289ef70975ae0c6 + checksum: 10/9649d390b91c0621b17ccd7faf046990385da46c53004fcc3f13e5887ece26d134316d466de8c21d0c90672c1fca2b7ec98f28603ee04df8cfe5bcfc1fb70e76 languageName: node linkType: hard -"conventional-changelog@npm:3.1.25": - version: 3.1.25 - resolution: "conventional-changelog@npm:3.1.25" +"conventional-changelog@npm:4.0.0": + version: 4.0.0 + resolution: "conventional-changelog@npm:4.0.0" dependencies: - conventional-changelog-angular: "npm:^5.0.12" - conventional-changelog-atom: "npm:^2.0.8" - conventional-changelog-codemirror: "npm:^2.0.8" - conventional-changelog-conventionalcommits: "npm:^4.5.0" - conventional-changelog-core: "npm:^4.2.1" - conventional-changelog-ember: "npm:^2.0.9" - conventional-changelog-eslint: "npm:^3.0.9" - conventional-changelog-express: "npm:^2.0.6" - conventional-changelog-jquery: "npm:^3.0.11" - conventional-changelog-jshint: "npm:^2.0.9" - conventional-changelog-preset-loader: "npm:^2.3.4" - checksum: 10/27f4651ec70d24ca45f8b12b88c81ac258ab0912044ea6dc701dd4119df326d9094919d032b2f4ab366f41aa70480d759398f910f6534975ace1989f7935b790 + conventional-changelog-angular: "npm:^6.0.0" + conventional-changelog-atom: "npm:^3.0.0" + conventional-changelog-codemirror: "npm:^3.0.0" + conventional-changelog-conventionalcommits: "npm:^6.0.0" + conventional-changelog-core: "npm:^5.0.0" + conventional-changelog-ember: "npm:^3.0.0" + conventional-changelog-eslint: "npm:^4.0.0" + conventional-changelog-express: "npm:^3.0.0" + conventional-changelog-jquery: "npm:^4.0.0" + conventional-changelog-jshint: "npm:^3.0.0" + conventional-changelog-preset-loader: "npm:^3.0.0" + checksum: 10/42c9297b2353950213d084903ce209a9d2e0c843510c5550952bf9cfb611edcc5a0a7ae2d6e6d408a3353716e428075198341f5ffea8e0328f244641df1dffc5 languageName: node linkType: hard -"conventional-commits-filter@npm:^2.0.7": - version: 2.0.7 - resolution: "conventional-commits-filter@npm:2.0.7" +"conventional-commits-filter@npm:^3.0.0": + version: 3.0.0 + resolution: "conventional-commits-filter@npm:3.0.0" dependencies: lodash.ismatch: "npm:^4.4.0" - modify-values: "npm:^1.0.0" - checksum: 10/c7e25df941047750324704ca61ea281cbc156d359a1bd8587dc5e9e94311fa8343d97be9f1115b2e3948624830093926992a2854ae1ac8cbc560e60e360fdd9b + modify-values: "npm:^1.0.1" + checksum: 10/73337f42acff7189e1dfca8d13c9448ce085ac1c09976cb33617cc909949621befb1640b1c6c30a1be4953a1be0deea9e93fa0dc86725b8be8e249a64fbb4632 languageName: node linkType: hard -"conventional-commits-parser@npm:^3.2.0": - version: 3.2.4 - resolution: "conventional-commits-parser@npm:3.2.4" +"conventional-commits-parser@npm:^4.0.0": + version: 4.0.0 + resolution: "conventional-commits-parser@npm:4.0.0" dependencies: - JSONStream: "npm:^1.0.4" + JSONStream: "npm:^1.3.5" is-text-path: "npm:^1.0.1" - lodash: "npm:^4.17.15" - meow: "npm:^8.0.0" - split2: "npm:^3.0.0" - through2: "npm:^4.0.0" + meow: "npm:^8.1.2" + split2: "npm:^3.2.2" bin: conventional-commits-parser: cli.js - checksum: 10/2f9d31bade60ae68c1296ae67e47099c547a9452e1670fc5bfa64b572cadc9f305797c88a855f064dd899cc4eb4f15dd5a860064cdd8c52085066538019fe2a5 + checksum: 10/d3b7d947b486d3bb40f961808947ee46487429e050be840030211a80aa2eec170e427207c830f2720d8ab898649a652bbbe1825993b8bf0596517e3603f5a1bd languageName: node linkType: hard -"conventional-recommended-bump@npm:6.1.0": - version: 6.1.0 - resolution: "conventional-recommended-bump@npm:6.1.0" +"conventional-recommended-bump@npm:7.0.1": + version: 7.0.1 + resolution: "conventional-recommended-bump@npm:7.0.1" dependencies: concat-stream: "npm:^2.0.0" - conventional-changelog-preset-loader: "npm:^2.3.4" - conventional-commits-filter: "npm:^2.0.7" - conventional-commits-parser: "npm:^3.2.0" - git-raw-commits: "npm:^2.0.8" - git-semver-tags: "npm:^4.1.1" - meow: "npm:^8.0.0" - q: "npm:^1.5.1" + conventional-changelog-preset-loader: "npm:^3.0.0" + conventional-commits-filter: "npm:^3.0.0" + conventional-commits-parser: "npm:^4.0.0" + git-raw-commits: "npm:^3.0.0" + git-semver-tags: "npm:^5.0.0" + meow: "npm:^8.1.2" bin: conventional-recommended-bump: cli.js - checksum: 10/5561a4163e097b502e5372420ae9eee240a2b0e00e8cca3f5d8a7110c35021a5fe61a18d457961ace815d58beecc0192ebd26da40c6affcfc038be2d3a5f77c4 + checksum: 10/8d815e7c6f8083085ce4c784b27b0799de628ad2671d99e23c4b08885fb04c5b2adcb6053898eb1f183ee26489273edcbb110c7cd9f80cb06153be53fef2b174 languageName: node linkType: hard @@ -3436,7 +3609,7 @@ __metadata: languageName: node linkType: hard -"cookies@npm:~0.9.0": +"cookies@npm:~0.9.0, cookies@npm:~0.9.1": version: 0.9.1 resolution: "cookies@npm:0.9.1" dependencies: @@ -3460,16 +3633,6 @@ __metadata: languageName: node linkType: hard -"create-ecdh@npm:^4.0.4": - version: 4.0.4 - resolution: "create-ecdh@npm:4.0.4" - dependencies: - bn.js: "npm:^4.1.0" - elliptic: "npm:^6.5.3" - checksum: 10/0dd7fca9711d09e152375b79acf1e3f306d1a25ba87b8ff14c2fd8e68b83aafe0a7dd6c4e540c9ffbdd227a5fa1ad9b81eca1f233c38bb47770597ba247e614b - languageName: node - linkType: hard - "create-hash@npm:^1.1.0, create-hash@npm:^1.2.0": version: 1.2.0 resolution: "create-hash@npm:1.2.0" @@ -3497,23 +3660,6 @@ __metadata: languageName: node linkType: hard -"create-jest@npm:^29.7.0": - version: 29.7.0 - resolution: "create-jest@npm:29.7.0" - dependencies: - "@jest/types": "npm:^29.6.3" - chalk: "npm:^4.0.0" - exit: "npm:^0.1.2" - graceful-fs: "npm:^4.2.9" - jest-config: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - prompts: "npm:^2.0.1" - bin: - create-jest: bin/create-jest.js - checksum: 10/847b4764451672b4174be4d5c6d7d63442ec3aa5f3de52af924e4d996d87d7801c18e125504f25232fc75840f6625b3ac85860fac6ce799b5efae7bdcaf4a2b7 - languageName: node - linkType: hard - "cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -3548,7 +3694,7 @@ __metadata: languageName: node linkType: hard -"dateformat@npm:^3.0.0": +"dateformat@npm:^3.0.3": version: 3.0.3 resolution: "dateformat@npm:3.0.3" checksum: 10/0504baf50c3777ad333c96c37d1673d67efcb7dd071563832f70b5cbf7f3f4753f18981d44bfd8f665d5e5a511d2fc0af8e0ead8b585b9b3ddaa90067864d3f0 @@ -3564,15 +3710,15 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4": - version: 4.4.0 - resolution: "debug@npm:4.4.0" +"debug@npm:4, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.3": + version: 4.4.3 + resolution: "debug@npm:4.4.3" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10/1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 + checksum: 10/9ada3434ea2993800bd9a1e320bd4aa7af69659fb51cca685d390949434bc0a8873c21ed7c9b852af6f2455a55c6d050aa3937d52b3c69f796dab666f762acad languageName: node linkType: hard @@ -3609,15 +3755,15 @@ __metadata: languageName: node linkType: hard -"dedent@npm:^1.0.0": - version: 1.5.1 - resolution: "dedent@npm:1.5.1" +"dedent@npm:^1.6.0": + version: 1.7.1 + resolution: "dedent@npm:1.7.1" peerDependencies: babel-plugin-macros: ^3.1.0 peerDependenciesMeta: babel-plugin-macros: optional: true - checksum: 10/fc00a8bc3dfb7c413a778dc40ee8151b6c6ff35159d641f36ecd839c1df5c6e0ec5f4992e658c82624a1a62aaecaffc23b9c965ceb0bbf4d698bfc16469ac27d + checksum: 10/78785ef592e37e0b1ca7a7a5964c8f3dee1abdff46c5bb49864168579c122328f6bb55c769bc7e005046a7381c3372d3859f0f78ab083950fa146e1c24873f4f languageName: node linkType: hard @@ -3642,7 +3788,7 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:^4.0.0, deepmerge@npm:^4.2.2, deepmerge@npm:^4.3.1": +"deepmerge@npm:^4.0.0, deepmerge@npm:^4.3.1": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 10/058d9e1b0ff1a154468bf3837aea436abcfea1ba1d165ddaaf48ca93765fdd01a30d33c36173da8fbbed951dd0a267602bc782fe288b0fc4b7e1e7091afc4529 @@ -3733,28 +3879,21 @@ __metadata: languageName: node linkType: hard -"destroy@npm:^1.0.4, destroy@npm:~1.2.0": +"destroy@npm:^1.0.4, destroy@npm:^1.2.0, destroy@npm:~1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" checksum: 10/0acb300b7478a08b92d810ab229d5afe0d2f4399272045ab22affa0d99dbaf12637659411530a6fcd597a9bdac718fc94373a61a95b4651bbc7b83684a565e38 languageName: node linkType: hard -"detect-indent@npm:^6.0.0": +"detect-indent@npm:^6.1.0": version: 6.1.0 resolution: "detect-indent@npm:6.1.0" checksum: 10/ab953a73c72dbd4e8fc68e4ed4bfd92c97eb6c43734af3900add963fd3a9316f3bc0578b018b24198d4c31a358571eff5f0656e81a1f3b9ad5c547d58b2d093d languageName: node linkType: hard -"detect-libc@npm:^2.0.0": - version: 2.0.2 - resolution: "detect-libc@npm:2.0.2" - checksum: 10/6118f30c0c425b1e56b9d2609f29bec50d35a6af0b762b6ad127271478f3bbfda7319ce869230cf1a351f2b219f39332cde290858553336d652c77b970f15de8 - languageName: node - linkType: hard - -"detect-newline@npm:^3.0.0, detect-newline@npm:^3.1.0": +"detect-newline@npm:^3.1.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" checksum: 10/ae6cd429c41ad01b164c59ea36f264a2c479598e61cba7c99da24175a7ab80ddf066420f2bec9a1c57a6bead411b4655ff15ad7d281c000a89791f48cbe939e7 @@ -3771,13 +3910,6 @@ __metadata: languageName: node linkType: hard -"diff-sequences@npm:^29.6.3": - version: 29.6.3 - resolution: "diff-sequences@npm:29.6.3" - checksum: 10/179daf9d2f9af5c57ad66d97cb902a538bcf8ed64963fa7aa0c329b3de3665ce2eb6ffdc2f69f29d445fa4af2517e5e55e5b6e00c00a9ae4f43645f97f7078cb - languageName: node - linkType: hard - "diffie-hellman@npm:^5.0.3": version: 5.0.3 resolution: "diffie-hellman@npm:5.0.3" @@ -3840,17 +3972,6 @@ __metadata: languageName: node linkType: hard -"ejs@npm:^3.1.10": - version: 3.1.10 - resolution: "ejs@npm:3.1.10" - dependencies: - jake: "npm:^10.8.5" - bin: - ejs: bin/cli.js - checksum: 10/a9cb7d7cd13b7b1cd0be5c4788e44dd10d92f7285d2f65b942f33e127230c054f99a42db4d99f766d8dbc6c57e94799593ee66a14efd7c8dd70c4812bf6aa384 - languageName: node - linkType: hard - "ejson@npm:^2.2.3": version: 2.2.3 resolution: "ejson@npm:2.2.3" @@ -3858,9 +3979,9 @@ __metadata: languageName: node linkType: hard -"elastic-apm-node@npm:^4.11.0": - version: 4.11.0 - resolution: "elastic-apm-node@npm:4.11.0" +"elastic-apm-node@npm:^4.15.0": + version: 4.15.0 + resolution: "elastic-apm-node@npm:4.15.0" dependencies: "@elastic/ecs-pino-format": "npm:^1.5.0" "@opentelemetry/api": "npm:^1.4.1" @@ -3880,7 +4001,7 @@ __metadata: fast-safe-stringify: "npm:^2.0.7" fast-stream-to-buffer: "npm:^1.0.0" http-headers: "npm:^3.0.2" - import-in-the-middle: "npm:1.12.0" + import-in-the-middle: "npm:1.14.4" json-bigint: "npm:^1.0.0" lru-cache: "npm:10.2.0" measured-reporting: "npm:^1.51.1" @@ -3892,36 +4013,21 @@ __metadata: pino: "npm:^8.15.0" readable-stream: "npm:^3.6.2" relative-microtime: "npm:^2.0.0" - require-in-the-middle: "npm:^7.1.1" + require-in-the-middle: "npm:^8.0.0" semver: "npm:^7.5.4" shallow-clone-shim: "npm:^2.0.0" source-map: "npm:^0.8.0-beta.0" - sql-summary: "npm:^1.0.1" - stream-chopper: "npm:^3.0.1" - unicode-byte-truncate: "npm:^1.0.0" - checksum: 10/b12aa4a4d4e89796727632b3f0b6399729e7151a63293e5e0e0bb9678f829059bce4e6ebe99bd8ef6300ea1f27ae451805b951f05a65bcccd7e9651dd3583502 - languageName: node - linkType: hard - -"electron-to-chromium@npm:^1.5.73": - version: 1.5.91 - resolution: "electron-to-chromium@npm:1.5.91" - checksum: 10/0b2785042abccecf2a2074bfc5a7b843707835327096809834a62ff94268771c55e549dec10decfca5148f6f5ef9f8ed83671b1910670ecb349d99ec8e8f8769 - languageName: node - linkType: hard - -"elliptic@npm:^6.5.3, elliptic@npm:^6.5.5, elliptic@npm:^6.6.0": - version: 6.6.1 - resolution: "elliptic@npm:6.6.1" - dependencies: - bn.js: "npm:^4.11.9" - brorand: "npm:^1.1.0" - hash.js: "npm:^1.0.0" - hmac-drbg: "npm:^1.0.1" - inherits: "npm:^2.0.4" - minimalistic-assert: "npm:^1.0.1" - minimalistic-crypto-utils: "npm:^1.0.1" - checksum: 10/dc678c9febd89a219c4008ba3a9abb82237be853d9fd171cd602c8fb5ec39927e65c6b5e7a1b2a4ea82ee8e0ded72275e7932bb2da04a5790c2638b818e4e1c5 + sql-summary: "npm:^1.0.1" + stream-chopper: "npm:^3.0.1" + unicode-byte-truncate: "npm:^1.0.0" + checksum: 10/6207a28ee1ab4b1d0459e2f545745377d6108a7d32587c51607f1f46bb6e08d051d67c73978ba2b089026bf22e6d1abcf44f8558a10013e6629577276688b199 + languageName: node + linkType: hard + +"electron-to-chromium@npm:^1.5.73": + version: 1.5.91 + resolution: "electron-to-chromium@npm:1.5.91" + checksum: 10/0b2785042abccecf2a2074bfc5a7b843707835327096809834a62ff94268771c55e549dec10decfca5148f6f5ef9f8ed83671b1910670ecb349d99ec8e8f8769 languageName: node linkType: hard @@ -3960,6 +4066,13 @@ __metadata: languageName: node linkType: hard +"encodeurl@npm:^2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: 10/abf5cd51b78082cf8af7be6785813c33b6df2068ce5191a40ca8b1afe6a86f9230af9a9ce694a5ce4665955e5c1120871826df9c128a642e09c58d592e2807fe + languageName: node + linkType: hard + "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -4284,7 +4397,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^9.18.0": +"eslint@npm:^9.39.2": version: 9.39.2 resolution: "eslint@npm:9.39.2" dependencies: @@ -4428,7 +4541,7 @@ __metadata: languageName: node linkType: hard -"execa@npm:^5.0.0": +"execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -4445,23 +4558,24 @@ __metadata: languageName: node linkType: hard -"exit@npm:^0.1.2": - version: 0.1.2 - resolution: "exit@npm:0.1.2" - checksum: 10/387555050c5b3c10e7a9e8df5f43194e95d7737c74532c409910e585d5554eaff34960c166643f5e23d042196529daad059c292dcf1fb61b8ca878d3677f4b87 +"exit-x@npm:^0.2.2": + version: 0.2.2 + resolution: "exit-x@npm:0.2.2" + checksum: 10/ee043053e6c1e237adf5ad9c4faf9f085b606f64a4ff859e2b138fab63fe642711d00c9af452a9134c4c92c55f752e818bfabab78c24d345022db163f3137027 languageName: node linkType: hard -"expect@npm:^29.0.0, expect@npm:^29.7.0": - version: 29.7.0 - resolution: "expect@npm:29.7.0" +"expect@npm:30.2.0, expect@npm:^30.0.0": + version: 30.2.0 + resolution: "expect@npm:30.2.0" dependencies: - "@jest/expect-utils": "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - jest-matcher-utils: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - checksum: 10/63f97bc51f56a491950fb525f9ad94f1916e8a014947f8d8445d3847a665b5471b768522d659f5e865db20b6c2033d2ac10f35fcbd881a4d26407a4f6f18451a + "@jest/expect-utils": "npm:30.2.0" + "@jest/get-type": "npm:30.1.0" + jest-matcher-utils: "npm:30.2.0" + jest-message-util: "npm:30.2.0" + jest-mock: "npm:30.2.0" + jest-util: "npm:30.2.0" + checksum: 10/cf98ab45ab2e9f2fb9943a3ae0097f72d63a94be179a19fd2818d8fdc3b7681d31cc8ef540606eb8dd967d9c44d73fef263a614e9de260c22943ffb122ad66fd languageName: node linkType: hard @@ -4550,6 +4664,27 @@ __metadata: languageName: node linkType: hard +"fast-xml-builder@npm:^1.0.0": + version: 1.1.1 + resolution: "fast-xml-builder@npm:1.1.1" + dependencies: + path-expression-matcher: "npm:^1.1.3" + checksum: 10/9e410f3d13d86ff398fdf712a71151c43c599a42d5cab64659172126b9f4f980135afc80c828026211fdfbcee525f2427d587af3190d0c8d03db5ba595bfade5 + languageName: node + linkType: hard + +"fast-xml-parser@npm:^5.2.5": + version: 5.4.2 + resolution: "fast-xml-parser@npm:5.4.2" + dependencies: + fast-xml-builder: "npm:^1.0.0" + strnum: "npm:^2.1.2" + bin: + fxparser: src/cli/cli.js + checksum: 10/12585d5dd77113411d01cf41818cfecbbaf8f3d9e8448b1c35f50a7eb51205408bc8db27af5733173a77f96f72d7e121d9e675674f71334569157c77845aba39 + languageName: node + linkType: hard + "fastq@npm:^1.13.0, fastq@npm:^1.6.0": version: 1.15.0 resolution: "fastq@npm:1.15.0" @@ -4559,7 +4694,7 @@ __metadata: languageName: node linkType: hard -"fb-watchman@npm:^2.0.0": +"fb-watchman@npm:^2.0.2": version: 2.0.2 resolution: "fb-watchman@npm:2.0.2" dependencies: @@ -4568,6 +4703,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.5.0": + version: 6.5.0 + resolution: "fdir@npm:6.5.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10/14ca1c9f0a0e8f4f2e9bf4e8551065a164a09545dae548c12a18d238b72e51e5a7b39bd8e5494b56463a0877672d0a6c1ef62c6fa0677db1b0c847773be939b1 + languageName: node + linkType: hard + "fecha@npm:^4.2.0": version: 4.2.3 resolution: "fecha@npm:4.2.3" @@ -4575,7 +4722,7 @@ __metadata: languageName: node linkType: hard -"figures@npm:^3.1.0": +"figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" dependencies: @@ -4604,15 +4751,6 @@ __metadata: languageName: node linkType: hard -"filelist@npm:^1.0.4": - version: 1.0.4 - resolution: "filelist@npm:1.0.4" - dependencies: - minimatch: "npm:^5.0.1" - checksum: 10/4b436fa944b1508b95cffdfc8176ae6947b92825483639ef1b9a89b27d82f3f8aa22b21eed471993f92709b431670d4e015b39c087d435a61e1bb04564cf51de - languageName: node - linkType: hard - "fill-range@npm:^7.1.1": version: 7.1.1 resolution: "fill-range@npm:7.1.1" @@ -4691,13 +4829,13 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.6": - version: 1.15.9 - resolution: "follow-redirects@npm:1.15.9" +"follow-redirects@npm:^1.15.11": + version: 1.15.11 + resolution: "follow-redirects@npm:1.15.11" peerDependenciesMeta: debug: optional: true - checksum: 10/e3ab42d1097e90d28b913903841e6779eb969b62a64706a3eb983e894a5db000fbd89296f45f08885a0e54cd558ef62e81be1165da9be25a6c44920da10f424c + checksum: 10/07372fd74b98c78cf4d417d68d41fdaa0be4dcacafffb9e67b1e3cf090bc4771515e65020651528faab238f10f9b9c0d9707d6c1574a6c0387c5de1042cde9ba languageName: node linkType: hard @@ -4710,7 +4848,7 @@ __metadata: languageName: node linkType: hard -"foreground-child@npm:^3.1.0, foreground-child@npm:^3.3.1": +"foreground-child@npm:^3.1.0": version: 3.3.1 resolution: "foreground-child@npm:3.3.1" dependencies: @@ -4720,21 +4858,7 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^2.5.0": - version: 2.5.5 - resolution: "form-data@npm:2.5.5" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.2" - mime-types: "npm:^2.1.35" - safe-buffer: "npm:^5.2.1" - checksum: 10/4b6a8d07bb67089da41048e734215f68317a8e29dd5385a972bf5c458a023313c69d3b5d6b8baafbb7f808fa9881e0e2e030ffe61e096b3ddc894c516401271d - languageName: node - linkType: hard - -"form-data@npm:^4.0.4": +"form-data@npm:^4.0.5": version: 4.0.5 resolution: "form-data@npm:4.0.5" dependencies: @@ -4761,15 +4885,6 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": - version: 2.1.0 - resolution: "fs-minipass@npm:2.1.0" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10/03191781e94bc9a54bd376d3146f90fe8e082627c502185dbf7b9b3032f66b0b142c1115f3b2cc5936575fc1b44845ce903dd4c21bec2a8d69f3bd56f9cee9ec - languageName: node - linkType: hard - "fs-minipass@npm:^3.0.0": version: 3.0.3 resolution: "fs-minipass@npm:3.0.3" @@ -4796,7 +4911,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2": +"fsevents@npm:^2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -4806,7 +4921,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A^2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -4841,39 +4956,6 @@ __metadata: languageName: node linkType: hard -"gauge@npm:^3.0.0": - version: 3.0.2 - resolution: "gauge@npm:3.0.2" - dependencies: - aproba: "npm:^1.0.3 || ^2.0.0" - color-support: "npm:^1.1.2" - console-control-strings: "npm:^1.0.0" - has-unicode: "npm:^2.0.1" - object-assign: "npm:^4.1.1" - signal-exit: "npm:^3.0.0" - string-width: "npm:^4.2.3" - strip-ansi: "npm:^6.0.1" - wide-align: "npm:^1.1.2" - checksum: 10/46df086451672a5fecd58f7ec86da74542c795f8e00153fbef2884286ce0e86653c3eb23be2d0abb0c4a82b9b2a9dec3b09b6a1cf31c28085fa0376599a26589 - languageName: node - linkType: hard - -"gauge@npm:^4.0.3": - version: 4.0.4 - resolution: "gauge@npm:4.0.4" - dependencies: - aproba: "npm:^1.0.3 || ^2.0.0" - color-support: "npm:^1.1.3" - console-control-strings: "npm:^1.1.0" - has-unicode: "npm:^2.0.1" - signal-exit: "npm:^3.0.7" - string-width: "npm:^4.2.3" - strip-ansi: "npm:^6.0.1" - wide-align: "npm:^1.1.5" - checksum: 10/09535dd53b5ced6a34482b1fa9f3929efdeac02f9858569cde73cef3ed95050e0f3d095706c1689614059898924b7a74aa14042f51381a1ccc4ee5c29d2389c4 - languageName: node - linkType: hard - "generator-function@npm:^2.0.0": version: 2.0.1 resolution: "generator-function@npm:2.0.1" @@ -4923,7 +5005,7 @@ __metadata: languageName: node linkType: hard -"get-pkg-repo@npm:^4.0.0": +"get-pkg-repo@npm:^4.2.1": version: 4.2.1 resolution: "get-pkg-repo@npm:4.2.1" dependencies: @@ -4992,18 +5074,16 @@ __metadata: languageName: node linkType: hard -"git-raw-commits@npm:^2.0.8": - version: 2.0.11 - resolution: "git-raw-commits@npm:2.0.11" +"git-raw-commits@npm:^3.0.0": + version: 3.0.0 + resolution: "git-raw-commits@npm:3.0.0" dependencies: dargs: "npm:^7.0.0" - lodash: "npm:^4.17.15" - meow: "npm:^8.0.0" - split2: "npm:^3.0.0" - through2: "npm:^4.0.0" + meow: "npm:^8.1.2" + split2: "npm:^3.2.2" bin: git-raw-commits: cli.js - checksum: 10/04e02b3da7c0e13a55f3e6fa8c1c5f06f7d0d641a9f90d896393ef0144bfcf91aa59beede68d14d61ed56aaf09f2c8dba175563c47ec000a8cf70f9df4877577 + checksum: 10/198892f307829d22fc8ec1c9b4a63876a1fde847763857bb74bd1b04c6f6bc0d7464340c25d0f34fd0fb395759363aa1f8ce324357027320d80523bf234676ab languageName: node linkType: hard @@ -5017,15 +5097,15 @@ __metadata: languageName: node linkType: hard -"git-semver-tags@npm:^4.0.0, git-semver-tags@npm:^4.1.1": - version: 4.1.1 - resolution: "git-semver-tags@npm:4.1.1" +"git-semver-tags@npm:^5.0.0, git-semver-tags@npm:^5.0.1": + version: 5.0.1 + resolution: "git-semver-tags@npm:5.0.1" dependencies: - meow: "npm:^8.0.0" - semver: "npm:^6.0.0" + meow: "npm:^8.1.2" + semver: "npm:^7.0.0" bin: git-semver-tags: cli.js - checksum: 10/ab2ad6c7c81aeb6e703f9c9dd1d590a4c546a86b036540780ca414eb6d327f582a9c2d164899ccf0c20e1e875ec4db13b1e665c12c9d5c802eee79d9c71fdd0f + checksum: 10/056e34a3dd0d91ca737225d360e46a0330c92f1508c38ad93965c3a204e5c7bfe7746f1f7e7d6b456bd61245c770fd0755148823bf852eed71099d094bee6cc2 languageName: node linkType: hard @@ -5072,7 +5152,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2": +"glob@npm:^10.3.10": version: 10.5.0 resolution: "glob@npm:10.5.0" dependencies: @@ -5088,23 +5168,18 @@ __metadata: languageName: node linkType: hard -"glob@npm:^11.0.1": - version: 11.1.0 - resolution: "glob@npm:11.1.0" +"glob@npm:^13.0.0, glob@npm:^13.0.1": + version: 13.0.1 + resolution: "glob@npm:13.0.1" dependencies: - foreground-child: "npm:^3.3.1" - jackspeak: "npm:^4.1.1" - minimatch: "npm:^10.1.1" + minimatch: "npm:^10.1.2" minipass: "npm:^7.1.2" - package-json-from-dist: "npm:^1.0.0" path-scurry: "npm:^2.0.0" - bin: - glob: dist/esm/bin.mjs - checksum: 10/da4501819633daff8822c007bb3f93d5c4d2cbc7b15a8e886660f4497dd251a1fb4f53a85fba1e760b31704eff7164aeb2c7a82db10f9f2c362d12c02fe52cf3 + checksum: 10/465e8cc269ab88d7415a3906cdc0f4543a2ae54df99207204af5bc28a944396d8d893822f546a8056a78ec714e608ab4f3502532c4d6b9cc5e113adf0fe5109e languageName: node linkType: hard -"glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4": +"glob@npm:^7.1.1, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -5118,26 +5193,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.1": - version: 8.1.0 - resolution: "glob@npm:8.1.0" - dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^5.0.1" - once: "npm:^1.3.0" - checksum: 10/9aab1c75eb087c35dbc41d1f742e51d0507aa2b14c910d96fb8287107a10a22f4bbdce26fc0a3da4c69a20f7b26d62f1640b346a4f6e6becfff47f335bb1dc5e - languageName: node - linkType: hard - -"globals@npm:^11.1.0": - version: 11.12.0 - resolution: "globals@npm:11.12.0" - checksum: 10/9f054fa38ff8de8fa356502eb9d2dae0c928217b8b5c8de1f09f5c9b6c8a96d8b9bd3afc49acbcd384a98a81fea713c859e1b09e214c60509517bb8fc2bc13c2 - languageName: node - linkType: hard - "globals@npm:^14.0.0": version: 14.0.0 resolution: "globals@npm:14.0.0" @@ -5168,7 +5223,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.8, graceful-fs@npm:^4.2.9": +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.2.10, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.8": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10/bf152d0ed1dc159239db1ba1f74fdbc40cb02f626770dcd5815c427ce0688c2635a06ed69af364396da4636d0408fcf7d4afdf7881724c3307e46aff30ca49e2 @@ -5191,7 +5246,7 @@ __metadata: languageName: node linkType: hard -"handlebars@npm:^4.7.7": +"handlebars@npm:^4.7.7, handlebars@npm:^4.7.8": version: 4.7.8 resolution: "handlebars@npm:4.7.8" dependencies: @@ -5269,13 +5324,6 @@ __metadata: languageName: node linkType: hard -"has-unicode@npm:^2.0.1": - version: 2.0.1 - resolution: "has-unicode@npm:2.0.1" - checksum: 10/041b4293ad6bf391e21c5d85ed03f412506d6623786b801c4ab39e4e6ca54993f13201bceb544d92963f9e0024e6e7fbf0cb1d84c9d6b31cb9c79c8c990d13d8 - languageName: node - linkType: hard - "has@npm:^1.0.3": version: 1.0.3 resolution: "has@npm:1.0.3" @@ -5369,7 +5417,7 @@ __metadata: languageName: node linkType: hard -"http-assert@npm:^1.3.0": +"http-assert@npm:^1.3.0, http-assert@npm:^1.5.0": version: 1.5.0 resolution: "http-assert@npm:1.5.0" dependencies: @@ -5379,7 +5427,7 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.1.0, http-cache-semantics@npm:^4.1.1": +"http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" checksum: 10/362d5ed66b12ceb9c0a328fb31200b590ab1b02f4a254a697dc796850cc4385603e75f53ec59f768b2dad3bfa1464bd229f7de278d2899a0e3beffc634b6683f @@ -5399,7 +5447,7 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:^2.0.0, http-errors@npm:~2.0.1": +"http-errors@npm:^2.0.0, http-errors@npm:^2.0.1, http-errors@npm:~2.0.1": version: 2.0.1 resolution: "http-errors@npm:2.0.1" dependencies: @@ -5433,14 +5481,13 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^5.0.0": - version: 5.0.0 - resolution: "http-proxy-agent@npm:5.0.0" +"http-proxy-agent@npm:^7.0.0": + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" dependencies: - "@tootallnate/once": "npm:2" - agent-base: "npm:6" - debug: "npm:4" - checksum: 10/5ee19423bc3e0fd5f23ce991b0755699ad2a46a440ce9cec99e8126bb98448ad3479d2c0ea54be5519db5b19a4ffaa69616bac01540db18506dd4dac3dc418f0 + agent-base: "npm:^7.1.0" + debug: "npm:^4.3.4" + checksum: 10/d062acfa0cb82beeb558f1043c6ba770ea892b5fb7b28654dbc70ea2aeea55226dd34c02a294f6c1ca179a5aa483c4ea641846821b182edbd9cc5d89b54c6848 languageName: node linkType: hard @@ -5451,13 +5498,13 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^5.0.0": - version: 5.0.1 - resolution: "https-proxy-agent@npm:5.0.1" +"https-proxy-agent@npm:^7.0.1": + version: 7.0.6 + resolution: "https-proxy-agent@npm:7.0.6" dependencies: - agent-base: "npm:6" + agent-base: "npm:^7.1.2" debug: "npm:4" - checksum: 10/f0dce7bdcac5e8eaa0be3c7368bb8836ed010fb5b6349ffb412b172a203efe8f807d9a6681319105ea1b6901e1972c7b5ea899672a7b9aad58309f766dcbe0df + checksum: 10/784b628cbd55b25542a9d85033bdfd03d4eda630fb8b3c9477959367f3be95dc476ed2ecbb9836c359c7c698027fc7b45723a302324433590f45d6c1706e8c13 languageName: node linkType: hard @@ -5591,27 +5638,27 @@ __metadata: languageName: node linkType: hard -"import-in-the-middle@npm:1.12.0": - version: 1.12.0 - resolution: "import-in-the-middle@npm:1.12.0" +"import-in-the-middle@npm:1.14.4": + version: 1.14.4 + resolution: "import-in-the-middle@npm:1.14.4" dependencies: - acorn: "npm:^8.8.2" + acorn: "npm:^8.14.0" acorn-import-attributes: "npm:^1.9.5" cjs-module-lexer: "npm:^1.2.2" module-details-from-path: "npm:^1.0.3" - checksum: 10/73f3f0ad8c3fceb90bcf308e84609290fe912af32a4be12fce2bf1fde28a0cb12d7219e15e8fe9e8d7ceafcb115a49a66566c2fd973d0a08e33437b00dfce3f9 + checksum: 10/96b657cfe33dda86cc1160446039b1ff115154a0242ff26b275177621e12f88ba2b23df5f15e1fa8e5cba57ee8f8d02d353df0d2ec1b08d3a3503e3e4e987ab3 languageName: node linkType: hard -"import-local@npm:^3.0.2": - version: 3.1.0 - resolution: "import-local@npm:3.1.0" +"import-local@npm:^3.2.0": + version: 3.2.0 + resolution: "import-local@npm:3.2.0" dependencies: pkg-dir: "npm:^4.2.0" resolve-cwd: "npm:^3.0.0" bin: import-local-fixture: fixtures/cli.js - checksum: 10/bfcdb63b5e3c0e245e347f3107564035b128a414c4da1172a20dc67db2504e05ede4ac2eee1252359f78b0bfd7b19ef180aec427c2fce6493ae782d73a04cddd + checksum: 10/0b0b0b412b2521739fbb85eeed834a3c34de9bc67e670b3d0b86248fc460d990a7b116ad056c084b87a693ef73d1f17268d6a5be626bb43c998a8b1c8a230004 languageName: node linkType: hard @@ -5643,13 +5690,6 @@ __metadata: languageName: node linkType: hard -"infer-owner@npm:^1.0.4": - version: 1.0.4 - resolution: "infer-owner@npm:1.0.4" - checksum: 10/181e732764e4a0611576466b4b87dac338972b839920b2a8cde43642e4ed6bd54dc1fb0b40874728f2a2df9a1b097b8ff83b56d5f8f8e3927f837fdcb47d8a89 - languageName: node - linkType: hard - "inflation@npm:^2.0.0": version: 2.0.0 resolution: "inflation@npm:2.0.0" @@ -5667,10 +5707,10 @@ __metadata: languageName: node linkType: hard -"influx@npm:^5.9.7": - version: 5.9.7 - resolution: "influx@npm:5.9.7" - checksum: 10/09ee08fc8ae963a45f60d4e6558df7231bc8891bc35720f378fc8399a9177e12d3d4d6784685345a206ebbe3d6c48f7b99c83ed94916f219f7d9ce065647d774 +"influx@npm:^5.12.0": + version: 5.12.0 + resolution: "influx@npm:5.12.0" + checksum: 10/3e0ec79775f444174a126d496b38515f703db605aed89acabac9796fa08b2d4d173361e8fe42fd69d692a4af0a5c48b11dfce211c73d0f2e8d160d2048e0bcba languageName: node linkType: hard @@ -5706,10 +5746,10 @@ __metadata: languageName: node linkType: hard -"ip@npm:^2.0.0": - version: 2.0.1 - resolution: "ip@npm:2.0.1" - checksum: 10/d6dd154e1bc5e8725adfdd6fb92218635b9cbe6d873d051bd63b178f009777f751a5eea4c67021723a7056325fc3052f8b6599af0a2d56f042c93e684b4a0349 +"ip-address@npm:^10.0.1": + version: 10.1.0 + resolution: "ip-address@npm:10.1.0" + checksum: 10/a6979629d1ad9c1fb424bc25182203fad739b40225aebc55ec6243bbff5035faf7b9ed6efab3a097de6e713acbbfde944baacfa73e11852bb43989c45a68d79e languageName: node linkType: hard @@ -5741,13 +5781,6 @@ __metadata: languageName: node linkType: hard -"is-arrayish@npm:^0.3.1": - version: 0.3.2 - resolution: "is-arrayish@npm:0.3.2" - checksum: 10/81a78d518ebd8b834523e25d102684ee0f7e98637136d3bdc93fd09636350fa06f1d8ca997ea28143d4d13cb1b69c0824f082db0ac13e1ab3311c10ffea60ade - languageName: node - linkType: hard - "is-bigint@npm:^1.0.1": version: 1.0.4 resolution: "is-bigint@npm:1.0.4" @@ -5822,7 +5855,7 @@ __metadata: languageName: node linkType: hard -"is-generator-fn@npm:^2.0.0": +"is-generator-fn@npm:^2.1.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" checksum: 10/a6ad5492cf9d1746f73b6744e0c43c0020510b59d56ddcb78a91cbc173f09b5e6beff53d75c9c5a29feb618bfef2bf458e025ecf3a57ad2268e2fb2569f56215 @@ -5867,13 +5900,6 @@ __metadata: languageName: node linkType: hard -"is-lambda@npm:^1.0.1": - version: 1.0.1 - resolution: "is-lambda@npm:1.0.1" - checksum: 10/93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 - languageName: node - linkType: hard - "is-nan@npm:^1.3.2": version: 1.3.2 resolution: "is-nan@npm:1.3.2" @@ -6004,7 +6030,16 @@ __metadata: languageName: node linkType: hard -"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.12, is-typed-array@npm:^1.1.14, is-typed-array@npm:^1.1.3, is-typed-array@npm:^1.1.9": +"is-typed-array@npm:^1.1.10, is-typed-array@npm:^1.1.12, is-typed-array@npm:^1.1.3, is-typed-array@npm:^1.1.9": + version: 1.1.12 + resolution: "is-typed-array@npm:1.1.12" + dependencies: + which-typed-array: "npm:^1.1.11" + checksum: 10/d953adfd3c41618d5e01b2a10f21817e4cdc9572772fa17211100aebb3811b6e3c2e308a0558cc87d218a30504cb90154b833013437776551bfb70606fb088ca + languageName: node + linkType: hard + +"is-typed-array@npm:^1.1.14": version: 1.1.15 resolution: "is-typed-array@npm:1.1.15" dependencies: @@ -6059,6 +6094,13 @@ __metadata: languageName: node linkType: hard +"isexe@npm:^3.1.1": + version: 3.1.1 + resolution: "isexe@npm:3.1.1" + checksum: 10/7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e + languageName: node + linkType: hard + "isobject@npm:^3.0.1": version: 3.0.1 resolution: "isobject@npm:3.0.1" @@ -6073,29 +6115,16 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-instrument@npm:^5.0.4": - version: 5.2.1 - resolution: "istanbul-lib-instrument@npm:5.2.1" - dependencies: - "@babel/core": "npm:^7.12.3" - "@babel/parser": "npm:^7.14.7" - "@istanbuljs/schema": "npm:^0.1.2" - istanbul-lib-coverage: "npm:^3.2.0" - semver: "npm:^6.3.0" - checksum: 10/bbc4496c2f304d799f8ec22202ab38c010ac265c441947f075c0f7d46bd440b45c00e46017cf9053453d42182d768b1d6ed0e70a142c95ab00df9843aa5ab80e - languageName: node - linkType: hard - -"istanbul-lib-instrument@npm:^6.0.0": - version: 6.0.0 - resolution: "istanbul-lib-instrument@npm:6.0.0" +"istanbul-lib-instrument@npm:^6.0.0, istanbul-lib-instrument@npm:^6.0.2": + version: 6.0.3 + resolution: "istanbul-lib-instrument@npm:6.0.3" dependencies: - "@babel/core": "npm:^7.12.3" - "@babel/parser": "npm:^7.14.7" - "@istanbuljs/schema": "npm:^0.1.2" + "@babel/core": "npm:^7.23.9" + "@babel/parser": "npm:^7.23.9" + "@istanbuljs/schema": "npm:^0.1.3" istanbul-lib-coverage: "npm:^3.2.0" semver: "npm:^7.5.4" - checksum: 10/a52efe2170ac2deeaaacc84d10fe8de41d97264a86e57df77e05c1e72227a333280f640836137b28fda802a2c71b2affb00a703979e6f7a462cc80047a6aff21 + checksum: 10/aa5271c0008dfa71b6ecc9ba1e801bf77b49dc05524e8c30d58aaf5b9505e0cd12f25f93165464d4266a518c5c75284ecb598fbd89fec081ae77d2c9d3327695 languageName: node linkType: hard @@ -6110,14 +6139,14 @@ __metadata: languageName: node linkType: hard -"istanbul-lib-source-maps@npm:^4.0.0": - version: 4.0.1 - resolution: "istanbul-lib-source-maps@npm:4.0.1" +"istanbul-lib-source-maps@npm:^5.0.0": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.23" debug: "npm:^4.1.1" istanbul-lib-coverage: "npm:^3.0.0" - source-map: "npm:^0.6.1" - checksum: 10/5526983462799aced011d776af166e350191b816821ea7bcf71cab3e5272657b062c47dc30697a22a43656e3ced78893a42de677f9ccf276a28c913190953b82 + checksum: 10/569dd0a392ee3464b1fe1accbaef5cc26de3479eacb5b91d8c67ebb7b425d39fd02247d85649c3a0e9c29b600809fa60b5af5a281a75a89c01f385b1e24823a2 languageName: node linkType: hard @@ -6144,261 +6173,235 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^4.1.1": - version: 4.1.1 - resolution: "jackspeak@npm:4.1.1" - dependencies: - "@isaacs/cliui": "npm:^8.0.2" - checksum: 10/ffceb270ec286841f48413bfb4a50b188662dfd599378ce142b6540f3f0a66821dc9dcb1e9ebc55c6c3b24dc2226c96e5819ba9bd7a241bd29031b61911718c7 - languageName: node - linkType: hard - -"jake@npm:^10.8.5": - version: 10.9.2 - resolution: "jake@npm:10.9.2" - dependencies: - async: "npm:^3.2.3" - chalk: "npm:^4.0.2" - filelist: "npm:^1.0.4" - minimatch: "npm:^3.1.2" - bin: - jake: bin/cli.js - checksum: 10/3be324708f99f031e0aec49ef8fd872eb4583cbe8a29a0c875f554f6ac638ee4ea5aa759bb63723fd54f77ca6d7db851eaa78353301734ed3700db9cb109a0cd - languageName: node - linkType: hard - -"jest-changed-files@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-changed-files@npm:29.7.0" +"jest-changed-files@npm:30.2.0": + version: 30.2.0 + resolution: "jest-changed-files@npm:30.2.0" dependencies: - execa: "npm:^5.0.0" - jest-util: "npm:^29.7.0" + execa: "npm:^5.1.1" + jest-util: "npm:30.2.0" p-limit: "npm:^3.1.0" - checksum: 10/3d93742e56b1a73a145d55b66e96711fbf87ef89b96c2fab7cfdfba8ec06612591a982111ca2b712bb853dbc16831ec8b43585a2a96b83862d6767de59cbf83d + checksum: 10/ff2275ed5839b88c12ffa66fdc5c17ba02d3e276be6b558bed92872c282d050c3fdd1a275a81187cbe35c16d6d40337b85838772836463c7a2fbd1cba9785ca0 languageName: node linkType: hard -"jest-circus@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-circus@npm:29.7.0" +"jest-circus@npm:30.2.0": + version: 30.2.0 + resolution: "jest-circus@npm:30.2.0" dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/expect": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" + "@jest/environment": "npm:30.2.0" + "@jest/expect": "npm:30.2.0" + "@jest/test-result": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - chalk: "npm:^4.0.0" + chalk: "npm:^4.1.2" co: "npm:^4.6.0" - dedent: "npm:^1.0.0" - is-generator-fn: "npm:^2.0.0" - jest-each: "npm:^29.7.0" - jest-matcher-utils: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-runtime: "npm:^29.7.0" - jest-snapshot: "npm:^29.7.0" - jest-util: "npm:^29.7.0" + dedent: "npm:^1.6.0" + is-generator-fn: "npm:^2.1.0" + jest-each: "npm:30.2.0" + jest-matcher-utils: "npm:30.2.0" + jest-message-util: "npm:30.2.0" + jest-runtime: "npm:30.2.0" + jest-snapshot: "npm:30.2.0" + jest-util: "npm:30.2.0" p-limit: "npm:^3.1.0" - pretty-format: "npm:^29.7.0" - pure-rand: "npm:^6.0.0" + pretty-format: "npm:30.2.0" + pure-rand: "npm:^7.0.0" slash: "npm:^3.0.0" - stack-utils: "npm:^2.0.3" - checksum: 10/716a8e3f40572fd0213bcfc1da90274bf30d856e5133af58089a6ce45089b63f4d679bd44e6be9d320e8390483ebc3ae9921981993986d21639d9019b523123d + stack-utils: "npm:^2.0.6" + checksum: 10/68bfc65d92385db1017643988215e4ff5af0b10bcab86fb749a063be6bb7d5eb556dc53dd21bedf833a19aa6ae1a781a8d27b2bea25562de02d294b3017435a9 languageName: node linkType: hard -"jest-cli@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-cli@npm:29.7.0" +"jest-cli@npm:30.2.0": + version: 30.2.0 + resolution: "jest-cli@npm:30.2.0" dependencies: - "@jest/core": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - chalk: "npm:^4.0.0" - create-jest: "npm:^29.7.0" - exit: "npm:^0.1.2" - import-local: "npm:^3.0.2" - jest-config: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-validate: "npm:^29.7.0" - yargs: "npm:^17.3.1" + "@jest/core": "npm:30.2.0" + "@jest/test-result": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + chalk: "npm:^4.1.2" + exit-x: "npm:^0.2.2" + import-local: "npm:^3.2.0" + jest-config: "npm:30.2.0" + jest-util: "npm:30.2.0" + jest-validate: "npm:30.2.0" + yargs: "npm:^17.7.2" peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true bin: - jest: bin/jest.js - checksum: 10/6cc62b34d002c034203065a31e5e9a19e7c76d9e8ef447a6f70f759c0714cb212c6245f75e270ba458620f9c7b26063cd8cf6cd1f7e3afd659a7cc08add17307 + jest: ./bin/jest.js + checksum: 10/1cc8304f0e2608801c84cdecce9565a6178f668a6475aed3767a1d82cc539915f98e7404d7c387510313684011dc3095c15397d6725f73aac80fbd96c4155faa languageName: node linkType: hard -"jest-config@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-config@npm:29.7.0" +"jest-config@npm:30.2.0": + version: 30.2.0 + resolution: "jest-config@npm:30.2.0" dependencies: - "@babel/core": "npm:^7.11.6" - "@jest/test-sequencer": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - babel-jest: "npm:^29.7.0" - chalk: "npm:^4.0.0" - ci-info: "npm:^3.2.0" - deepmerge: "npm:^4.2.2" - glob: "npm:^7.1.3" - graceful-fs: "npm:^4.2.9" - jest-circus: "npm:^29.7.0" - jest-environment-node: "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - jest-regex-util: "npm:^29.6.3" - jest-resolve: "npm:^29.7.0" - jest-runner: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-validate: "npm:^29.7.0" - micromatch: "npm:^4.0.4" + "@babel/core": "npm:^7.27.4" + "@jest/get-type": "npm:30.1.0" + "@jest/pattern": "npm:30.0.1" + "@jest/test-sequencer": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + babel-jest: "npm:30.2.0" + chalk: "npm:^4.1.2" + ci-info: "npm:^4.2.0" + deepmerge: "npm:^4.3.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.11" + jest-circus: "npm:30.2.0" + jest-docblock: "npm:30.2.0" + jest-environment-node: "npm:30.2.0" + jest-regex-util: "npm:30.0.1" + jest-resolve: "npm:30.2.0" + jest-runner: "npm:30.2.0" + jest-util: "npm:30.2.0" + jest-validate: "npm:30.2.0" + micromatch: "npm:^4.0.8" parse-json: "npm:^5.2.0" - pretty-format: "npm:^29.7.0" + pretty-format: "npm:30.2.0" slash: "npm:^3.0.0" strip-json-comments: "npm:^3.1.1" peerDependencies: "@types/node": "*" + esbuild-register: ">=3.4.0" ts-node: ">=9.0.0" peerDependenciesMeta: "@types/node": optional: true + esbuild-register: + optional: true ts-node: optional: true - checksum: 10/6bdf570e9592e7d7dd5124fc0e21f5fe92bd15033513632431b211797e3ab57eaa312f83cc6481b3094b72324e369e876f163579d60016677c117ec4853cf02b + checksum: 10/296786b0a3d62de77e2f691f208d54ab541c1a73f87747d922eda643c6f25b89125ef3150170c07a6c8a316a30c15428e46237d499f688b0777f38de8a61ad16 languageName: node linkType: hard -"jest-diff@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-diff@npm:29.7.0" +"jest-diff@npm:30.2.0": + version: 30.2.0 + resolution: "jest-diff@npm:30.2.0" dependencies: - chalk: "npm:^4.0.0" - diff-sequences: "npm:^29.6.3" - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.7.0" - checksum: 10/6f3a7eb9cd9de5ea9e5aa94aed535631fa6f80221832952839b3cb59dd419b91c20b73887deb0b62230d06d02d6b6cf34ebb810b88d904bb4fe1e2e4f0905c98 + "@jest/diff-sequences": "npm:30.0.1" + "@jest/get-type": "npm:30.1.0" + chalk: "npm:^4.1.2" + pretty-format: "npm:30.2.0" + checksum: 10/1fb9e4fb7dff81814b4f69eaa7db28e184d62306a3a8ea2447d02ca53d2cfa771e83ede513f67ec5239dffacfaac32ff2b49866d211e4c7516f51c1fc06ede42 languageName: node linkType: hard -"jest-docblock@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-docblock@npm:29.7.0" +"jest-docblock@npm:30.2.0": + version: 30.2.0 + resolution: "jest-docblock@npm:30.2.0" dependencies: - detect-newline: "npm:^3.0.0" - checksum: 10/8d48818055bc96c9e4ec2e217a5a375623c0d0bfae8d22c26e011074940c202aa2534a3362294c81d981046885c05d304376afba9f2874143025981148f3e96d + detect-newline: "npm:^3.1.0" + checksum: 10/e01a7d1193947ed0f9713c26bfc7852e51cb758cafec807e5665a0a8d582473a43778bee099f8aa5c70b2941963e5341f4b10bd86b036a4fa3bcec0f4c04e099 languageName: node linkType: hard -"jest-each@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-each@npm:29.7.0" +"jest-each@npm:30.2.0": + version: 30.2.0 + resolution: "jest-each@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" - chalk: "npm:^4.0.0" - jest-get-type: "npm:^29.6.3" - jest-util: "npm:^29.7.0" - pretty-format: "npm:^29.7.0" - checksum: 10/bd1a077654bdaa013b590deb5f7e7ade68f2e3289180a8c8f53bc8a49f3b40740c0ec2d3a3c1aee906f682775be2bebbac37491d80b634d15276b0aa0f2e3fda + "@jest/get-type": "npm:30.1.0" + "@jest/types": "npm:30.2.0" + chalk: "npm:^4.1.2" + jest-util: "npm:30.2.0" + pretty-format: "npm:30.2.0" + checksum: 10/f95e7dc1cef4b6a77899325702a214834ae25d01276cc31279654dc7e04f63c1925a37848dd16a0d16508c0fd3d182145f43c10af93952b7a689df3aeac198e9 languageName: node linkType: hard -"jest-environment-node@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-environment-node@npm:29.7.0" +"jest-environment-node@npm:30.2.0": + version: 30.2.0 + resolution: "jest-environment-node@npm:30.2.0" dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/fake-timers": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" + "@jest/environment": "npm:30.2.0" + "@jest/fake-timers": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - jest-mock: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - checksum: 10/9cf7045adf2307cc93aed2f8488942e39388bff47ec1df149a997c6f714bfc66b2056768973770d3f8b1bf47396c19aa564877eb10ec978b952c6018ed1bd637 - languageName: node - linkType: hard - -"jest-get-type@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-get-type@npm:29.6.3" - checksum: 10/88ac9102d4679d768accae29f1e75f592b760b44277df288ad76ce5bf038c3f5ce3719dea8aa0f035dac30e9eb034b848ce716b9183ad7cc222d029f03e92205 + jest-mock: "npm:30.2.0" + jest-util: "npm:30.2.0" + jest-validate: "npm:30.2.0" + checksum: 10/7918bfea7367bd3e12dbbc4ea5afb193b5c47e480a6d1382512f051e2f028458fc9f5ef2f6260737ad41a0b1894661790ff3aaf3cbb4148a33ce2ce7aec64847 languageName: node linkType: hard -"jest-haste-map@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-haste-map@npm:29.7.0" +"jest-haste-map@npm:30.2.0": + version: 30.2.0 + resolution: "jest-haste-map@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" - "@types/graceful-fs": "npm:^4.1.3" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - anymatch: "npm:^3.0.3" - fb-watchman: "npm:^2.0.0" - fsevents: "npm:^2.3.2" - graceful-fs: "npm:^4.2.9" - jest-regex-util: "npm:^29.6.3" - jest-util: "npm:^29.7.0" - jest-worker: "npm:^29.7.0" - micromatch: "npm:^4.0.4" + anymatch: "npm:^3.1.3" + fb-watchman: "npm:^2.0.2" + fsevents: "npm:^2.3.3" + graceful-fs: "npm:^4.2.11" + jest-regex-util: "npm:30.0.1" + jest-util: "npm:30.2.0" + jest-worker: "npm:30.2.0" + micromatch: "npm:^4.0.8" walker: "npm:^1.0.8" dependenciesMeta: fsevents: optional: true - checksum: 10/8531b42003581cb18a69a2774e68c456fb5a5c3280b1b9b77475af9e346b6a457250f9d756bfeeae2fe6cbc9ef28434c205edab9390ee970a919baddfa08bb85 + checksum: 10/a88be6b0b672144aa30fe2d72e630d639c8d8729ee2cef84d0f830eac2005ac021cd8354f8ed8ecd74223f6a8b281efb62f466f5c9e01ed17650e38761051f4c languageName: node linkType: hard -"jest-leak-detector@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-leak-detector@npm:29.7.0" +"jest-leak-detector@npm:30.2.0": + version: 30.2.0 + resolution: "jest-leak-detector@npm:30.2.0" dependencies: - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.7.0" - checksum: 10/e3950e3ddd71e1d0c22924c51a300a1c2db6cf69ec1e51f95ccf424bcc070f78664813bef7aed4b16b96dfbdeea53fe358f8aeaaea84346ae15c3735758f1605 + "@jest/get-type": "npm:30.1.0" + pretty-format: "npm:30.2.0" + checksum: 10/c430d6ed7910b2174738fbdca4ea64cbfe805216414c0d143c1090148f1389fec99d0733c0a8ed0a86709c89b4a4085b4749ac3a2cbc7deaf3ca87457afd24fc languageName: node linkType: hard -"jest-matcher-utils@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-matcher-utils@npm:29.7.0" +"jest-matcher-utils@npm:30.2.0": + version: 30.2.0 + resolution: "jest-matcher-utils@npm:30.2.0" dependencies: - chalk: "npm:^4.0.0" - jest-diff: "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.7.0" - checksum: 10/981904a494299cf1e3baed352f8a3bd8b50a8c13a662c509b6a53c31461f94ea3bfeffa9d5efcfeb248e384e318c87de7e3baa6af0f79674e987482aa189af40 + "@jest/get-type": "npm:30.1.0" + chalk: "npm:^4.1.2" + jest-diff: "npm:30.2.0" + pretty-format: "npm:30.2.0" + checksum: 10/f3f1ecf68ca63c9d1d80a175637a8fc655edfd1ee83220f6e3f6bd464ecbe2f93148fdd440a5a5e5a2b0b2cc8ee84ddc3dcef58a6dbc66821c792f48d260c6d4 languageName: node linkType: hard -"jest-message-util@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-message-util@npm:29.7.0" +"jest-message-util@npm:30.2.0": + version: 30.2.0 + resolution: "jest-message-util@npm:30.2.0" dependencies: - "@babel/code-frame": "npm:^7.12.13" - "@jest/types": "npm:^29.6.3" - "@types/stack-utils": "npm:^2.0.0" - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" - micromatch: "npm:^4.0.4" - pretty-format: "npm:^29.7.0" + "@babel/code-frame": "npm:^7.27.1" + "@jest/types": "npm:30.2.0" + "@types/stack-utils": "npm:^2.0.3" + chalk: "npm:^4.1.2" + graceful-fs: "npm:^4.2.11" + micromatch: "npm:^4.0.8" + pretty-format: "npm:30.2.0" slash: "npm:^3.0.0" - stack-utils: "npm:^2.0.3" - checksum: 10/31d53c6ed22095d86bab9d14c0fa70c4a92c749ea6ceece82cf30c22c9c0e26407acdfbdb0231435dc85a98d6d65ca0d9cbcd25cd1abb377fe945e843fb770b9 + stack-utils: "npm:^2.0.6" + checksum: 10/e29ec76e8c8e4da5f5b25198be247535626ccf3a940e93fdd51fc6a6bcf70feaa2921baae3806182a090431d90b08c939eb13fb64249b171d2e9ae3a452a8fd2 languageName: node linkType: hard -"jest-mock@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-mock@npm:29.7.0" +"jest-mock@npm:30.2.0": + version: 30.2.0 + resolution: "jest-mock@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - jest-util: "npm:^29.7.0" - checksum: 10/ae51d1b4f898724be5e0e52b2268a68fcd876d9b20633c864a6dd6b1994cbc48d62402b0f40f3a1b669b30ebd648821f086c26c08ffde192ced951ff4670d51c + jest-util: "npm:30.2.0" + checksum: 10/cde9b56805f90bf811a9231873ee88a0fb83bf4bf50972ae76960725da65220fcb119688f2e90e1ef33fbfd662194858d7f43809d881f1c41bb55d94e62adeab languageName: node linkType: hard -"jest-pnp-resolver@npm:^1.2.2": +"jest-pnp-resolver@npm:^1.2.3": version: 1.2.3 resolution: "jest-pnp-resolver@npm:1.2.3" peerDependencies: @@ -6410,199 +6413,201 @@ __metadata: languageName: node linkType: hard -"jest-regex-util@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-regex-util@npm:29.6.3" - checksum: 10/0518beeb9bf1228261695e54f0feaad3606df26a19764bc19541e0fc6e2a3737191904607fb72f3f2ce85d9c16b28df79b7b1ec9443aa08c3ef0e9efda6f8f2a +"jest-regex-util@npm:30.0.1": + version: 30.0.1 + resolution: "jest-regex-util@npm:30.0.1" + checksum: 10/fa8dac80c3e94db20d5e1e51d1bdf101cf5ede8f4e0b8f395ba8b8ea81e71804ffd747452a6bb6413032865de98ac656ef8ae43eddd18d980b6442a2764ed562 languageName: node linkType: hard -"jest-resolve-dependencies@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-resolve-dependencies@npm:29.7.0" +"jest-resolve-dependencies@npm:30.2.0": + version: 30.2.0 + resolution: "jest-resolve-dependencies@npm:30.2.0" dependencies: - jest-regex-util: "npm:^29.6.3" - jest-snapshot: "npm:^29.7.0" - checksum: 10/1e206f94a660d81e977bcfb1baae6450cb4a81c92e06fad376cc5ea16b8e8c6ea78c383f39e95591a9eb7f925b6a1021086c38941aa7c1b8a6a813c2f6e93675 + jest-regex-util: "npm:30.0.1" + jest-snapshot: "npm:30.2.0" + checksum: 10/0ff1a574f8c07f2e54a4ac8ab17aea00dfe2982e99b03fbd44f4211a94b8e5a59fdc43a59f9d6c0578a10a7b56a0611ad5ab40e4893973ff3f40dd414433b194 languageName: node linkType: hard -"jest-resolve@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-resolve@npm:29.7.0" +"jest-resolve@npm:30.2.0": + version: 30.2.0 + resolution: "jest-resolve@npm:30.2.0" dependencies: - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.7.0" - jest-pnp-resolver: "npm:^1.2.2" - jest-util: "npm:^29.7.0" - jest-validate: "npm:^29.7.0" - resolve: "npm:^1.20.0" - resolve.exports: "npm:^2.0.0" + chalk: "npm:^4.1.2" + graceful-fs: "npm:^4.2.11" + jest-haste-map: "npm:30.2.0" + jest-pnp-resolver: "npm:^1.2.3" + jest-util: "npm:30.2.0" + jest-validate: "npm:30.2.0" slash: "npm:^3.0.0" - checksum: 10/faa466fd9bc69ea6c37a545a7c6e808e073c66f46ab7d3d8a6ef084f8708f201b85d5fe1799789578b8b47fa1de47b9ee47b414d1863bc117a49e032ba77b7c7 + unrs-resolver: "npm:^1.7.11" + checksum: 10/e1f03da6811a946f5d885ea739a973975d099cc760641f9e1f90ac9c6621408538ba1e909f789d45d6e8d2411b78fb09230f16f15669621aa407aed7511fdf01 languageName: node linkType: hard -"jest-runner@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-runner@npm:29.7.0" +"jest-runner@npm:30.2.0": + version: 30.2.0 + resolution: "jest-runner@npm:30.2.0" dependencies: - "@jest/console": "npm:^29.7.0" - "@jest/environment": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" + "@jest/console": "npm:30.2.0" + "@jest/environment": "npm:30.2.0" + "@jest/test-result": "npm:30.2.0" + "@jest/transform": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - chalk: "npm:^4.0.0" + chalk: "npm:^4.1.2" emittery: "npm:^0.13.1" - graceful-fs: "npm:^4.2.9" - jest-docblock: "npm:^29.7.0" - jest-environment-node: "npm:^29.7.0" - jest-haste-map: "npm:^29.7.0" - jest-leak-detector: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-resolve: "npm:^29.7.0" - jest-runtime: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-watcher: "npm:^29.7.0" - jest-worker: "npm:^29.7.0" + exit-x: "npm:^0.2.2" + graceful-fs: "npm:^4.2.11" + jest-docblock: "npm:30.2.0" + jest-environment-node: "npm:30.2.0" + jest-haste-map: "npm:30.2.0" + jest-leak-detector: "npm:30.2.0" + jest-message-util: "npm:30.2.0" + jest-resolve: "npm:30.2.0" + jest-runtime: "npm:30.2.0" + jest-util: "npm:30.2.0" + jest-watcher: "npm:30.2.0" + jest-worker: "npm:30.2.0" p-limit: "npm:^3.1.0" source-map-support: "npm:0.5.13" - checksum: 10/9d8748a494bd90f5c82acea99be9e99f21358263ce6feae44d3f1b0cd90991b5df5d18d607e73c07be95861ee86d1cbab2a3fc6ca4b21805f07ac29d47c1da1e + checksum: 10/d3706aa70e64a7ef8b38360d34ea6c261ba4d0b42136d7fb603c4fa71c24fa81f22c39ed2e39ee0db2363a42827810291f3ceb6a299e5996b41d701ad9b24184 languageName: node linkType: hard -"jest-runtime@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-runtime@npm:29.7.0" +"jest-runtime@npm:30.2.0": + version: 30.2.0 + resolution: "jest-runtime@npm:30.2.0" dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/fake-timers": "npm:^29.7.0" - "@jest/globals": "npm:^29.7.0" - "@jest/source-map": "npm:^29.6.3" - "@jest/test-result": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" + "@jest/environment": "npm:30.2.0" + "@jest/fake-timers": "npm:30.2.0" + "@jest/globals": "npm:30.2.0" + "@jest/source-map": "npm:30.0.1" + "@jest/test-result": "npm:30.2.0" + "@jest/transform": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - chalk: "npm:^4.0.0" - cjs-module-lexer: "npm:^1.0.0" - collect-v8-coverage: "npm:^1.0.0" - glob: "npm:^7.1.3" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-mock: "npm:^29.7.0" - jest-regex-util: "npm:^29.6.3" - jest-resolve: "npm:^29.7.0" - jest-snapshot: "npm:^29.7.0" - jest-util: "npm:^29.7.0" + chalk: "npm:^4.1.2" + cjs-module-lexer: "npm:^2.1.0" + collect-v8-coverage: "npm:^1.0.2" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.11" + jest-haste-map: "npm:30.2.0" + jest-message-util: "npm:30.2.0" + jest-mock: "npm:30.2.0" + jest-regex-util: "npm:30.0.1" + jest-resolve: "npm:30.2.0" + jest-snapshot: "npm:30.2.0" + jest-util: "npm:30.2.0" slash: "npm:^3.0.0" strip-bom: "npm:^4.0.0" - checksum: 10/59eb58eb7e150e0834a2d0c0d94f2a0b963ae7182cfa6c63f2b49b9c6ef794e5193ef1634e01db41420c36a94cefc512cdd67a055cd3e6fa2f41eaf0f82f5a20 - languageName: node - linkType: hard - -"jest-snapshot@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-snapshot@npm:29.7.0" - dependencies: - "@babel/core": "npm:^7.11.6" - "@babel/generator": "npm:^7.7.2" - "@babel/plugin-syntax-jsx": "npm:^7.7.2" - "@babel/plugin-syntax-typescript": "npm:^7.7.2" - "@babel/types": "npm:^7.3.3" - "@jest/expect-utils": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - babel-preset-current-node-syntax: "npm:^1.0.0" - chalk: "npm:^4.0.0" - expect: "npm:^29.7.0" - graceful-fs: "npm:^4.2.9" - jest-diff: "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - jest-matcher-utils: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - natural-compare: "npm:^1.4.0" - pretty-format: "npm:^29.7.0" - semver: "npm:^7.5.3" - checksum: 10/cb19a3948256de5f922d52f251821f99657339969bf86843bd26cf3332eae94883e8260e3d2fba46129a27c3971c1aa522490e460e16c7fad516e82d10bbf9f8 + checksum: 10/81a3a9951420863f001e74c510bf35b85ae983f636f43ee1ffa1618b5a8ddafb681bc2810f71814bc8c8373e9593c89576b2325daf3c765e50057e48d5941df3 + languageName: node + linkType: hard + +"jest-snapshot@npm:30.2.0": + version: 30.2.0 + resolution: "jest-snapshot@npm:30.2.0" + dependencies: + "@babel/core": "npm:^7.27.4" + "@babel/generator": "npm:^7.27.5" + "@babel/plugin-syntax-jsx": "npm:^7.27.1" + "@babel/plugin-syntax-typescript": "npm:^7.27.1" + "@babel/types": "npm:^7.27.3" + "@jest/expect-utils": "npm:30.2.0" + "@jest/get-type": "npm:30.1.0" + "@jest/snapshot-utils": "npm:30.2.0" + "@jest/transform": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + babel-preset-current-node-syntax: "npm:^1.2.0" + chalk: "npm:^4.1.2" + expect: "npm:30.2.0" + graceful-fs: "npm:^4.2.11" + jest-diff: "npm:30.2.0" + jest-matcher-utils: "npm:30.2.0" + jest-message-util: "npm:30.2.0" + jest-util: "npm:30.2.0" + pretty-format: "npm:30.2.0" + semver: "npm:^7.7.2" + synckit: "npm:^0.11.8" + checksum: 10/119390b49f397ed622ba7c375fc15f97af67c4fc49a34cf829c86ee732be2b06ad3c7171c76bb842a0e84a234783f1a4c721909aa316fbe00c6abc7c5962dfbc languageName: node linkType: hard -"jest-util@npm:^29.0.0, jest-util@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-util@npm:29.7.0" +"jest-util@npm:30.2.0, jest-util@npm:^30.2.0": + version: 30.2.0 + resolution: "jest-util@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - chalk: "npm:^4.0.0" - ci-info: "npm:^3.2.0" - graceful-fs: "npm:^4.2.9" - picomatch: "npm:^2.2.3" - checksum: 10/30d58af6967e7d42bd903ccc098f3b4d3859ed46238fbc88d4add6a3f10bea00c226b93660285f058bc7a65f6f9529cf4eb80f8d4707f79f9e3a23686b4ab8f3 + chalk: "npm:^4.1.2" + ci-info: "npm:^4.2.0" + graceful-fs: "npm:^4.2.11" + picomatch: "npm:^4.0.2" + checksum: 10/cf2f2fb83417ea69f9992121561c95cf4e9aad7946819b771b8b52addf78811101b33b51d0a39fa0c305f2751dab262feed7699de052659ff03d51827c8862f5 languageName: node linkType: hard -"jest-validate@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-validate@npm:29.7.0" +"jest-validate@npm:30.2.0": + version: 30.2.0 + resolution: "jest-validate@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" - camelcase: "npm:^6.2.0" - chalk: "npm:^4.0.0" - jest-get-type: "npm:^29.6.3" + "@jest/get-type": "npm:30.1.0" + "@jest/types": "npm:30.2.0" + camelcase: "npm:^6.3.0" + chalk: "npm:^4.1.2" leven: "npm:^3.1.0" - pretty-format: "npm:^29.7.0" - checksum: 10/8ee1163666d8eaa16d90a989edba2b4a3c8ab0ffaa95ad91b08ca42b015bfb70e164b247a5b17f9de32d096987cada63ed8491ab82761bfb9a28bc34b27ae161 + pretty-format: "npm:30.2.0" + checksum: 10/61e66c6df29a1e181f8de063678dd2096bb52cc8a8ead3c9a3f853d54eca458ad04c7fb81931d9274affb67d0504a91a2a520456a139a26665810c3bf039b677 languageName: node linkType: hard -"jest-watcher@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-watcher@npm:29.7.0" +"jest-watcher@npm:30.2.0": + version: 30.2.0 + resolution: "jest-watcher@npm:30.2.0" dependencies: - "@jest/test-result": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" + "@jest/test-result": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - ansi-escapes: "npm:^4.2.1" - chalk: "npm:^4.0.0" + ansi-escapes: "npm:^4.3.2" + chalk: "npm:^4.1.2" emittery: "npm:^0.13.1" - jest-util: "npm:^29.7.0" - string-length: "npm:^4.0.1" - checksum: 10/4f616e0345676631a7034b1d94971aaa719f0cd4a6041be2aa299be437ea047afd4fe05c48873b7963f5687a2f6c7cbf51244be8b14e313b97bfe32b1e127e55 + jest-util: "npm:30.2.0" + string-length: "npm:^4.0.2" + checksum: 10/fa38d06dcc59dbbd6a9ff22dea499d3c81ed376d9993b82d01797a99bf466d48641a99b9f3670a4b5480ca31144c5e017b96b7059e4d7541358fb48cf517a2db languageName: node linkType: hard -"jest-worker@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-worker@npm:29.7.0" +"jest-worker@npm:30.2.0": + version: 30.2.0 + resolution: "jest-worker@npm:30.2.0" dependencies: "@types/node": "npm:*" - jest-util: "npm:^29.7.0" + "@ungap/structured-clone": "npm:^1.3.0" + jest-util: "npm:30.2.0" merge-stream: "npm:^2.0.0" - supports-color: "npm:^8.0.0" - checksum: 10/364cbaef00d8a2729fc760227ad34b5e60829e0869bd84976bdfbd8c0d0f9c2f22677b3e6dd8afa76ed174765351cd12bae3d4530c62eefb3791055127ca9745 + supports-color: "npm:^8.1.1" + checksum: 10/9354b0c71c80173f673da6bbc0ddaad26e4395b06532f7332e0c1e93e855b873b10139b040e01eda77f3dc5a0b67613e2bd7c56c4947ee771acfc3611de2ca29 languageName: node linkType: hard -"jest@npm:^29.7.0": - version: 29.7.0 - resolution: "jest@npm:29.7.0" +"jest@npm:^30.2.0": + version: 30.2.0 + resolution: "jest@npm:30.2.0" dependencies: - "@jest/core": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - import-local: "npm:^3.0.2" - jest-cli: "npm:^29.7.0" + "@jest/core": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + import-local: "npm:^3.2.0" + jest-cli: "npm:30.2.0" peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true bin: - jest: bin/jest.js - checksum: 10/97023d78446098c586faaa467fbf2c6b07ff06e2c85a19e3926adb5b0effe9ac60c4913ae03e2719f9c01ae8ffd8d92f6b262cedb9555ceeb5d19263d8c6362a + jest: ./bin/jest.js + checksum: 10/61c9d100750e4354cd7305d1f3ba253ffde4deaf12cb4be4d42d54f2dd5986e383a39c4a8691dbdc3839c69094a52413ed36f1886540ac37b71914a990b810d0 languageName: node linkType: hard @@ -6712,7 +6717,7 @@ __metadata: languageName: node linkType: hard -"kairos-lib@npm:^0.2.3": +"kairos-lib@npm:0.2.3, kairos-lib@npm:^0.2.3": version: 0.2.3 resolution: "kairos-lib@npm:0.2.3" dependencies: @@ -6746,13 +6751,6 @@ __metadata: languageName: node linkType: hard -"kleur@npm:^3.0.3": - version: 3.0.3 - resolution: "kleur@npm:3.0.3" - checksum: 10/0c0ecaf00a5c6173d25059c7db2113850b5457016dfa1d0e3ef26da4704fbb186b4938d7611246d86f0ddf1bccf26828daa5877b1f232a65e7373d0122a83e7f - languageName: node - linkType: hard - "koa-bodyparser@npm:^4.4.1": version: 4.4.1 resolution: "koa-bodyparser@npm:4.4.1" @@ -6781,13 +6779,13 @@ __metadata: languageName: node linkType: hard -"koa-mount@npm:^4.0.0": - version: 4.0.0 - resolution: "koa-mount@npm:4.0.0" +"koa-mount@npm:^4.2.0": + version: 4.2.0 + resolution: "koa-mount@npm:4.2.0" dependencies: debug: "npm:^4.0.1" koa-compose: "npm:^4.1.0" - checksum: 10/c7e8c5cca4d2ccc4742e63c81b86b44f0290075148897b5d633acdd137e90f554c60c232fbc62e843eaedb913b67c5a49367c1142e290b8cfd9c28eb4a0480ec + checksum: 10/c89b83b0fcd94941755fe0b87a5335fc4670eea80078fea5c52c0c153c6823748caa367d1773efb930805e19db26068db25c7c89456a34200910e655987331ef languageName: node linkType: hard @@ -6812,9 +6810,9 @@ __metadata: languageName: node linkType: hard -"koa@npm:^2.13.4, koa@npm:^2.15.3": - version: 2.16.3 - resolution: "koa@npm:2.16.3" +"koa@npm:^2.13.4": + version: 2.15.3 + resolution: "koa@npm:2.15.3" dependencies: accepts: "npm:^1.3.5" cache-content-type: "npm:^1.0.0" @@ -6839,7 +6837,33 @@ __metadata: statuses: "npm:^1.5.0" type-is: "npm:^1.6.16" vary: "npm:^1.1.2" - checksum: 10/62b6bc4939003eab2b77d523207e252f4eed3f75471fce3b50fe46a80fb01b9f425d4094437f25e3579ad90bcf43b652c166ac5b58d277255ed82a0ea7069ac8 + checksum: 10/b2c2771a4ee5268f9d039ce025b9c3798a0baba8c3cf3895a6fc2d286363e0cd2c98c02a5b87f14100baa2bc17d854eed6ed80f9bd41afda1d056f803b206514 + languageName: node + linkType: hard + +"koa@npm:^3.1.1": + version: 3.1.2 + resolution: "koa@npm:3.1.2" + dependencies: + accepts: "npm:^1.3.8" + content-disposition: "npm:~1.0.1" + content-type: "npm:^1.0.5" + cookies: "npm:~0.9.1" + delegates: "npm:^1.0.0" + destroy: "npm:^1.2.0" + encodeurl: "npm:^2.0.0" + escape-html: "npm:^1.0.3" + fresh: "npm:~0.5.2" + http-assert: "npm:^1.5.0" + http-errors: "npm:^2.0.0" + koa-compose: "npm:^4.1.0" + mime-types: "npm:^3.0.1" + on-finished: "npm:^2.4.1" + parseurl: "npm:^1.3.3" + statuses: "npm:^2.0.1" + type-is: "npm:^2.0.1" + vary: "npm:^1.1.2" + checksum: 10/0f89a4c69b5d0cc72ff4e971cbaf1e85ae4865c96612c6efab6d21218b25582c064cba202b1c9e678aeb121aee5908d415b45f3098d3e297f4723e49648e5ce7 languageName: node linkType: hard @@ -6995,10 +7019,10 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.0.0, lodash@npm:^4.17.15": - version: 4.17.23 - resolution: "lodash@npm:4.17.23" - checksum: 10/82504c88250f58da7a5a4289f57a4f759c44946c005dd232821c7688b5fcfbf4a6268f6a6cdde4b792c91edd2f3b5398c1d2a0998274432cff76def48735e233 +"lodash@npm:^4.0.0": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 languageName: node linkType: hard @@ -7030,10 +7054,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^11.0.0": - version: 11.0.2 - resolution: "lru-cache@npm:11.0.2" - checksum: 10/25fcb66e9d91eaf17227c6abfe526a7bed5903de74f93bfde380eb8a13410c5e8d3f14fe447293f3f322a7493adf6f9f015c6f1df7a235ff24ec30f366e1c058 +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": + version: 11.2.5 + resolution: "lru-cache@npm:11.2.5" + checksum: 10/be50f66c6e23afeaab9c7eefafa06344dd13cde7b3528809c2660c4ad70d93b9ba537366634623cbb2eb411671f526b5a4af2c602507b9258aead0fa8d713f6c languageName: node linkType: hard @@ -7055,22 +7079,6 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^7.7.1": - version: 7.18.3 - resolution: "lru-cache@npm:7.18.3" - checksum: 10/6029ca5aba3aacb554e919d7ef804fffd4adfc4c83db00fac8248c7c78811fb6d4b6f70f7fd9d55032b3823446546a007edaa66ad1f2377ae833bd983fac5d98 - languageName: node - linkType: hard - -"make-dir@npm:^3.1.0": - version: 3.1.0 - resolution: "make-dir@npm:3.1.0" - dependencies: - semver: "npm:^6.0.0" - checksum: 10/484200020ab5a1fdf12f393fe5f385fc8e4378824c940fba1729dcd198ae4ff24867bc7a5646331e50cead8abff5d9270c456314386e629acec6dff4b8016b78 - languageName: node - linkType: hard - "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -7087,50 +7095,22 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^10.0.3": - version: 10.2.1 - resolution: "make-fetch-happen@npm:10.2.1" - dependencies: - agentkeepalive: "npm:^4.2.1" - cacache: "npm:^16.1.0" - http-cache-semantics: "npm:^4.1.0" - http-proxy-agent: "npm:^5.0.0" - https-proxy-agent: "npm:^5.0.0" - is-lambda: "npm:^1.0.1" - lru-cache: "npm:^7.7.1" - minipass: "npm:^3.1.6" - minipass-collect: "npm:^1.0.2" - minipass-fetch: "npm:^2.0.3" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^0.6.3" - promise-retry: "npm:^2.0.1" - socks-proxy-agent: "npm:^7.0.0" - ssri: "npm:^9.0.0" - checksum: 10/fef5acb865a46f25ad0b5ad7d979799125db5dbb24ea811ffa850fbb804bc8e495df2237a8ec3a4fc6250e73c2f95549cca6d6d36a73b1faa61224504eb1188f - languageName: node - linkType: hard - -"make-fetch-happen@npm:^11.0.3": - version: 11.1.1 - resolution: "make-fetch-happen@npm:11.1.1" +"make-fetch-happen@npm:^15.0.0": + version: 15.0.3 + resolution: "make-fetch-happen@npm:15.0.3" dependencies: - agentkeepalive: "npm:^4.2.1" - cacache: "npm:^17.0.0" + "@npmcli/agent": "npm:^4.0.0" + cacache: "npm:^20.0.1" http-cache-semantics: "npm:^4.1.1" - http-proxy-agent: "npm:^5.0.0" - https-proxy-agent: "npm:^5.0.0" - is-lambda: "npm:^1.0.1" - lru-cache: "npm:^7.7.1" - minipass: "npm:^5.0.0" - minipass-fetch: "npm:^3.0.0" + minipass: "npm:^7.0.2" + minipass-fetch: "npm:^5.0.0" minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^0.6.3" + negotiator: "npm:^1.0.0" + proc-log: "npm:^6.0.0" promise-retry: "npm:^2.0.1" - socks-proxy-agent: "npm:^7.0.0" - ssri: "npm:^10.0.0" - checksum: 10/b4b442cfaaec81db159f752a5f2e3ee3d7aa682782868fa399200824ec6298502e01bdc456e443dc219bcd5546c8e4471644d54109c8599841dc961d17a805fa + ssri: "npm:^13.0.0" + checksum: 10/78da4fc1df83cb596e2bae25aa0653b8a9c6cbdd6674a104894e03be3acfcd08c70b78f06ef6407fbd6b173f6a60672480d78641e693d05eb71c09c13ee35278 languageName: node linkType: hard @@ -7211,6 +7191,13 @@ __metadata: languageName: node linkType: hard +"media-typer@npm:^1.1.0": + version: 1.1.0 + resolution: "media-typer@npm:1.1.0" + checksum: 10/a58dd60804df73c672942a7253ccc06815612326dc1c0827984b1a21704466d7cde351394f47649e56cf7415e6ee2e26e000e81b51b3eebb5a93540e8bf93cbd + languageName: node + linkType: hard + "memory-pager@npm:^1.0.2": version: 1.5.0 resolution: "memory-pager@npm:1.5.0" @@ -7232,7 +7219,7 @@ __metadata: languageName: node linkType: hard -"meow@npm:^8.0.0": +"meow@npm:^8.1.2": version: 8.1.2 resolution: "meow@npm:8.1.2" dependencies: @@ -7272,9 +7259,9 @@ __metadata: languageName: node linkType: hard -"meteor-node-stubs@npm:^1.2.12": - version: 1.2.12 - resolution: "meteor-node-stubs@npm:1.2.12" +"meteor-node-stubs@npm:^1.2.25": + version: 1.2.25 + resolution: "meteor-node-stubs@npm:1.2.25" dependencies: "@meteorjs/crypto-browserify": "npm:^3.12.1" assert: "npm:^2.1.0" @@ -7283,7 +7270,6 @@ __metadata: console-browserify: "npm:^1.2.0" constants-browserify: "npm:^1.0.0" domain-browser: "npm:^4.23.0" - elliptic: "npm:^6.6.0" events: "npm:^3.3.0" https-browserify: "npm:^1.0.0" os-browserify: "npm:^0.3.0" @@ -7292,6 +7278,7 @@ __metadata: punycode: "npm:^1.4.1" querystring-es3: "npm:^0.2.1" readable-stream: "npm:^3.6.2" + sha.js: "npm:^2.4.12" stream-browserify: "npm:^3.0.0" stream-http: "npm:^3.2.0" string_decoder: "npm:^1.3.0" @@ -7300,7 +7287,7 @@ __metadata: url: "npm:^0.11.4" util: "npm:^0.12.5" vm-browserify: "npm:^1.1.2" - checksum: 10/d42a26894b15a306979f1afee9ab27ff66027e2ac2e236c348c9f3ca8f537c63bcad49a2efe3cfe57162ff6b93b07bb786174777868e2370b1073720f5e5f49d + checksum: 10/57390ff99f2fa775f8b46b0faabbbfbcab9f193d264f3cb1c2e250e24cfd211c3437ad3c7646d7c3965531155fc698383e5bd5d8f1504d93a4b1cc9cbc007b0f languageName: node linkType: hard @@ -7311,7 +7298,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.4, micromatch@npm:^4.0.8": +"micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -7340,7 +7327,14 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:^2.1.35, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-db@npm:^1.54.0": + version: 1.54.0 + resolution: "mime-db@npm:1.54.0" + checksum: 10/9e7834be3d66ae7f10eaa69215732c6d389692b194f876198dca79b2b90cbf96688d9d5d05ef7987b20f749b769b11c01766564264ea5f919c88b32a29011311 + languageName: node + linkType: hard + +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.18, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -7349,6 +7343,15 @@ __metadata: languageName: node linkType: hard +"mime-types@npm:^3.0.0, mime-types@npm:^3.0.1": + version: 3.0.2 + resolution: "mime-types@npm:3.0.2" + dependencies: + mime-db: "npm:^1.54.0" + checksum: 10/9db0ad31f5eff10ee8f848130779b7f2d056ddfdb6bda696cb69be68d486d33a3457b4f3f9bdeb60d0736edb471bd5a7c0a384375c011c51c889fd0d5c3b893e + languageName: node + linkType: hard + "mime@npm:^1.3.4": version: 1.6.0 resolution: "mime@npm:1.6.0" @@ -7386,39 +7389,30 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.1.1": - version: 10.1.1 - resolution: "minimatch@npm:10.1.1" +"minimatch@npm:^10.1.2": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" dependencies: - "@isaacs/brace-expansion": "npm:^5.0.0" - checksum: 10/110f38921ea527022e90f7a5f43721838ac740d0a0c26881c03b57c261354fb9a0430e40b2c56dfcea2ef3c773768f27210d1106f1f2be19cde3eea93f26f45e + brace-expansion: "npm:^5.0.2" + checksum: 10/aea4874e521c55bb60744685bbffe3d152e5460f84efac3ea936e6bbe2ceba7deb93345fec3f9bb17f7b6946776073a64d40ae32bf5f298ad690308121068a1f languageName: node linkType: hard "minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" dependencies: brace-expansion: "npm:^1.1.7" - checksum: 10/e0b25b04cd4ec6732830344e5739b13f8690f8a012d73445a4a19fbc623f5dd481ef7a5827fde25954cd6026fede7574cc54dc4643c99d6c6b653d6203f94634 - languageName: node - linkType: hard - -"minimatch@npm:^5.0.1": - version: 5.1.6 - resolution: "minimatch@npm:5.1.6" - dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/126b36485b821daf96d33b5c821dac600cc1ab36c87e7a532594f9b1652b1fa89a1eebcaad4dff17c764dce1a7ac1531327f190fed5f97d8f6e5f889c116c429 + checksum: 10/b11a7ee5773cd34c1a0c8436cdbe910901018fb4b6cb47aa508a18d567f6efd2148507959e35fba798389b161b8604a2d704ccef751ea36bd4582f9852b7d63f languageName: node linkType: hard "minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": - version: 9.0.5 - resolution: "minimatch@npm:9.0.5" + version: 9.0.9 + resolution: "minimatch@npm:9.0.9" dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/dd6a8927b063aca6d910b119e1f2df6d2ce7d36eab91de83167dd136bb85e1ebff97b0d3de1cb08bd1f7e018ca170b4962479fefab5b2a69e2ae12cb2edc8348 + brace-expansion: "npm:^2.0.2" + checksum: 10/b91fad937deaffb68a45a2cb731ff3cff1c3baf9b6469c879477ed16f15c8f4ce39d63a3f75c2455107c2fdff0f3ab597d97dc09e2e93b883aafcf926ef0c8f9 languageName: node linkType: hard @@ -7440,42 +7434,27 @@ __metadata: languageName: node linkType: hard -"minipass-collect@npm:^1.0.2": - version: 1.0.2 - resolution: "minipass-collect@npm:1.0.2" - dependencies: - minipass: "npm:^3.0.0" - checksum: 10/14df761028f3e47293aee72888f2657695ec66bd7d09cae7ad558da30415fdc4752bbfee66287dcc6fd5e6a2fa3466d6c484dc1cbd986525d9393b9523d97f10 - languageName: node - linkType: hard - -"minipass-fetch@npm:^2.0.3": - version: 2.1.2 - resolution: "minipass-fetch@npm:2.1.2" +"minipass-collect@npm:^2.0.1": + version: 2.0.1 + resolution: "minipass-collect@npm:2.0.1" dependencies: - encoding: "npm:^0.1.13" - minipass: "npm:^3.1.6" - minipass-sized: "npm:^1.0.3" - minizlib: "npm:^2.1.2" - dependenciesMeta: - encoding: - optional: true - checksum: 10/8cfc589563ae2a11eebbf79121ef9a526fd078fca949ed3f1e4a51472ca4a4aad89fcea1738982ce9d7d833116ecc9c6ae9ebbd844832a94e3f4a3d4d1b9d3b9 + minipass: "npm:^7.0.3" + checksum: 10/b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 languageName: node linkType: hard -"minipass-fetch@npm:^3.0.0": - version: 3.0.4 - resolution: "minipass-fetch@npm:3.0.4" +"minipass-fetch@npm:^5.0.0": + version: 5.0.1 + resolution: "minipass-fetch@npm:5.0.1" dependencies: encoding: "npm:^0.1.13" minipass: "npm:^7.0.3" - minipass-sized: "npm:^1.0.3" - minizlib: "npm:^2.1.2" + minipass-sized: "npm:^2.0.0" + minizlib: "npm:^3.0.1" dependenciesMeta: encoding: optional: true - checksum: 10/3edf72b900e30598567eafe96c30374432a8709e61bb06b87198fa3192d466777e2ec21c52985a0999044fa6567bd6f04651585983a1cbb27e2c1770a07ed2a2 + checksum: 10/08bf0c9866e7f344bf1863ce0d99c0a6fe96b43ef5a4119e23d84a21e613a3f55ecf302adf28d9e228b4ebd50e81d5e84c397e0535089090427319379f478d94 languageName: node linkType: hard @@ -7497,16 +7476,16 @@ __metadata: languageName: node linkType: hard -"minipass-sized@npm:^1.0.3": - version: 1.0.3 - resolution: "minipass-sized@npm:1.0.3" +"minipass-sized@npm:^2.0.0": + version: 2.0.0 + resolution: "minipass-sized@npm:2.0.0" dependencies: - minipass: "npm:^3.0.0" - checksum: 10/40982d8d836a52b0f37049a0a7e5d0f089637298e6d9b45df9c115d4f0520682a78258905e5c8b180fb41b593b0a82cc1361d2c74b45f7ada66334f84d1ecfdd + minipass: "npm:^7.1.2" + checksum: 10/3b89adf64ca705662f77481e278eff5ec0a57aeffb5feba7cc8843722b1e7770efc880f2a17d1d4877b2d7bf227873cd46afb4da44c0fd18088b601ea50f96bb languageName: node linkType: hard -"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6": +"minipass@npm:^3.0.0": version: 3.3.6 resolution: "minipass@npm:3.3.6" dependencies: @@ -7515,27 +7494,19 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0": - version: 5.0.0 - resolution: "minipass@npm:5.0.0" - checksum: 10/61682162d29f45d3152b78b08bab7fb32ca10899bc5991ffe98afc18c9e9543bd1e3be94f8b8373ba6262497db63607079dc242ea62e43e7b2270837b7347c93 - languageName: node - linkType: hard - -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.3, minipass@npm:^7.1.2": +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, minipass@npm:^7.1.2": version: 7.1.2 resolution: "minipass@npm:7.1.2" checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 languageName: node linkType: hard -"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": - version: 2.1.2 - resolution: "minizlib@npm:2.1.2" +"minizlib@npm:^3.0.1, minizlib@npm:^3.1.0": + version: 3.1.0 + resolution: "minizlib@npm:3.1.0" dependencies: - minipass: "npm:^3.0.0" - yallist: "npm:^4.0.0" - checksum: 10/ae0f45436fb51344dcb87938446a32fbebb540d0e191d63b35e1c773d47512e17307bf54aa88326cc6d176594d00e4423563a091f7266c2f9a6872cdc1e234d1 + minipass: "npm:^7.1.2" + checksum: 10/f47365cc2cb7f078cbe7e046eb52655e2e7e97f8c0a9a674f4da60d94fb0624edfcec9b5db32e8ba5a99a5f036f595680ae6fe02a262beaa73026e505cc52f99 languageName: node linkType: hard @@ -7550,7 +7521,7 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": +"mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" bin: @@ -7559,7 +7530,7 @@ __metadata: languageName: node linkType: hard -"modify-values@npm:^1.0.0": +"modify-values@npm:^1.0.1": version: 1.0.1 resolution: "modify-values@npm:1.0.1" checksum: 10/16fa93f7ddb2540a8e82c99738ae4ed0e8e8cae57c96e13a0db9d68dfad074fd2eec542929b62ebbb18b357bbb3e4680b92d3a4099baa7aeb32360cb1c8f0247 @@ -7580,7 +7551,7 @@ __metadata: languageName: node linkType: hard -"mongodb-connection-string-url@npm:^3.0.0": +"mongodb-connection-string-url@npm:^3.0.2": version: 3.0.2 resolution: "mongodb-connection-string-url@npm:3.0.2" dependencies: @@ -7590,20 +7561,20 @@ __metadata: languageName: node linkType: hard -"mongodb@npm:^6.12.0": - version: 6.13.0 - resolution: "mongodb@npm:6.13.0" +"mongodb@npm:^6.21.0": + version: 6.21.0 + resolution: "mongodb@npm:6.21.0" dependencies: - "@mongodb-js/saslprep": "npm:^1.1.9" - bson: "npm:^6.10.1" - mongodb-connection-string-url: "npm:^3.0.0" + "@mongodb-js/saslprep": "npm:^1.3.0" + bson: "npm:^6.10.4" + mongodb-connection-string-url: "npm:^3.0.2" peerDependencies: "@aws-sdk/credential-providers": ^3.188.0 "@mongodb-js/zstd": ^1.1.0 || ^2.0.0 gcp-metadata: ^5.2.0 kerberos: ^2.0.1 mongodb-client-encryption: ">=6.0.0 <7" - snappy: ^7.2.2 + snappy: ^7.3.2 socks: ^2.7.1 peerDependenciesMeta: "@aws-sdk/credential-providers": @@ -7620,7 +7591,7 @@ __metadata: optional: true socks: optional: true - checksum: 10/769cc18eb3e34dabdbe56abd4862a1d79214fab79a96f8e8b0c67f2681dd002e88e0f8c869871b82a257297f87b318f930900eb65ad3378edc36bac5d2a7d542 + checksum: 10/28d2cab1c55c4cf58e410529ac6ae4c79a233adeb2147ba872d912819a0b496ee2dc5b9819ccbf0527618ced3b841e733b221fd1c627901e8e87ae60a8dc0553 languageName: node linkType: hard @@ -7645,12 +7616,21 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.8": - version: 3.3.8 - resolution: "nanoid@npm:3.3.8" +"nanoid@npm:^3.3.11": + version: 3.3.11 + resolution: "nanoid@npm:3.3.11" bin: nanoid: bin/nanoid.cjs - checksum: 10/2d1766606cf0d6f47b6f0fdab91761bb81609b2e3d367027aff45e6ee7006f660fb7e7781f4a34799fe6734f1268eeed2e37a5fdee809ade0c2d4eb11b0f9c40 + checksum: 10/73b5afe5975a307aaa3c95dfe3334c52cdf9ae71518176895229b8d65ab0d1c0417dd081426134eb7571c055720428ea5d57c645138161e7d10df80815527c48 + languageName: node + linkType: hard + +"napi-postinstall@npm:^0.3.0": + version: 0.3.4 + resolution: "napi-postinstall@npm:0.3.4" + bin: + napi-postinstall: lib/cli.js + checksum: 10/5541381508f9e1051ff3518701c7130ebac779abb3a1ffe9391fcc3cab4cc0569b0ba0952357db3f6b12909c3bb508359a7a60261ffd795feebbdab967175832 languageName: node linkType: hard @@ -7661,13 +7641,20 @@ __metadata: languageName: node linkType: hard -"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": +"negotiator@npm:0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" checksum: 10/2723fb822a17ad55c93a588a4bc44d53b22855bf4be5499916ca0cab1e7165409d0b288ba2577d7b029f10ce18cf2ed8e703e5af31c984e1e2304277ef979837 languageName: node linkType: hard +"negotiator@npm:^1.0.0": + version: 1.0.0 + resolution: "negotiator@npm:1.0.0" + checksum: 10/b5734e87295324fabf868e36fb97c84b7d7f3156ec5f4ee5bf6e488079c11054f818290fc33804cef7b1ee21f55eeb14caea83e7dafae6492a409b3e573153e5 + languageName: node + linkType: hard + "neo-async@npm:^2.6.2": version: 2.6.2 resolution: "neo-async@npm:2.6.2" @@ -7682,26 +7669,12 @@ __metadata: languageName: node linkType: hard -"node-addon-api@npm:^5.0.0": - version: 5.1.0 - resolution: "node-addon-api@npm:5.1.0" +"node-addon-api@npm:^8.3.0": + version: 8.5.0 + resolution: "node-addon-api@npm:8.5.0" dependencies: node-gyp: "npm:latest" - checksum: 10/595f59ffb4630564f587c502119cbd980d302e482781021f3b479f5fc7e41cf8f2f7280fdc2795f32d148e4f3259bd15043c52d4a3442796aa6f1ae97b959636 - languageName: node - linkType: hard - -"node-fetch@npm:^2.6.7": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" - dependencies: - whatwg-url: "npm:^5.0.0" - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: 10/b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676 + checksum: 10/9a893f4f835fbc3908e0070f7bcacf36e37fd06be8008409b104c30df4092a0d9a29927b3a74cdbc1d34338274ba4116d597a41f573e06c29538a1a70d07413f languageName: node linkType: hard @@ -7714,45 +7687,34 @@ __metadata: languageName: node linkType: hard -"node-gyp@npm:^9.4.1": - version: 9.4.1 - resolution: "node-gyp@npm:9.4.1" - dependencies: - env-paths: "npm:^2.2.0" - exponential-backoff: "npm:^3.1.1" - glob: "npm:^7.1.4" - graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^10.0.3" - nopt: "npm:^6.0.0" - npmlog: "npm:^6.0.0" - rimraf: "npm:^3.0.2" - semver: "npm:^7.3.5" - tar: "npm:^6.1.2" - which: "npm:^2.0.2" +"node-gyp-build@npm:^4.8.4": + version: 4.8.4 + resolution: "node-gyp-build@npm:4.8.4" bin: - node-gyp: bin/node-gyp.js - checksum: 10/329b109b138e48cb0416a6bca56e171b0e479d6360a548b80f06eced4bef3cf37652a3d20d171c20023fb18d996bd7446a49d4297ddb59fc48100178a92f432d + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 10/6a7d62289d1afc419fc8fc9bd00aa4e554369e50ca0acbc215cb91446148b75ff7e2a3b53c2c5b2c09a39d416d69f3d3237937860373104b5fe429bf30ad9ac5 languageName: node linkType: hard "node-gyp@npm:latest": - version: 9.4.0 - resolution: "node-gyp@npm:9.4.0" + version: 12.2.0 + resolution: "node-gyp@npm:12.2.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" - glob: "npm:^7.1.4" graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^11.0.3" - nopt: "npm:^6.0.0" - npmlog: "npm:^6.0.0" - rimraf: "npm:^3.0.2" + make-fetch-happen: "npm:^15.0.0" + nopt: "npm:^9.0.0" + proc-log: "npm:^6.0.0" semver: "npm:^7.3.5" - tar: "npm:^6.1.2" - which: "npm:^2.0.2" + tar: "npm:^7.5.4" + tinyglobby: "npm:^0.2.12" + which: "npm:^6.0.0" bin: node-gyp: bin/node-gyp.js - checksum: 10/458317127c63877365f227b18ef2362b013b7f8440b35ae722935e61b31e6b84ec0e3625ab07f90679e2f41a1d5a7df6c4049fdf8e7b3c81fcf22775147b47ac + checksum: 10/4ebab5b77585a637315e969c2274b5520562473fe75de850639a580c2599652fb9f33959ec782ea45a2e149d8f04b548030f472eeeb3dbdf19a7f2ccbc30b908 languageName: node linkType: hard @@ -7800,25 +7762,14 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^5.0.0": - version: 5.0.0 - resolution: "nopt@npm:5.0.0" - dependencies: - abbrev: "npm:1" - bin: - nopt: bin/nopt.js - checksum: 10/00f9bb2d16449469ba8ffcf9b8f0eae6bae285ec74b135fec533e5883563d2400c0cd70902d0a7759e47ac031ccf206ace4e86556da08ed3f1c66dda206e9ccd - languageName: node - linkType: hard - -"nopt@npm:^6.0.0": - version: 6.0.0 - resolution: "nopt@npm:6.0.0" +"nopt@npm:^9.0.0": + version: 9.0.0 + resolution: "nopt@npm:9.0.0" dependencies: - abbrev: "npm:^1.0.0" + abbrev: "npm:^4.0.0" bin: nopt: bin/nopt.js - checksum: 10/3c1128e07cd0241ae66d6e6a472170baa9f3e84dd4203950ba8df5bafac4efa2166ce917a57ef02b01ba7c40d18b2cc64b29b225fd3640791fe07b24f0b33a32 + checksum: 10/56a1ccd2ad711fb5115918e2c96828703cddbe12ba2c3bd00591758f6fa30e6f47dd905c59dbfcf9b773f3a293b45996609fb6789ae29d6bfcc3cf3a6f7d9fda languageName: node linkType: hard @@ -7834,7 +7785,7 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^3.0.0": +"normalize-package-data@npm:^3.0.0, normalize-package-data@npm:^3.0.3": version: 3.0.3 resolution: "normalize-package-data@npm:3.0.3" dependencies: @@ -7889,30 +7840,6 @@ __metadata: languageName: node linkType: hard -"npmlog@npm:^5.0.1": - version: 5.0.1 - resolution: "npmlog@npm:5.0.1" - dependencies: - are-we-there-yet: "npm:^2.0.0" - console-control-strings: "npm:^1.1.0" - gauge: "npm:^3.0.0" - set-blocking: "npm:^2.0.0" - checksum: 10/f42c7b9584cdd26a13c41a21930b6f5912896b6419ab15be88cc5721fc792f1c3dd30eb602b26ae08575694628ba70afdcf3675d86e4f450fc544757e52726ec - languageName: node - linkType: hard - -"npmlog@npm:^6.0.0": - version: 6.0.2 - resolution: "npmlog@npm:6.0.2" - dependencies: - are-we-there-yet: "npm:^3.0.0" - console-control-strings: "npm:^1.1.0" - gauge: "npm:^4.0.3" - set-blocking: "npm:^2.0.0" - checksum: 10/82b123677e62deb9e7472e27b92386c09e6e254ee6c8bcd720b3011013e4168bc7088e984f4fbd53cb6e12f8b4690e23e4fa6132689313e0d0dc4feea45489bb - languageName: node - linkType: hard - "ntp-client@npm:^0.5.3": version: 0.5.3 resolution: "ntp-client@npm:0.5.3" @@ -7922,13 +7849,6 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4.1.1": - version: 4.1.1 - resolution: "object-assign@npm:4.1.1" - checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f - languageName: node - linkType: hard - "object-filter-sequence@npm:^1.0.0": version: 1.0.0 resolution: "object-filter-sequence@npm:1.0.0" @@ -8006,7 +7926,7 @@ __metadata: languageName: node linkType: hard -"on-finished@npm:^2.3.0, on-finished@npm:~2.4.1": +"on-finished@npm:^2.3.0, on-finished@npm:^2.4.1, on-finished@npm:~2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" dependencies: @@ -8207,12 +8127,10 @@ __metadata: languageName: node linkType: hard -"p-map@npm:^4.0.0": - version: 4.0.0 - resolution: "p-map@npm:4.0.0" - dependencies: - aggregate-error: "npm:^3.0.0" - checksum: 10/7ba4a2b1e24c05e1fc14bbaea0fc6d85cf005ae7e9c9425d4575550f37e2e584b1af97bcde78eacd7559208f20995988d52881334db16cf77bc1bcf68e48ed7c +"p-map@npm:^7.0.2": + version: 7.0.4 + resolution: "p-map@npm:7.0.4" + checksum: 10/ef48c3b2e488f31c693c9fcc0df0ef76518cf6426a495cf9486ebbb0fd7f31aef7f90e96f72e0070c0ff6e3177c9318f644b512e2c29e3feee8d7153fcb6782e languageName: node linkType: hard @@ -8342,6 +8260,13 @@ __metadata: languageName: node linkType: hard +"path-expression-matcher@npm:^1.1.3": + version: 1.1.3 + resolution: "path-expression-matcher@npm:1.1.3" + checksum: 10/9a607d0bf9807cf86b0a29fb4263f0c00285c13bedafb6ad3efc8bc87ae878da2faf657a9138ac918726cb19f147235a0ca695aec3e4ea1ee04641b6520e6c9e + languageName: node + linkType: hard + "path-is-absolute@npm:1.0.1, path-is-absolute@npm:^1.0.0": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" @@ -8383,10 +8308,10 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:^6.3.0": - version: 6.3.0 - resolution: "path-to-regexp@npm:6.3.0" - checksum: 10/6822f686f01556d99538b350722ef761541ec0ce95ca40ce4c29e20a5b492fe8361961f57993c71b2418de12e604478dcf7c430de34b2c31a688363a7a944d9c +"path-to-regexp@npm:^8.3.0": + version: 8.3.0 + resolution: "path-to-regexp@npm:8.3.0" + checksum: 10/568f148fc64f5fd1ecebf44d531383b28df924214eabf5f2570dce9587a228e36c37882805ff02d71c6209b080ea3ee6a4d2b712b5df09741b67f1f3cf91e55a languageName: node linkType: hard @@ -8427,13 +8352,20 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.0.4, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10/60c2595003b05e4535394d1da94850f5372c9427ca4413b71210f437f7b2ca091dbd611c45e8b37d10036fa8eade25c1b8951654f9d3973bfa66a2ff4d3b08bc languageName: node linkType: hard +"picomatch@npm:^4.0.2, picomatch@npm:^4.0.3": + version: 4.0.3 + resolution: "picomatch@npm:4.0.3" + checksum: 10/57b99055f40b16798f2802916d9c17e9744e620a0db136554af01d19598b96e45e2f00014c91d1b8b13874b80caa8c295b3d589a3f72373ec4aaf54baa5962d5 + languageName: node + linkType: hard + "pify@npm:^2.3.0": version: 2.3.0 resolution: "pify@npm:2.3.0" @@ -8486,10 +8418,10 @@ __metadata: languageName: node linkType: hard -"pirates@npm:^4.0.4": - version: 4.0.6 - resolution: "pirates@npm:4.0.6" - checksum: 10/d02dda76f4fec1cbdf395c36c11cf26f76a644f9f9a1bfa84d3167d0d3154d5289aacc72677aa20d599bb4a6937a471de1b65c995e2aea2d8687cbcd7e43ea5f +"pirates@npm:^4.0.7": + version: 4.0.7 + resolution: "pirates@npm:4.0.7" + checksum: 10/2427f371366081ae42feb58214f04805d6b41d6b84d74480ebcc9e0ddbd7105a139f7c653daeaf83ad8a1a77214cf07f64178e76de048128fec501eab3305a96 languageName: node linkType: hard @@ -8525,23 +8457,30 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.4.2": - version: 3.4.2 - resolution: "prettier@npm:3.4.2" +"prettier@npm:^3.8.1": + version: 3.8.1 + resolution: "prettier@npm:3.8.1" bin: prettier: bin/prettier.cjs - checksum: 10/a3e806fb0b635818964d472d35d27e21a4e17150c679047f5501e1f23bd4aa806adf660f0c0d35214a210d5d440da6896c2e86156da55f221a57938278dc326e + checksum: 10/3da1cf8c1ef9bea828aa618553696c312e951f810bee368f6887109b203f18ee869fe88f66e65f9cf60b7cb1f2eae859892c860a300c062ff8ec69c381fc8dbd languageName: node linkType: hard -"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": - version: 29.7.0 - resolution: "pretty-format@npm:29.7.0" +"pretty-format@npm:30.2.0, pretty-format@npm:^30.0.0": + version: 30.2.0 + resolution: "pretty-format@npm:30.2.0" dependencies: - "@jest/schemas": "npm:^29.6.3" - ansi-styles: "npm:^5.0.0" - react-is: "npm:^18.0.0" - checksum: 10/dea96bc83c83cd91b2bfc55757b6b2747edcaac45b568e46de29deee80742f17bc76fe8898135a70d904f4928eafd8bb693cd1da4896e8bdd3c5e82cadf1d2bb + "@jest/schemas": "npm:30.0.5" + ansi-styles: "npm:^5.2.0" + react-is: "npm:^18.3.1" + checksum: 10/725890d648e3400575eebc99a334a4cd1498e0d36746313913706bbeea20ada27e17c184a3cd45c50f705c16111afa829f3450233fc0fda5eed293c69757e926 + languageName: node + linkType: hard + +"proc-log@npm:^6.0.0": + version: 6.1.0 + resolution: "proc-log@npm:6.1.0" + checksum: 10/9033f30f168ed5a0991b773d0c50ff88384c4738e9a0a67d341de36bf7293771eed648ab6a0562f62276da12fde91f3bbfc75ffff6e71ad49aafd74fc646be66 languageName: node linkType: hard @@ -8576,13 +8515,6 @@ __metadata: languageName: node linkType: hard -"promise-inflight@npm:^1.0.1": - version: 1.0.1 - resolution: "promise-inflight@npm:1.0.1" - checksum: 10/1560d413ea20c5a74f3631d39ba8cbd1972b9228072a755d01e1f5ca5110382d9af76a1582d889445adc6e75bb5ac4886b56dc4b6eae51b30145d7bb1ac7505b - languageName: node - linkType: hard - "promise-retry@npm:^2.0.1": version: 2.0.1 resolution: "promise-retry@npm:2.0.1" @@ -8593,16 +8525,6 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.0.1": - version: 2.4.2 - resolution: "prompts@npm:2.4.2" - dependencies: - kleur: "npm:^3.0.3" - sisteransi: "npm:^1.0.5" - checksum: 10/c52536521a4d21eff4f2f2aa4572446cad227464066365a7167e52ccf8d9839c099f9afec1aba0eed3d5a2514b3e79e0b3e7a1dc326b9acde6b75d27ed74b1a9 - languageName: node - linkType: hard - "proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" @@ -8638,21 +8560,14 @@ __metadata: languageName: node linkType: hard -"pure-rand@npm:^6.0.0": - version: 6.0.3 - resolution: "pure-rand@npm:6.0.3" - checksum: 10/68e6ebbc918d0022870cc436c26fd07b8ae6a71acc9aa83145d6e2ec0022e764926cbffc70c606fd25213c3b7234357d10458939182fb6568c2a364d1098cf34 - languageName: node - linkType: hard - -"q@npm:^1.5.1": - version: 1.5.1 - resolution: "q@npm:1.5.1" - checksum: 10/70c4a30b300277165cd855889cd3aa681929840a5940413297645c5691e00a3549a2a4153131efdf43fe8277ee8cf5a34c9636dcb649d83ad47f311a015fd380 +"pure-rand@npm:^7.0.0": + version: 7.0.1 + resolution: "pure-rand@npm:7.0.1" + checksum: 10/c61a576fda5032ec9763ecb000da4a8f19263b9e2f9ae9aa2759c8fbd9dc6b192b2ce78391ebd41abb394a5fedb7bcc4b03c9e6141ac8ab20882dd5717698b80 languageName: node linkType: hard -"qs@npm:^6.12.3, qs@npm:^6.5.2, qs@npm:~6.14.0": +"qs@npm:^6.12.3, qs@npm:^6.5.2": version: 6.14.1 resolution: "qs@npm:6.14.1" dependencies: @@ -8661,6 +8576,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:~6.14.0": + version: 6.14.2 + resolution: "qs@npm:6.14.2" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10/682933a85bb4b7bd0d66e13c0a40d9e612b5e4bcc2cb9238f711a9368cd22d91654097a74fff93551e58146db282c56ac094957dfdc60ce64ea72c3c9d7779ac + languageName: node + linkType: hard + "querystring-es3@npm:^0.2.1": version: 0.2.1 resolution: "querystring-es3@npm:0.2.1" @@ -8741,10 +8665,10 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^18.0.0": - version: 18.2.0 - resolution: "react-is@npm:18.2.0" - checksum: 10/200cd65bf2e0be7ba6055f647091b725a45dd2a6abef03bf2380ce701fd5edccee40b49b9d15edab7ac08a762bf83cb4081e31ec2673a5bfb549a36ba21570df +"react-is@npm:^18.3.1": + version: 18.3.1 + resolution: "react-is@npm:18.3.1" + checksum: 10/d5f60c87d285af24b1e1e7eaeb123ec256c3c8bdea7061ab3932e3e14685708221bf234ec50b21e10dd07f008f1b966a2730a0ce4ff67905b3872ff2042aec22 languageName: node linkType: hard @@ -8962,14 +8886,13 @@ __metadata: languageName: node linkType: hard -"require-in-the-middle@npm:^7.1.1": - version: 7.2.0 - resolution: "require-in-the-middle@npm:7.2.0" +"require-in-the-middle@npm:^8.0.0": + version: 8.0.1 + resolution: "require-in-the-middle@npm:8.0.1" dependencies: - debug: "npm:^4.1.1" + debug: "npm:^4.3.5" module-details-from-path: "npm:^1.0.3" - resolve: "npm:^1.22.1" - checksum: 10/f77f865d5f689d8cada40c9bb947a86d2992b34ee9d3b98aaa7f643acd101ede624e5fe3e9200103900f6b772af4277ef97d08a9332160c895861dc3f801be67 + checksum: 10/4ce98c681489d383a0ffccb79b06df7a1dffbb31c13f3b713ae2c5a1967597a259e67612507ef69748d83d531bba7c9bb0477211771fe78c685e1d52b1a44b64 languageName: node linkType: hard @@ -9029,14 +8952,7 @@ __metadata: languageName: node linkType: hard -"resolve.exports@npm:^2.0.0": - version: 2.0.2 - resolution: "resolve.exports@npm:2.0.2" - checksum: 10/f1cc0b6680f9a7e0345d783e0547f2a5110d8336b3c2a4227231dd007271ffd331fd722df934f017af90bae0373920ca0d4005da6f76cb3176c8ae426370f893 - languageName: node - linkType: hard - -"resolve@npm:^1.10.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1": +"resolve@npm:^1.10.0": version: 1.22.6 resolution: "resolve@npm:1.22.6" dependencies: @@ -9049,7 +8965,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin": +"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin": version: 1.22.6 resolution: "resolve@patch:resolve@npm%3A1.22.6#optional!builtin::version=1.22.6&hash=c3c19d" dependencies: @@ -9076,17 +8992,6 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^3.0.2": - version: 3.0.2 - resolution: "rimraf@npm:3.0.2" - dependencies: - glob: "npm:^7.1.3" - bin: - rimraf: bin.js - checksum: 10/063ffaccaaaca2cfd0ef3beafb12d6a03dd7ff1260d752d62a6077b5dfff6ae81bea571f655bb6b589d366930ec1bdd285d40d560c0dae9b12f125e54eb743d5 - languageName: node - linkType: hard - "ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1, ripemd160@npm:^2.0.3": version: 2.0.3 resolution: "ripemd160@npm:2.0.3" @@ -9180,7 +9085,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": +"semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -9189,19 +9094,21 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.1.1, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3": - version: 7.7.1 - resolution: "semver@npm:7.7.1" +"semver@npm:^7.0.0, semver@npm:^7.7.2, semver@npm:^7.7.3": + version: 7.7.3 + resolution: "semver@npm:7.7.3" bin: semver: bin/semver.js - checksum: 10/4cfa1eb91ef3751e20fc52e47a935a0118d56d6f15a837ab814da0c150778ba2ca4f1a4d9068b33070ea4273629e615066664c2cfcd7c272caf7a8a0f6518b2c + checksum: 10/8dbc3168e057a38fc322af909c7f5617483c50caddba135439ff09a754b20bdd6482a5123ff543dad4affa488ecf46ec5fb56d61312ad20bb140199b88dfaea9 languageName: node linkType: hard -"set-blocking@npm:^2.0.0": - version: 2.0.0 - resolution: "set-blocking@npm:2.0.0" - checksum: 10/8980ebf7ae9eb945bb036b6e283c547ee783a1ad557a82babf758a065e2fb6ea337fd82cac30dd565c1e606e423f30024a19fff7afbf4977d784720c4026a8ef +"semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3": + version: 7.7.1 + resolution: "semver@npm:7.7.1" + bin: + semver: bin/semver.js + checksum: 10/4cfa1eb91ef3751e20fc52e47a935a0118d56d6f15a837ab814da0c150778ba2ca4f1a4d9068b33070ea4273629e615066664c2cfcd7c272caf7a8a0f6518b2c languageName: node linkType: hard @@ -9251,7 +9158,19 @@ __metadata: languageName: node linkType: hard -"sha.js@npm:^2.4.0, sha.js@npm:^2.4.12, sha.js@npm:^2.4.8": +"sha.js@npm:^2.4.0, sha.js@npm:^2.4.8": + version: 2.4.11 + resolution: "sha.js@npm:2.4.11" + dependencies: + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + bin: + sha.js: ./bin.js + checksum: 10/d833bfa3e0a67579a6ce6e1bc95571f05246e0a441dd8c76e3057972f2a3e098465687a4369b07e83a0375a88703577f71b5b2e966809e67ebc340dbedb478c7 + languageName: node + linkType: hard + +"sha.js@npm:^2.4.12": version: 2.4.12 resolution: "sha.js@npm:2.4.12" dependencies: @@ -9344,7 +9263,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.3": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: 10/a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -9358,22 +9277,6 @@ __metadata: languageName: node linkType: hard -"simple-swizzle@npm:^0.2.2": - version: 0.2.2 - resolution: "simple-swizzle@npm:0.2.2" - dependencies: - is-arrayish: "npm:^0.3.1" - checksum: 10/c6dffff17aaa383dae7e5c056fbf10cf9855a9f79949f20ee225c04f06ddde56323600e0f3d6797e82d08d006e93761122527438ee9531620031c08c9e0d73cc - languageName: node - linkType: hard - -"sisteransi@npm:^1.0.5": - version: 1.0.5 - resolution: "sisteransi@npm:1.0.5" - checksum: 10/aba6438f46d2bfcef94cf112c835ab395172c75f67453fe05c340c770d3c402363018ae1ab4172a1026a90c47eaccf3af7b6ff6fa749a680c2929bd7fa2b37a4 - languageName: node - linkType: hard - "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -9395,24 +9298,24 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^7.0.0": - version: 7.0.0 - resolution: "socks-proxy-agent@npm:7.0.0" +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.5 + resolution: "socks-proxy-agent@npm:8.0.5" dependencies: - agent-base: "npm:^6.0.2" - debug: "npm:^4.3.3" - socks: "npm:^2.6.2" - checksum: 10/26c75d9c62a9ed3fd494df60e65e88da442f78e0d4bc19bfd85ac37bd2c67470d6d4bba5202e804561cda6674db52864c9e2a2266775f879bc8d89c1445a5f4c + agent-base: "npm:^7.1.2" + debug: "npm:^4.3.4" + socks: "npm:^2.8.3" + checksum: 10/ee99e1dacab0985b52cbe5a75640be6e604135e9489ebdc3048635d186012fbaecc20fbbe04b177dee434c319ba20f09b3e7dfefb7d932466c0d707744eac05c languageName: node linkType: hard -"socks@npm:^2.6.2": - version: 2.7.1 - resolution: "socks@npm:2.7.1" +"socks@npm:^2.8.3": + version: 2.8.7 + resolution: "socks@npm:2.8.7" dependencies: - ip: "npm:^2.0.0" + ip-address: "npm:^10.0.1" smart-buffer: "npm:^4.2.0" - checksum: 10/5074f7d6a13b3155fa655191df1c7e7a48ce3234b8ccf99afa2ccb56591c195e75e8bb78486f8e9ea8168e95a29573cbaad55b2b5e195160ae4d2ea6811ba833 + checksum: 10/d19366c95908c19db154f329bbe94c2317d315dc933a7c2b5101e73f32a555c84fb199b62174e1490082a593a4933d8d5a9b297bde7d1419c14a11a965f51356 languageName: node linkType: hard @@ -9530,7 +9433,7 @@ __metadata: languageName: node linkType: hard -"split2@npm:^3.0.0": +"split2@npm:^3.2.2": version: 3.2.2 resolution: "split2@npm:3.2.2" dependencies: @@ -9546,7 +9449,7 @@ __metadata: languageName: node linkType: hard -"split@npm:^1.0.0": +"split@npm:^1.0.1": version: 1.0.1 resolution: "split@npm:1.0.1" dependencies: @@ -9569,21 +9472,12 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^10.0.0": - version: 10.0.5 - resolution: "ssri@npm:10.0.5" +"ssri@npm:^13.0.0": + version: 13.0.0 + resolution: "ssri@npm:13.0.0" dependencies: minipass: "npm:^7.0.3" - checksum: 10/453f9a1c241c13f5dfceca2ab7b4687bcff354c3ccbc932f35452687b9ef0ccf8983fd13b8a3baa5844c1a4882d6e3ddff48b0e7fd21d743809ef33b80616d79 - languageName: node - linkType: hard - -"ssri@npm:^9.0.0": - version: 9.0.1 - resolution: "ssri@npm:9.0.1" - dependencies: - minipass: "npm:^3.1.1" - checksum: 10/7638a61e91432510718e9265d48d0438a17d53065e5184f1336f234ef6aa3479663942e41e97df56cda06bb24d9d0b5ef342c10685add3cac7267a82d7fa6718 + checksum: 10/fd59bfedf0659c1b83f6e15459162da021f08ec0f5834dd9163296f8b77ee82f9656aa1d415c3d3848484293e0e6aefdd482e863e52ddb53d520bb73da1eeec1 languageName: node linkType: hard @@ -9594,7 +9488,7 @@ __metadata: languageName: node linkType: hard -"stack-utils@npm:^2.0.3": +"stack-utils@npm:^2.0.6": version: 2.0.6 resolution: "stack-utils@npm:2.0.6" dependencies: @@ -9610,30 +9504,6 @@ __metadata: languageName: node linkType: hard -"standard-version@npm:^9.5.0": - version: 9.5.0 - resolution: "standard-version@npm:9.5.0" - dependencies: - chalk: "npm:^2.4.2" - conventional-changelog: "npm:3.1.25" - conventional-changelog-config-spec: "npm:2.1.0" - conventional-changelog-conventionalcommits: "npm:4.6.3" - conventional-recommended-bump: "npm:6.1.0" - detect-indent: "npm:^6.0.0" - detect-newline: "npm:^3.1.0" - dotgitignore: "npm:^2.1.0" - figures: "npm:^3.1.0" - find-up: "npm:^5.0.0" - git-semver-tags: "npm:^4.0.0" - semver: "npm:^7.1.1" - stringify-package: "npm:^1.0.1" - yargs: "npm:^16.0.0" - bin: - standard-version: bin/cli.js - checksum: 10/a59fc3a3046007d376bf164b053011db8f6c1417b3512db697e36ea574ec47fca55086513f602ba237c62a2e61f4c60480eb84793fd0450a715bac9dd8634aa2 - languageName: node - linkType: hard - "statuses@npm:>= 1.4.0 < 2, statuses@npm:>= 1.5.0 < 2, statuses@npm:^1.5.0": version: 1.5.0 resolution: "statuses@npm:1.5.0" @@ -9641,7 +9511,7 @@ __metadata: languageName: node linkType: hard -"statuses@npm:~2.0.2": +"statuses@npm:^2.0.1, statuses@npm:~2.0.2": version: 2.0.2 resolution: "statuses@npm:2.0.2" checksum: 10/6927feb50c2a75b2a4caab2c565491f7a93ad3d8dbad7b1398d52359e9243a20e2ebe35e33726dee945125ef7a515e9097d8a1b910ba2bbd818265a2f6c39879 @@ -9698,7 +9568,7 @@ __metadata: languageName: node linkType: hard -"string-length@npm:^4.0.1": +"string-length@npm:^4.0.2": version: 4.0.2 resolution: "string-length@npm:4.0.2" dependencies: @@ -9708,7 +9578,7 @@ __metadata: languageName: node linkType: hard -"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -9781,13 +9651,6 @@ __metadata: languageName: node linkType: hard -"stringify-package@npm:^1.0.1": - version: 1.0.1 - resolution: "stringify-package@npm:1.0.1" - checksum: 10/462036085a0cf7ae073d9b88a2bbf7efb3792e3df3e1fd436851f64196eb0234c8f8ffac436357e355687d6030b7af42e98af9515929e41a6a5c8653aa62a5aa - languageName: node - linkType: hard - "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -9798,11 +9661,11 @@ __metadata: linkType: hard "strip-ansi@npm:^7.0.1": - version: 7.1.0 - resolution: "strip-ansi@npm:7.1.0" + version: 7.1.2 + resolution: "strip-ansi@npm:7.1.2" dependencies: ansi-regex: "npm:^6.0.1" - checksum: 10/475f53e9c44375d6e72807284024ac5d668ee1d06010740dec0b9744f2ddf47de8d7151f80e5f6190fc8f384e802fdf9504b76a7e9020c9faee7103623338be2 + checksum: 10/db0e3f9654e519c8a33c50fc9304d07df5649388e7da06d3aabf66d29e5ad65d5e6315d8519d409c15b32fa82c1df7e11ed6f8cd50b0e4404463f0c9d77c8d0b languageName: node linkType: hard @@ -9843,6 +9706,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^2.1.2": + version: 2.2.0 + resolution: "strnum@npm:2.2.0" + checksum: 10/2969dbc8441f5af1b55db1d2fcea64a8f912de18515b57f85574e66bdb8f30ae76c419cf1390b343d72d687e2aea5aca82390f18b9e0de45d6bcc6d605eb9385 + languageName: node + linkType: hard + "strtok3@npm:^7.0.0": version: 7.0.0 resolution: "strtok3@npm:7.0.0" @@ -9880,7 +9750,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^8.0.0": +"supports-color@npm:^8.1.1": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -9896,6 +9766,15 @@ __metadata: languageName: node linkType: hard +"synckit@npm:^0.11.8": + version: 0.11.12 + resolution: "synckit@npm:0.11.12" + dependencies: + "@pkgr/core": "npm:^0.2.9" + checksum: 10/2f51978bfed81aaf0b093f596709a72c49b17909020f42b43c5549f9c0fe18b1fe29f82e41ef771172d729b32e9ce82900a85d2b87fa14d59f886d4df8d7a329 + languageName: node + linkType: hard + "synckit@npm:^0.9.1": version: 0.9.2 resolution: "synckit@npm:0.9.2" @@ -9913,17 +9792,16 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.2.0 - resolution: "tar@npm:6.2.0" +"tar@npm:^7.5.4": + version: 7.5.11 + resolution: "tar@npm:7.5.11" dependencies: - chownr: "npm:^2.0.0" - fs-minipass: "npm:^2.0.0" - minipass: "npm:^5.0.0" - minizlib: "npm:^2.1.1" - mkdirp: "npm:^1.0.3" - yallist: "npm:^4.0.0" - checksum: 10/2042bbb14830b5cd0d584007db0eb0a7e933e66d1397e72a4293768d2332449bc3e312c266a0887ec20156dea388d8965e53b4fc5097f42d78593549016da089 + "@isaacs/fs-minipass": "npm:^4.0.0" + chownr: "npm:^3.0.0" + minipass: "npm:^7.1.2" + minizlib: "npm:^3.1.0" + yallist: "npm:^5.0.0" + checksum: 10/fb2e77ee858a73936c68e066f4a602d428d6f812e6da0cc1e14a41f99498e4f7fd3535e355fa15157240a5538aa416026cfa6306bb0d1d1c1abf314b1f878e9a languageName: node linkType: hard @@ -9998,15 +9876,15 @@ __metadata: languageName: node linkType: hard -"threadedclass@npm:^1.2.2": - version: 1.2.2 - resolution: "threadedclass@npm:1.2.2" +"threadedclass@npm:^1.3.0": + version: 1.3.0 + resolution: "threadedclass@npm:1.3.0" dependencies: callsites: "npm:^3.1.0" eventemitter3: "npm:^4.0.4" is-running: "npm:^2.1.0" tslib: "npm:^1.13.0" - checksum: 10/0965f2b4a3350c0a9522bb02bcaf3be7ae12f8c3b3e54d846bf650ea16ac2af1e1b99ed105b889c77a381ee8984b826be25c02688121d9ba303fdec5de421ab6 + checksum: 10/9e048e82ee745ee2009dabb0015330fa9d4f4d83629c799c6059f77a6a1c6a8b0392e6e8c2a28834a88532be6b86ac276cf1f0133a855ea867b0217021350043 languageName: node linkType: hard @@ -10043,12 +9921,13 @@ __metadata: languageName: node linkType: hard -"timeline-state-resolver-types@npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0": - version: 10.0.0-nightly-release53-20251217-143607-df590aa96.0 - resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" +"timeline-state-resolver-types@npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0": + version: 10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0 + resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0" dependencies: + kairos-lib: "npm:0.2.3" tslib: "npm:^2.8.1" - checksum: 10/6f17030e9f10568757b3ebaae40a54ce5220ce5c2f37d9b5d8a5d286704e4779c80fbfddae500e1952a6efdf6e76b67bc18d0151211c84eb194ae3b52f78c7bf + checksum: 10/5558f4a80ebc60f3f86e76172965fce0f415cfd76fd109f0c84ad6873521a1dbe054a60e1c8d87535b2e16df3be25e637099742b38628be9c794f1701762903a languageName: node linkType: hard @@ -10061,6 +9940,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.12": + version: 0.2.15 + resolution: "tinyglobby@npm:0.2.15" + dependencies: + fdir: "npm:^6.5.0" + picomatch: "npm:^4.0.3" + checksum: 10/d72bd826a8b0fa5fa3929e7fe5ba48fceb2ae495df3a231b6c5408cd7d8c00b58ab5a9c2a76ba56a62ee9b5e083626f1f33599734bed1ffc4b792406408f0ca2 + languageName: node + linkType: hard + "tmpl@npm:1.0.5": version: 1.0.5 resolution: "tmpl@npm:1.0.5" @@ -10132,13 +10021,6 @@ __metadata: languageName: node linkType: hard -"tr46@npm:~0.0.3": - version: 0.0.3 - resolution: "tr46@npm:0.0.3" - checksum: 10/8f1f5aa6cb232f9e1bdc86f485f916b7aa38caee8a778b378ffec0b70d9307873f253f5cbadbe2955ece2ac5c83d0dc14a77513166ccd0a0c7fe197e21396695 - languageName: node - linkType: hard - "treeify@npm:^1.1.0": version: 1.1.0 resolution: "treeify@npm:1.1.0" @@ -10169,25 +10051,26 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:^29.2.5": - version: 29.2.5 - resolution: "ts-jest@npm:29.2.5" +"ts-jest@npm:^29.4.6": + version: 29.4.6 + resolution: "ts-jest@npm:29.4.6" dependencies: bs-logger: "npm:^0.2.6" - ejs: "npm:^3.1.10" fast-json-stable-stringify: "npm:^2.1.0" - jest-util: "npm:^29.0.0" + handlebars: "npm:^4.7.8" json5: "npm:^2.2.3" lodash.memoize: "npm:^4.1.2" make-error: "npm:^1.3.6" - semver: "npm:^7.6.3" + semver: "npm:^7.7.3" + type-fest: "npm:^4.41.0" yargs-parser: "npm:^21.1.1" peerDependencies: "@babel/core": ">=7.0.0-beta.0 <8" - "@jest/transform": ^29.0.0 - "@jest/types": ^29.0.0 - babel-jest: ^29.0.0 - jest: ^29.0.0 + "@jest/transform": ^29.0.0 || ^30.0.0 + "@jest/types": ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 typescript: ">=4.3 <6" peerDependenciesMeta: "@babel/core": @@ -10200,9 +10083,11 @@ __metadata: optional: true esbuild: optional: true + jest-util: + optional: true bin: ts-jest: cli.js - checksum: 10/f89e562816861ec4510840a6b439be6145f688b999679328de8080dc8e66481325fc5879519b662163e33b7578f35243071c38beb761af34e5fe58e3e326a958 + checksum: 10/e0ff9e13f684166d5331808b288043b8054f49a1c2970480a92ba3caec8d0ff20edd092f2a4e7a3ad8fcb9ba4d674bee10ec7ee75046d8066bbe43a7d16cf72e languageName: node linkType: hard @@ -10213,7 +10098,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.6.0, tslib@npm:^2.6.2, tslib@npm:^2.8.1": +"tslib@npm:^2.4.0, tslib@npm:^2.6.0, tslib@npm:^2.6.2, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 @@ -10292,10 +10177,10 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^4.33.0, type-fest@npm:^4.6.0, type-fest@npm:^4.7.1": - version: 4.33.0 - resolution: "type-fest@npm:4.33.0" - checksum: 10/0d179e66fa765bd0a25a785b12dc797f90f2f92bdb8c9c8a789f3fd8e5a4492444e7ef83551b3b8463aeab24fd6195761e26b03174722de636b4b75aa5726fb7 +"type-fest@npm:^4.41.0, type-fest@npm:^4.6.0, type-fest@npm:^4.7.1": + version: 4.41.0 + resolution: "type-fest@npm:4.41.0" + checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212 languageName: node linkType: hard @@ -10309,6 +10194,17 @@ __metadata: languageName: node linkType: hard +"type-is@npm:^2.0.1": + version: 2.0.1 + resolution: "type-is@npm:2.0.1" + dependencies: + content-type: "npm:^1.0.5" + media-typer: "npm:^1.1.0" + mime-types: "npm:^3.0.0" + checksum: 10/bacdb23c872dacb7bd40fbd9095e6b2fca2895eedbb689160c05534d7d4810a7f4b3fd1ae87e96133c505958f6d602967a68db5ff577b85dd6be76eaa75d58af + languageName: node + linkType: hard + "typed-array-buffer@npm:^1.0.0, typed-array-buffer@npm:^1.0.3": version: 1.0.3 resolution: "typed-array-buffer@npm:1.0.3" @@ -10419,16 +10315,16 @@ __metadata: linkType: hard "underscore@npm:^1.13.7": - version: 1.13.7 - resolution: "underscore@npm:1.13.7" - checksum: 10/1ce3368dbe73d1e99678fa5d341a9682bd27316032ad2de7883901918f0f5d50e80320ccc543f53c1862ab057a818abc560462b5f83578afe2dd8dd7f779766c + version: 1.13.8 + resolution: "underscore@npm:1.13.8" + checksum: 10/b50ac5806d059cc180b1bd9adea6f7ed500021f4dc782dfc75d66a90337f6f0506623c1b37863f4a9bf64ffbeb5769b638a54b7f2f5966816189955815953139 languageName: node linkType: hard -"undici-types@npm:~6.20.0": - version: 6.20.0 - resolution: "undici-types@npm:6.20.0" - checksum: 10/583ac7bbf4ff69931d3985f4762cde2690bb607844c16a5e2fbb92ed312fe4fa1b365e953032d469fa28ba8b224e88a595f0b10a449332f83fa77c695e567dbe +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10/ec8f41aa4359d50f9b59fa61fe3efce3477cc681908c8f84354d8567bb3701fafdddf36ef6bff307024d3feb42c837cf6f670314ba37fc8145e219560e473d14 languageName: node linkType: hard @@ -10456,39 +10352,21 @@ __metadata: languageName: node linkType: hard -"unique-filename@npm:^2.0.0": - version: 2.0.1 - resolution: "unique-filename@npm:2.0.1" - dependencies: - unique-slug: "npm:^3.0.0" - checksum: 10/807acf3381aff319086b64dc7125a9a37c09c44af7620bd4f7f3247fcd5565660ac12d8b80534dcbfd067e6fe88a67e621386dd796a8af828d1337a8420a255f - languageName: node - linkType: hard - -"unique-filename@npm:^3.0.0": - version: 3.0.0 - resolution: "unique-filename@npm:3.0.0" - dependencies: - unique-slug: "npm:^4.0.0" - checksum: 10/8e2f59b356cb2e54aab14ff98a51ac6c45781d15ceaab6d4f1c2228b780193dc70fae4463ce9e1df4479cb9d3304d7c2043a3fb905bdeca71cc7e8ce27e063df - languageName: node - linkType: hard - -"unique-slug@npm:^3.0.0": - version: 3.0.0 - resolution: "unique-slug@npm:3.0.0" +"unique-filename@npm:^5.0.0": + version: 5.0.0 + resolution: "unique-filename@npm:5.0.0" dependencies: - imurmurhash: "npm:^0.1.4" - checksum: 10/26fc5bc209a875956dd5e84ca39b89bc3be777b112504667c35c861f9547df95afc80439358d836b878b6d91f6ee21fe5ba1a966e9ec2e9f071ddf3fd67d45ee + unique-slug: "npm:^6.0.0" + checksum: 10/a5f67085caef74bdd2a6869a200ed5d68d171f5cc38435a836b5fd12cce4e4eb55e6a190298035c325053a5687ed7a3c96f0a91e82215fd14729769d9ac57d9b languageName: node linkType: hard -"unique-slug@npm:^4.0.0": - version: 4.0.0 - resolution: "unique-slug@npm:4.0.0" +"unique-slug@npm:^6.0.0": + version: 6.0.0 + resolution: "unique-slug@npm:6.0.0" dependencies: imurmurhash: "npm:^0.1.4" - checksum: 10/40912a8963fc02fb8b600cf50197df4a275c602c60de4cac4f75879d3c48558cfac48de08a25cc10df8112161f7180b3bbb4d662aadb711568602f9eddee54f0 + checksum: 10/b78ed9d5b01ff465f80975f17387750ed3639909ac487fa82c4ae4326759f6de87c2131c0c39eca4c68cf06c537a8d104fba1dfc8a30308f99bc505345e1eba3 languageName: node linkType: hard @@ -10508,6 +10386,73 @@ __metadata: languageName: node linkType: hard +"unrs-resolver@npm:^1.7.11": + version: 1.11.1 + resolution: "unrs-resolver@npm:1.11.1" + dependencies: + "@unrs/resolver-binding-android-arm-eabi": "npm:1.11.1" + "@unrs/resolver-binding-android-arm64": "npm:1.11.1" + "@unrs/resolver-binding-darwin-arm64": "npm:1.11.1" + "@unrs/resolver-binding-darwin-x64": "npm:1.11.1" + "@unrs/resolver-binding-freebsd-x64": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm-gnueabihf": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm-musleabihf": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm64-musl": "npm:1.11.1" + "@unrs/resolver-binding-linux-ppc64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-riscv64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-riscv64-musl": "npm:1.11.1" + "@unrs/resolver-binding-linux-s390x-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-x64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-x64-musl": "npm:1.11.1" + "@unrs/resolver-binding-wasm32-wasi": "npm:1.11.1" + "@unrs/resolver-binding-win32-arm64-msvc": "npm:1.11.1" + "@unrs/resolver-binding-win32-ia32-msvc": "npm:1.11.1" + "@unrs/resolver-binding-win32-x64-msvc": "npm:1.11.1" + napi-postinstall: "npm:^0.3.0" + dependenciesMeta: + "@unrs/resolver-binding-android-arm-eabi": + optional: true + "@unrs/resolver-binding-android-arm64": + optional: true + "@unrs/resolver-binding-darwin-arm64": + optional: true + "@unrs/resolver-binding-darwin-x64": + optional: true + "@unrs/resolver-binding-freebsd-x64": + optional: true + "@unrs/resolver-binding-linux-arm-gnueabihf": + optional: true + "@unrs/resolver-binding-linux-arm-musleabihf": + optional: true + "@unrs/resolver-binding-linux-arm64-gnu": + optional: true + "@unrs/resolver-binding-linux-arm64-musl": + optional: true + "@unrs/resolver-binding-linux-ppc64-gnu": + optional: true + "@unrs/resolver-binding-linux-riscv64-gnu": + optional: true + "@unrs/resolver-binding-linux-riscv64-musl": + optional: true + "@unrs/resolver-binding-linux-s390x-gnu": + optional: true + "@unrs/resolver-binding-linux-x64-gnu": + optional: true + "@unrs/resolver-binding-linux-x64-musl": + optional: true + "@unrs/resolver-binding-wasm32-wasi": + optional: true + "@unrs/resolver-binding-win32-arm64-msvc": + optional: true + "@unrs/resolver-binding-win32-ia32-msvc": + optional: true + "@unrs/resolver-binding-win32-x64-msvc": + optional: true + checksum: 10/4de653508cbaae47883a896bd5cdfef0e5e87b428d62620d16fd35cd534beaebf08ebf0cf2f8b4922aa947b2fe745180facf6cc3f39ba364f7ce0f974cb06a70 + languageName: node + linkType: hard + "update-browserslist-db@npm:^1.1.1": version: 1.1.2 resolution: "update-browserslist-db@npm:1.1.2" @@ -10688,13 +10633,6 @@ __metadata: languageName: node linkType: hard -"webidl-conversions@npm:^3.0.0": - version: 3.0.1 - resolution: "webidl-conversions@npm:3.0.1" - checksum: 10/b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad - languageName: node - linkType: hard - "webidl-conversions@npm:^4.0.2": version: 4.0.2 resolution: "webidl-conversions@npm:4.0.2" @@ -10719,16 +10657,6 @@ __metadata: languageName: node linkType: hard -"whatwg-url@npm:^5.0.0": - version: 5.0.0 - resolution: "whatwg-url@npm:5.0.0" - dependencies: - tr46: "npm:~0.0.3" - webidl-conversions: "npm:^3.0.0" - checksum: 10/f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07 - languageName: node - linkType: hard - "whatwg-url@npm:^7.0.0": version: 7.1.0 resolution: "whatwg-url@npm:7.1.0" @@ -10753,7 +10681,20 @@ __metadata: languageName: node linkType: hard -"which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.16, which-typed-array@npm:^1.1.2": +"which-typed-array@npm:^1.1.11, which-typed-array@npm:^1.1.2": + version: 1.1.11 + resolution: "which-typed-array@npm:1.1.11" + dependencies: + available-typed-arrays: "npm:^1.0.5" + call-bind: "npm:^1.0.2" + for-each: "npm:^0.3.3" + gopd: "npm:^1.0.1" + has-tostringtag: "npm:^1.0.0" + checksum: 10/bc9e8690e71d6c64893c9d88a7daca33af45918861003013faf77574a6a49cc6194d32ca7826e90de341d2f9ef3ac9e3acbe332a8ae73cadf07f59b9c6c6ecad + languageName: node + linkType: hard + +"which-typed-array@npm:^1.1.16": version: 1.1.20 resolution: "which-typed-array@npm:1.1.20" dependencies: @@ -10768,7 +10709,7 @@ __metadata: languageName: node linkType: hard -"which@npm:^2.0.1, which@npm:^2.0.2": +"which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" dependencies: @@ -10779,12 +10720,14 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:^1.1.2, wide-align@npm:^1.1.5": - version: 1.1.5 - resolution: "wide-align@npm:1.1.5" +"which@npm:^6.0.0": + version: 6.0.0 + resolution: "which@npm:6.0.0" dependencies: - string-width: "npm:^1.0.2 || 2 || 3 || 4" - checksum: 10/d5f8027b9a8255a493a94e4ec1b74a27bff6679d5ffe29316a3215e4712945c84ef73ca4045c7e20ae7d0c72f5f57f296e04a4928e773d4276a2f1222e4c2e99 + isexe: "npm:^3.1.1" + bin: + node-which: bin/which.js + checksum: 10/df19b2cd8aac94b333fa29b42e8e371a21e634a742a3b156716f7752a5afe1d73fb5d8bce9b89326f453d96879e8fe626eb421e0117eb1a3ce9fd8c97f6b7db9 languageName: node linkType: hard @@ -10799,12 +10742,12 @@ __metadata: languageName: node linkType: hard -"winston@npm:^3.17.0": - version: 3.17.0 - resolution: "winston@npm:3.17.0" +"winston@npm:^3.19.0": + version: 3.19.0 + resolution: "winston@npm:3.19.0" dependencies: "@colors/colors": "npm:^1.6.0" - "@dabh/diagnostics": "npm:^2.0.2" + "@dabh/diagnostics": "npm:^2.0.8" async: "npm:^3.2.3" is-stream: "npm:^2.0.0" logform: "npm:^2.7.0" @@ -10814,7 +10757,7 @@ __metadata: stack-trace: "npm:0.0.x" triple-beam: "npm:^1.3.0" winston-transport: "npm:^4.9.0" - checksum: 10/220309a0ead36c1171158ab28cb9133f8597fba19c8c1c190df9329555530565b58f3af0037c1b80e0c49f7f9b6b3b01791d0c56536eb0be38678d36e316c2a3 + checksum: 10/8279e221d8017da601a725939d31d65de71504d8328051312a85b1b4d7ddc68634329f8d611fb1ff91cb797643409635f3e97ef5b4a650c587639e080af76b7b languageName: node linkType: hard @@ -10854,13 +10797,13 @@ __metadata: languageName: node linkType: hard -"write-file-atomic@npm:^4.0.2": - version: 4.0.2 - resolution: "write-file-atomic@npm:4.0.2" +"write-file-atomic@npm:^5.0.1": + version: 5.0.1 + resolution: "write-file-atomic@npm:5.0.1" dependencies: imurmurhash: "npm:^0.1.4" - signal-exit: "npm:^3.0.7" - checksum: 10/3be1f5508a46c190619d5386b1ac8f3af3dbe951ed0f7b0b4a0961eed6fc626bd84b50cf4be768dabc0a05b672f5d0c5ee7f42daa557b14415d18c3a13c7d246 + signal-exit: "npm:^4.0.1" + checksum: 10/648efddba54d478d0e4330ab6f239976df3b9752b123db5dc9405d9b5af768fa9d70ce60c52fdbe61d1200d24350bc4fbcbaf09288496c2be050de126bd95b7e languageName: node linkType: hard @@ -10910,6 +10853,22 @@ __metadata: languageName: node linkType: hard +"yallist@npm:^5.0.0": + version: 5.0.0 + resolution: "yallist@npm:5.0.0" + checksum: 10/1884d272d485845ad04759a255c71775db0fac56308764b4c77ea56a20d56679fad340213054c8c9c9c26fcfd4c4b2a90df993b7e0aaf3cdb73c618d1d1a802a + languageName: node + linkType: hard + +"yaml@npm:^2.6.0": + version: 2.8.2 + resolution: "yaml@npm:2.8.2" + bin: + yaml: bin.mjs + checksum: 10/4eab0074da6bc5a5bffd25b9b359cf7061b771b95d1b3b571852098380db3b1b8f96e0f1f354b56cc7216aa97cea25163377ccbc33a2e9ce00316fe8d02f4539 + languageName: node + linkType: hard + "yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.3": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" @@ -10924,7 +10883,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^16.0.0, yargs@npm:^16.2.0": +"yargs@npm:^16.2.0": version: 16.2.0 resolution: "yargs@npm:16.2.0" dependencies: @@ -10939,7 +10898,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^17.3.1, yargs@npm:^17.7.2": +"yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: diff --git a/package.json b/package.json index 4e521a0f7d7..f929601411d 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "unit:meteor": "cd meteor && yarn unit", "meteor:run": "cd meteor && yarn start", "lint": "run lint:meteor && run lint:packages", + "lint:fix": "run lint:meteor --fix && run lint:packages --fix", "unit": "run unit:meteor && run unit:packages", "validate:release": "yarn install && run install-and-build && run validate:versions && run validate:release:packages && run validate:release:meteor", "validate:release:meteor": "cd meteor && yarn validate:prod-dependencies && yarn license-validate && yarn lint && yarn test", @@ -39,9 +40,9 @@ "concurrently": "^9.2.1", "husky": "^9.1.7", "lint-staged": "^15.5.2", - "rimraf": "^6.1.2", - "semver": "^7.7.3", - "snyk-nodejs-lockfile-parser": "^1.60.1" + "rimraf": "^6.1.3", + "semver": "^7.7.4", + "snyk-nodejs-lockfile-parser": "^2.6.0" }, "packageManager": "yarn@4.12.0" } diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index ffe54d1390c..375875e55b1 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,64 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [26.3.0-2](https://github.com/Sofie-Automation/sofie-core/compare/v26.3.0-1...v26.3.0-2) (2026-02-18) + +**Note:** Version bump only for package @sofie-automation/blueprints-integration + + + + + +# [26.3.0-1](https://github.com/Sofie-Automation/sofie-core/compare/v26.3.0-0...v26.3.0-1) (2026-02-11) + +**Note:** Version bump only for package @sofie-automation/blueprints-integration + + + + + +# [26.3.0-0](https://github.com/Sofie-Automation/sofie-core/compare/v1.52.0...v26.3.0-0) (2026-02-04) + + +### Bug Fixes + +* add plannedStartedPlayback and plannedStoppedPlayback to IBlueprintPartInstanceTimings interface ([#1515](https://github.com/Sofie-Automation/sofie-core/issues/1515)) ([9e8ee71](https://github.com/Sofie-Automation/sofie-core/commit/9e8ee71863a8b00be521a2325b2375f03a32956c)) +* missing export ([7956f7b](https://github.com/Sofie-Automation/sofie-core/commit/7956f7bba509d892389bb3c564312da730c0495b)) +* remove unimplemented return type of blueprint executeAction ([5e74d4f](https://github.com/Sofie-Automation/sofie-core/commit/5e74d4ff2b5322683d6bb2bff1bc228ec9709ec8)) + + +### Features + +* add BlueprintAssetIcon component ([e05afd6](https://github.com/Sofie-Automation/sofie-core/commit/e05afd68386fbdcc7e21c23ef60f3f138048df78)) +* add getUpcomingParts method to OnSetAsNextContext ([#1577](https://github.com/Sofie-Automation/sofie-core/issues/1577)) ([aba5ed4](https://github.com/Sofie-Automation/sofie-core/commit/aba5ed42b51e7132c2d1c50878b260aa268989b3)) +* Add getUpcomingParts to action context ([#1524](https://github.com/Sofie-Automation/sofie-core/issues/1524)) ([0d1552d](https://github.com/Sofie-Automation/sofie-core/commit/0d1552dca9fc3f3dbaa94a8edb7f0f25c369f7dc)) +* Add support for Gateway configuration from the studio API ([#1539](https://github.com/Sofie-Automation/sofie-core/issues/1539)) ([963542a](https://github.com/Sofie-Automation/sofie-core/commit/963542aa060f7db768d47a1d7e4e1f25367bb321)) +* allow adlib-actions to be marked as invalid ([#1609](https://github.com/Sofie-Automation/sofie-core/issues/1609)) ([6271ffd](https://github.com/Sofie-Automation/sofie-core/commit/6271ffd8bef5abe5691fa7b726209fc7d3758341)) +* allow part to be queued from onTake ([#1497](https://github.com/Sofie-Automation/sofie-core/issues/1497)) ([1a6619f](https://github.com/Sofie-Automation/sofie-core/commit/1a6619f42d1c7621faf10238edbcde646ef2eb33)) +* Allow restricting dragging to current part ([e9f66e7](https://github.com/Sofie-Automation/sofie-core/commit/e9f66e7e21e577822eb432f85f62c80770d5a5f2)) +* **blueprints-integration:** Add isRehearsal property to action contexts ([8d923a5](https://github.com/Sofie-Automation/sofie-core/commit/8d923a5e627ea50764eefa8cd2c345373c86453f)) +* cleanup media manager support ([#1509](https://github.com/Sofie-Automation/sofie-core/issues/1509)) ([76dfbd2](https://github.com/Sofie-Automation/sofie-core/commit/76dfbd2fa8cd18bda5713484c40e5bfe5c838529)) +* **EAV-603:** add `manuallySelected` to OnSetAsNextContext ([ec1114e](https://github.com/Sofie-Automation/sofie-core/commit/ec1114e99c77bd395cf69912e92527d91afcc845)) +* edit mode for drag operations ([4347c6a](https://github.com/Sofie-Automation/sofie-core/commit/4347c6ad0762ed5081c377aa92841bebfb5800c6)) +* expose getSegment in blueprint context ([e727028](https://github.com/Sofie-Automation/sofie-core/commit/e7270281ccd3cde2ac6490f34055f039cf24404a)) +* expose persistent playout store to more methods ([ab7c6bc](https://github.com/Sofie-Automation/sofie-core/commit/ab7c6bc116b768dd030c9160a90554db37880762)) +* GW config types in Blueprints ([c8e669f](https://github.com/Sofie-Automation/sofie-core/commit/c8e669f333010cc88930d1684bd2d2795104cc88)) +* implement Bucket Panel Icon ([fbcc6e8](https://github.com/Sofie-Automation/sofie-core/commit/fbcc6e8eeb780b24f7595b5386e729ea9d1dda9a)) +* mos status flow rework ([#1356](https://github.com/Sofie-Automation/sofie-core/issues/1356)) ([672f2bd](https://github.com/Sofie-Automation/sofie-core/commit/672f2bd2873ae306db9dfcbbc3064fdcc9ea1cd0)) +* move GW config types to generated in shared lib ([f54d9ca](https://github.com/Sofie-Automation/sofie-core/commit/f54d9ca63bc00a05915aac45e0be5b595c980567)) +* optional studioLabelShort for presenters view ([cf62762](https://github.com/Sofie-Automation/sofie-core/commit/cf6276289b3bc47df3635b34ca75994ccc37713b)) +* PieceGeneric type - optional nameShort and nameTruncated ([c7d87a7](https://github.com/Sofie-Automation/sofie-core/commit/c7d87a7b463a4dbb546e967f87620badedfd0046)) +* replace `wasActive` in onRundownActivate with context ([#1514](https://github.com/Sofie-Automation/sofie-core/issues/1514)) ([007a9da](https://github.com/Sofie-Automation/sofie-core/commit/007a9da74583702b347c613e5aed8514422d5c3d)) +* retime piece user action ([385e884](https://github.com/Sofie-Automation/sofie-core/commit/385e884e8f3f9d1165fcfa06af649d5af951b516)) +* Set sub-device peripheralDeviceId from deviceOptions parentDeviceName ([#1505](https://github.com/Sofie-Automation/sofie-core/issues/1505)) ([4d34cec](https://github.com/Sofie-Automation/sofie-core/commit/4d34cecac83929d999b088423f98fd9b787c0c31)) +* support custom types from tsr plugins ([#1585](https://github.com/Sofie-Automation/sofie-core/issues/1585)) ([3bae757](https://github.com/Sofie-Automation/sofie-core/commit/3bae7576ede0e2f71cf9882e6f2c1ac5589d9b63)) +* time of day pieces ([#1406](https://github.com/Sofie-Automation/sofie-core/issues/1406)) ([2500780](https://github.com/Sofie-Automation/sofie-core/commit/25007807845e03e92c17e623c159611f89703672)) +* update meteor to 3.3.2 ([#1529](https://github.com/Sofie-Automation/sofie-core/issues/1529)) ([9bd232e](https://github.com/Sofie-Automation/sofie-core/commit/9bd232e8f0561a46db8cc6143c5353d7fa531206)) + + + + + # [1.52.0](https://github.com/Sofie-Automation/sofie-core/compare/v1.52.0-in-testing.1...v1.52.0) (2025-06-30) **Note:** Version bump only for package @sofie-automation/blueprints-integration diff --git a/packages/blueprints-integration/eslint.config.mjs b/packages/blueprints-integration/eslint.config.mjs deleted file mode 100644 index b9e5a88fd88..00000000000 --- a/packages/blueprints-integration/eslint.config.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' - -export default generateEslintConfig({}) diff --git a/packages/blueprints-integration/jest.config.js b/packages/blueprints-integration/jest.config.js index 3c2898390eb..4b068877330 100644 --- a/packages/blueprints-integration/jest.config.js +++ b/packages/blueprints-integration/jest.config.js @@ -6,6 +6,11 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.json', + diagnostics: { + ignoreCodes: [ + 151002, // hybrid module kind (Node16/18/Next) + ], + }, }, ], }, diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index ec0dd2915ae..06172465c6b 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -15,8 +15,7 @@ }, "homepage": "https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration#readme", "scripts": { - "lint:raw": "run -T eslint", - "lint": "run lint:raw .", + "lint": "run -T lint blueprints-integration", "unit": "run -T jest", "test": "run lint && run unit", "watch": "run -T jest --watch", @@ -36,17 +35,9 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.53.0-in-development", + "@sofie-automation/shared-lib": "26.3.0-2", "tslib": "^2.8.1", - "type-fest": "^4.33.0" - }, - "lint-staged": { - "*.{css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx,js,jsx}": [ - "yarn lint:raw" - ] + "type-fest": "^4.41.0" }, "publishConfig": { "access": "public" diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 43182638f38..0e7fba32754 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -21,7 +21,6 @@ import type { } from '../context/index.js' import type { IngestAdlib, ExtendedIngestRundown, IngestRundown } from '../ingest.js' import type { IBlueprintExternalMessageQueueObj } from '../message.js' -import type {} from '../migrations.js' import type { IBlueprintAdLibPiece, IBlueprintResolvedPieceInstance, @@ -57,8 +56,10 @@ export { PackageStatusMessage } export type TimelinePersistentState = unknown -export interface ShowStyleBlueprintManifest - extends BlueprintManifestBase { +export interface ShowStyleBlueprintManifest< + TRawConfig = IBlueprintConfig, + TProcessedConfig = unknown, +> extends BlueprintManifestBase { blueprintType: BlueprintManifestType.SHOWSTYLE /** A list of config items this blueprint expects to be available on the ShowStyle */ @@ -199,12 +200,16 @@ export interface ShowStyleBlueprintManifest Promise /** Called upon the first take in a RundownPlaylist */ onRundownFirstTake?: (context: IPartEventContext) => Promise - /** Called when a RundownPlaylist has been deactivated */ + /** + * Called at the final stage of RundownPlaylist deactivation, before the updated timeline is submitted to the Playout Gateway, + * This is a good place to prepare any external systems for the rundown going offline. + */ onRundownDeActivate?: (context: IRundownActivationContext) => Promise /** Called before a Take action */ @@ -290,6 +295,15 @@ export interface BlueprintResultPart { } export interface BlueprintSyncIngestNewData { + /** All parts in the rundown, including the new/updated part */ + allParts: IBlueprintPartDB[] + /** + * An approximate index of the current part in the allParts array + * Note: this will not always be an integer, such as when the part is an adlib part + * `null` means the part could not be placed + */ + currentPartIndex: number | null + // source: BlueprintSyncIngestDataSource /** The new part */ part: IBlueprintPartDB | undefined diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 11fbaa9d0c7..bd988641de5 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -17,6 +17,7 @@ import type { IngestRundown, MutableIngestRundown, UserOperationChange, + PlayoutOperationChange, } from '../ingest.js' import type { ExpectedPlayoutItemGeneric, @@ -30,15 +31,20 @@ import type { StudioRouteSet, StudioRouteSetExclusivityGroup, } from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' -import type { StudioPackageContainer } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' +import type { + StudioPackageContainer, + StudioPackageContainerSettings, +} from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' import type { IStudioSettings } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' import type { MosDeviceConfig } from '@sofie-automation/shared-lib/dist/generated/MosGatewayDevicesTypes' import type { MosGatewayConfig } from '@sofie-automation/shared-lib/dist/generated/MosGatewayOptionsTypes' import type { PlayoutGatewayConfig } from '@sofie-automation/shared-lib/dist/generated/PlayoutGatewayConfigTypes' import type { LiveStatusGatewayConfig } from '@sofie-automation/shared-lib/dist/generated/LiveStatusGatewayOptionsTypes' -export interface StudioBlueprintManifest - extends BlueprintManifestBase { +export interface StudioBlueprintManifest< + TRawConfig = IBlueprintConfig, + TProcessedConfig = unknown, +> extends BlueprintManifestBase { blueprintType: BlueprintManifestType.STUDIO /** A list of config items this blueprint expects to be available on the Studio */ @@ -103,6 +109,15 @@ export interface StudioBlueprintManifest Array + /** Validate the rundown payload passed to this blueprint according to the API schema, returning a list of error messages. */ + validateRundownPayloadFromAPI?: (context: ICommonContext, payload: unknown) => Array + + /** Validate the segment payload passed to this blueprint according to the API schema, returning a list of error messages. */ + validateSegmentPayloadFromAPI?: (context: ICommonContext, payload: unknown) => Array + + /** Validate the part payload passed to this blueprint according to the API schema, returning a list of error messages. */ + validatePartPayloadFromAPI?: (context: ICommonContext, payload: unknown) => Array + /** * Optional method to transform from an API blueprint config to the database blueprint config if these are required to be different. * If this method is not defined the config object will be used directly @@ -123,7 +138,7 @@ export interface StudioBlueprintManifest, nrcsIngestRundown: IngestRundown, previousNrcsIngestRundown: IngestRundown | undefined, - changes: NrcsIngestChangeDetails | UserOperationChange + changes: NrcsIngestChangeDetails | UserOperationChange | PlayoutOperationChange ) => Promise } @@ -166,6 +181,8 @@ export interface BlueprintResultApplyStudioConfig { routeSetExclusivityGroups?: Record /** Package Containers */ packageContainers?: Record + /** Which Package Containers are used for media previews/thumbnails in GUI */ + packageContainerSettings?: StudioPackageContainerSettings studioSettings?: IStudioSettings } diff --git a/packages/blueprints-integration/src/api/system.ts b/packages/blueprints-integration/src/api/system.ts index f052c4e1b14..3d905ac7cc2 100644 --- a/packages/blueprints-integration/src/api/system.ts +++ b/packages/blueprints-integration/src/api/system.ts @@ -1,5 +1,4 @@ import type { IBlueprintTriggeredActions } from '../triggers.js' -import type { MigrationStepSystem } from '../migrations.js' import type { BlueprintManifestBase, BlueprintManifestType } from './base.js' import type { ICoreSystemApplyConfigContext } from '../context/systemApplyConfigContext.js' import type { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core/model/CoreSystemSettings' @@ -7,11 +6,6 @@ import type { ICoreSystemSettings } from '@sofie-automation/shared-lib/dist/core export interface SystemBlueprintManifest extends BlueprintManifestBase { blueprintType: BlueprintManifestType.SYSTEM - /** A list of Migration steps related to the Core system - * @deprecated This has been replaced with `applyConfig` - */ - coreMigrations: MigrationStepSystem[] - /** Translations connected to the studio (as stringified JSON) */ translations?: string diff --git a/packages/blueprints-integration/src/content.ts b/packages/blueprints-integration/src/content.ts index 71c6058969e..649d4465b2c 100644 --- a/packages/blueprints-integration/src/content.ts +++ b/packages/blueprints-integration/src/content.ts @@ -104,6 +104,7 @@ export interface ScriptContent extends BaseContent { firstWords: string lastWords: string fullScript?: string + fullScriptFormatted?: string comment?: string lastModified?: Time | null } diff --git a/packages/blueprints-integration/src/context/adlibActionContext.ts b/packages/blueprints-integration/src/context/adlibActionContext.ts index 6f9931eeea7..00da91558df 100644 --- a/packages/blueprints-integration/src/context/adlibActionContext.ts +++ b/packages/blueprints-integration/src/context/adlibActionContext.ts @@ -2,9 +2,10 @@ import type { DatastorePersistenceMode, Time } from '../common.js' import type { IEventContext } from './index.js' import type { IShowStyleUserContext } from './showStyleContext.js' import { IPartAndPieceActionContext } from './partsAndPieceActionContext.js' -import { IExecuteTSRActionsContext } from './executeTsrActionContext.js' +import { IExecuteTSRActionsContext, ITriggerIngestChangeContext } from './executeTsrActionContext.js' import { IBlueprintPart, IBlueprintPartInstance, IBlueprintPiece } from '../index.js' import { IRouteSetMethods } from './routeSetContext.js' +import { ITTimersContext } from './tTimersContext.js' /** Actions */ export interface IDataStoreMethods { @@ -21,12 +22,15 @@ export interface IDataStoreMethods { export interface IDataStoreActionExecutionContext extends IDataStoreMethods, IShowStyleUserContext, IEventContext {} export interface IActionExecutionContext - extends IShowStyleUserContext, + extends + IShowStyleUserContext, IEventContext, IDataStoreMethods, IPartAndPieceActionContext, IExecuteTSRActionsContext, - IRouteSetMethods { + ITriggerIngestChangeContext, + IRouteSetMethods, + ITTimersContext { /** Fetch the showstyle config for the specified part */ // getNextShowStyleConfig(): Readonly<{ [key: string]: ConfigItemValue }> diff --git a/packages/blueprints-integration/src/context/executeTsrActionContext.ts b/packages/blueprints-integration/src/context/executeTsrActionContext.ts index a18fa45c823..a8efe08ca41 100644 --- a/packages/blueprints-integration/src/context/executeTsrActionContext.ts +++ b/packages/blueprints-integration/src/context/executeTsrActionContext.ts @@ -8,6 +8,19 @@ export interface IExecuteTSRActionsContext { executeTSRAction( deviceId: PeripheralDeviceId, actionId: string, - payload: Record + payload: Record, + /** Timeout for the action, default: 3000 */ + timeoutMs?: number ): Promise } + +export interface ITriggerIngestChangeContext { + /** + * Execute an ingest operation + * This dispatches the operation but does not wait for it to be processed + * Note: This should be used with caution, it will not be good for performance to trigger a lot of ingest operations + * during playout of a rundown, especially if they need to make changes to many segments + * @param operation The blueprint defined payload for the operation + */ + emitIngestOperation(operation: unknown): Promise +} diff --git a/packages/blueprints-integration/src/context/onSetAsNextContext.ts b/packages/blueprints-integration/src/context/onSetAsNextContext.ts index 9e729ce4029..114ef0f47ca 100644 --- a/packages/blueprints-integration/src/context/onSetAsNextContext.ts +++ b/packages/blueprints-integration/src/context/onSetAsNextContext.ts @@ -6,18 +6,21 @@ import { IBlueprintPieceDB, IBlueprintPieceInstance, IBlueprintResolvedPieceInstance, - IBlueprintSegment, + IBlueprintSegmentDB, IEventContext, IShowStyleUserContext, } from '../index.js' +import { ITriggerIngestChangeContext } from './executeTsrActionContext.js' import { BlueprintQuickLookInfo } from './quickLoopInfo.js' import { ReadonlyDeep } from 'type-fest' +import type { ITTimersContext } from './tTimersContext.js' /** * Context in which 'current' is the part currently on air, and 'next' is the partInstance being set as Next * This is similar to `IPartAndPieceActionContext`, but has more limits on what is allowed to be changed. */ -export interface IOnSetAsNextContext extends IShowStyleUserContext, IEventContext { +export interface IOnSetAsNextContext + extends IShowStyleUserContext, IEventContext, ITriggerIngestChangeContext, ITTimersContext { /** Information about the current loop, if there is one */ readonly quickLoopInfo: BlueprintQuickLookInfo | null @@ -55,7 +58,7 @@ export interface IOnSetAsNextContext extends IShowStyleUserContext, IEventContex /** Gets the Part for a Piece retrieved from findLastScriptedPieceOnLayer. This primarily allows for accessing metadata of the Part */ getPartForPreviousPiece(piece: IBlueprintPieceDB): Promise /** Gets the Segment. This primarily allows for accessing metadata */ - getSegment(segment: 'current' | 'next'): Promise + getSegment(segment: 'current' | 'next'): Promise /** Get a list of the upcoming Parts in the Rundown, in the order that they will be Taken * @@ -69,7 +72,7 @@ export interface IOnSetAsNextContext extends IShowStyleUserContext, IEventContex */ /** Insert a pieceInstance. Returns id of new PieceInstance. Any timelineObjects will have their ids changed, so are not safe to reference from another piece */ insertPiece(part: 'next', piece: IBlueprintPiece): Promise - /** Update a piecesInstance from the partInstance being set as Next */ + /** Update a piecesInstance */ updatePieceInstance(pieceInstanceId: string, piece: Partial): Promise /** Update a partInstance */ diff --git a/packages/blueprints-integration/src/context/onTakeContext.ts b/packages/blueprints-integration/src/context/onTakeContext.ts index 461f64bfa1d..20e462092f8 100644 --- a/packages/blueprints-integration/src/context/onTakeContext.ts +++ b/packages/blueprints-integration/src/context/onTakeContext.ts @@ -1,15 +1,19 @@ import { IBlueprintPart, IBlueprintPiece, IEventContext, IShowStyleUserContext, Time } from '../index.js' import { IPartAndPieceActionContext } from './partsAndPieceActionContext.js' -import { IExecuteTSRActionsContext } from './executeTsrActionContext.js' +import { IExecuteTSRActionsContext, ITriggerIngestChangeContext } from './executeTsrActionContext.js' +import { ITTimersContext } from './tTimersContext.js' /** * Context in which 'current' is the partInstance we're leaving, and 'next' is the partInstance we're taking */ export interface IOnTakeContext - extends IPartAndPieceActionContext, + extends + IPartAndPieceActionContext, IShowStyleUserContext, IEventContext, - IExecuteTSRActionsContext { + IExecuteTSRActionsContext, + ITriggerIngestChangeContext, + ITTimersContext { /** Inform core that a take out of the taken partinstance should be blocked until the specified time */ blockTakeUntil(time: Time | null): Promise /** diff --git a/packages/blueprints-integration/src/context/partsAndPieceActionContext.ts b/packages/blueprints-integration/src/context/partsAndPieceActionContext.ts index 6f10958eebc..a5a2b9c998c 100644 --- a/packages/blueprints-integration/src/context/partsAndPieceActionContext.ts +++ b/packages/blueprints-integration/src/context/partsAndPieceActionContext.ts @@ -7,7 +7,7 @@ import { IBlueprintPieceDB, IBlueprintPieceInstance, IBlueprintResolvedPieceInstance, - IBlueprintSegment, + IBlueprintSegmentDB, Time, } from '../index.js' import { BlueprintQuickLookInfo } from './quickLoopInfo.js' @@ -50,7 +50,7 @@ export interface IPartAndPieceActionContext { /** Gets the Part for a Piece retrieved from findLastScriptedPieceOnLayer. This primarily allows for accessing metadata of the Part */ getPartForPreviousPiece(piece: IBlueprintPieceDB): Promise /** Gets the Segment. This primarily allows for accessing metadata */ - getSegment(segment: 'current' | 'next'): Promise + getSegment(segment: 'current' | 'next'): Promise /** Get a list of the upcoming Parts in the Rundown, in the order that they will be Taken * diff --git a/packages/blueprints-integration/src/context/playoutStore.ts b/packages/blueprints-integration/src/context/playoutStore.ts index 8ecb499fd47..5cda81ac927 100644 --- a/packages/blueprints-integration/src/context/playoutStore.ts +++ b/packages/blueprints-integration/src/context/playoutStore.ts @@ -1,26 +1,48 @@ /** * A store for persisting playout state between bluerpint method calls * This belongs to the Playlist and will be discarded when the Playlist is reset + * This wraps both the 'privateData' and 'publicData' variants, the private variant only accessible to Blueprints, and the public variant available in APIs such as the LSG */ export interface BlueprintPlayoutPersistentStore { /** - * Get all the data in the store + * Get all the private data in the store */ getAll(): Partial /** - * Retrieve a key of data from the store + * Retrieve a key of private data from the store * @param k The key to retrieve */ getKey(k: K): T[K] | undefined /** - * Update a key of data in the store + * Update a key of private data in the store * @param k The key to update * @param v The value to set */ setKey(k: K, v: T[K]): void /** - * Replace all the data in the store + * Replace all the private data in the store * @param obj The new data */ setAll(obj: T): void + + /** + * Get all the public data in the store + */ + getAllPublic(): Partial + /** + * Retrieve a key of public data from the store + * @param k The key to retrieve + */ + getKeyPublic(k: K): T[K] | undefined + /** + * Update a key of public data in the store + * @param k The key to update + * @param v The value to set + */ + setKeyPublic(k: K, v: T[K]): void + /** + * Replace all the public data in the store + * @param obj The new data + */ + setAllPublic(obj: T): void } diff --git a/packages/blueprints-integration/src/context/rundownContext.ts b/packages/blueprints-integration/src/context/rundownContext.ts index 402da1fa396..cb57cbd5692 100644 --- a/packages/blueprints-integration/src/context/rundownContext.ts +++ b/packages/blueprints-integration/src/context/rundownContext.ts @@ -4,6 +4,7 @@ import type { IPackageInfoContext } from './packageInfoContext.js' import type { IShowStyleContext } from './showStyleContext.js' import type { IExecuteTSRActionsContext } from './executeTsrActionContext.js' import type { IDataStoreMethods } from './adlibActionContext.js' +import { ITTimersContext } from './tTimersContext.js' export interface IRundownContext extends IShowStyleContext { readonly rundownId: string @@ -13,7 +14,8 @@ export interface IRundownContext extends IShowStyleContext { export interface IRundownUserContext extends IUserNotesContext, IRundownContext {} -export interface IRundownActivationContext extends IRundownContext, IExecuteTSRActionsContext, IDataStoreMethods { +export interface IRundownActivationContext + extends IRundownContext, IExecuteTSRActionsContext, IDataStoreMethods, ITTimersContext { /** Info about the RundownPlaylist state before the Activation / Deactivation event */ readonly previousState: IRundownActivationContextState readonly currentState: IRundownActivationContextState diff --git a/packages/blueprints-integration/src/context/syncIngestChangesContext.ts b/packages/blueprints-integration/src/context/syncIngestChangesContext.ts index e6917d443b6..668e5bfd3e1 100644 --- a/packages/blueprints-integration/src/context/syncIngestChangesContext.ts +++ b/packages/blueprints-integration/src/context/syncIngestChangesContext.ts @@ -6,8 +6,9 @@ import type { IBlueprintPieceInstance, } from '../documents/index.js' import type { IEventContext } from './eventContext.js' +import type { ITTimersContext } from './tTimersContext.js' -export interface ISyncIngestUpdateToPartInstanceContext extends IRundownUserContext, IEventContext { +export interface ISyncIngestUpdateToPartInstanceContext extends IRundownUserContext, ITTimersContext, IEventContext { /** Sync a pieceInstance. Inserts the pieceInstance if new, updates if existing. Optionally pass in a mutated Piece, to override the content of the instance */ syncPieceInstance( pieceInstanceId: string, diff --git a/packages/blueprints-integration/src/context/tTimersContext.ts b/packages/blueprints-integration/src/context/tTimersContext.ts new file mode 100644 index 00000000000..7b00d9258a1 --- /dev/null +++ b/packages/blueprints-integration/src/context/tTimersContext.ts @@ -0,0 +1,164 @@ +export type IPlaylistTTimerIndex = 1 | 2 | 3 + +export interface ITTimersContext { + /** + * Get a T-timer by its index + * Note: Index is 1-based (1, 2, 3) + * @param index Number of the timer to retrieve + */ + getTimer(index: IPlaylistTTimerIndex): IPlaylistTTimer + + /** + * Clear all T-timers + */ + clearAllTimers(): void +} + +export interface IPlaylistTTimer { + readonly index: IPlaylistTTimerIndex + + /** The label of the T-timer */ + readonly label: string + + /** + * The current state of the T-timer + * Null if the T-timer is not initialized + */ + readonly state: IPlaylistTTimerState | null + + /** Set the label of the T-timer */ + setLabel(label: string): void + + /** Clear the T-timer back to an uninitialized state */ + clearTimer(): void + + /** + * Start a countdown timer + * @param duration Duration of the countdown in milliseconds + * @param options Options for the countdown + */ + startCountdown(duration: number, options?: { stopAtZero?: boolean; startPaused?: boolean }): void + + /** + * Start a timeOfDay timer, counting towards the target time + * This will throw if it is unable to parse the target time + * @param targetTime The target time, as a string (e.g. "14:30", "2023-12-31T23:59:59Z") or a timestamp number + */ + startTimeOfDay(targetTime: string | number, options?: { stopAtZero?: boolean }): void + + /** + * Start a free-running timer + */ + startFreeRun(options?: { startPaused?: boolean }): void + + /** + * If the current mode supports being paused, pause the timer + * Note: This is supported by the countdown and freerun modes + * @returns True if the timer was paused, false if it could not be paused + */ + pause(): boolean + + /** + * If the current mode supports being paused, resume the timer + * This is the opposite of `pause()` + * @returns True if the timer was resumed, false if it could not be resumed + */ + resume(): boolean + + /** + * If the timer can be restarted, restore it to its initial/restarted state + * Note: This is supported by the countdown and timeOfDay modes + * @returns True if the timer was restarted, false if it could not be restarted + */ + restart(): boolean + + /** + * Clear any projection (manual or anchor-based) for this timer + * This removes both manual projections set via setProjectedTime/setProjectedDuration + * and automatic projections based on anchor parts set via setProjectedAnchorPart. + */ + clearProjected(): void + + /** + * Set the anchor part for automatic projection calculation + * When set, the server automatically calculates when we expect to reach this part + * based on remaining part durations, and updates the projection accordingly. + * Clears any manual projection set via setProjectedTime/setProjectedDuration. + * @param partId The ID of the part to use as timing anchor + */ + setProjectedAnchorPart(partId: string): void + + /** + * Set the anchor part for automatic projection calculation, looked up by its externalId. + * This is a convenience method when you know the externalId of the part (e.g. set during ingest) + * but not its internal PartId. If no part with the given externalId is found, this is a no-op. + * Clears any manual projection set via setProjectedTime/setProjectedDuration. + * @param externalId The externalId of the part to use as timing anchor + */ + setProjectedAnchorPartByExternalId(externalId: string): void + + /** + * Manually set the projection as an absolute timestamp + * Use this when you have custom logic for calculating when you expect to reach a timing point. + * Clears any anchor part set via setAnchorPart. + * @param time Unix timestamp (milliseconds) when we expect to reach the timing point + * @param paused If true, we're currently delayed/pushing (projection won't update with time passing). + * If false (default), we're progressing normally (projection counts down in real-time). + */ + setProjectedTime(time: number, paused?: boolean): void + + /** + * Manually set the projection as a relative duration from now + * Use this when you want to express the projection as "X milliseconds from now". + * Clears any anchor part set via setAnchorPart. + * @param duration Milliseconds until we expect to reach the timing point + * @param paused If true, we're currently delayed/pushing (projection won't update with time passing). + * If false (default), we're progressing normally (projection counts down in real-time). + */ + setProjectedDuration(duration: number, paused?: boolean): void +} + +export type IPlaylistTTimerState = + | IPlaylistTTimerStateCountdown + | IPlaylistTTimerStateFreeRun + | IPlaylistTTimerStateTimeOfDay + +export interface IPlaylistTTimerStateCountdown { + /** The mode of the T-timer */ + readonly mode: 'countdown' + /** The current time of the countdown, in milliseconds */ + readonly currentTime: number + /** The total duration of the countdown, in milliseconds */ + readonly duration: number + /** Whether the timer is currently paused */ + readonly paused: boolean + + /** If the countdown is set to stop at zero, or continue into negative values */ + readonly stopAtZero: boolean +} +export interface IPlaylistTTimerStateFreeRun { + /** The mode of the T-timer */ + readonly mode: 'freeRun' + /** The current time of the freerun, in milliseconds */ + readonly currentTime: number + /** Whether the timer is currently paused */ + readonly paused: boolean +} + +export interface IPlaylistTTimerStateTimeOfDay { + /** The mode of the T-timer */ + readonly mode: 'timeOfDay' + /** The current remaining time of the timer, in milliseconds */ + readonly currentTime: number + /** The target timestamp of the timer, in milliseconds */ + readonly targetTime: number + + /** + * The raw target string of the timer, as provided when setting the timer + * (e.g. "14:30", "2023-12-31T23:59:59Z", or a timestamp number) + */ + readonly targetRaw: string | number + + /** If the countdown is set to stop at zero, or continue into negative values */ + readonly stopAtZero: boolean +} diff --git a/packages/blueprints-integration/src/documents/adlibPiece.ts b/packages/blueprints-integration/src/documents/adlibPiece.ts index 148d668e153..3c4291bb77a 100644 --- a/packages/blueprints-integration/src/documents/adlibPiece.ts +++ b/packages/blueprints-integration/src/documents/adlibPiece.ts @@ -1,7 +1,9 @@ import type { IBlueprintPieceGeneric } from './pieceGeneric.js' -export interface IBlueprintAdLibPiece - extends IBlueprintPieceGeneric { +export interface IBlueprintAdLibPiece extends IBlueprintPieceGeneric< + TPrivateData, + TPublicData +> { /** Used for sorting in the UI */ _rank: number /** When something bad has happened, we can mark the AdLib as invalid, which will prevent the user from TAKE:ing it */ @@ -26,7 +28,9 @@ export interface IBlueprintAdLibPiece - extends IBlueprintAdLibPiece { +export interface IBlueprintAdLibPieceDB extends IBlueprintAdLibPiece< + TPrivateData, + TPublicData +> { _id: string } diff --git a/packages/blueprints-integration/src/documents/part.ts b/packages/blueprints-integration/src/documents/part.ts index 09e88c44e6a..42283ac3609 100644 --- a/packages/blueprints-integration/src/documents/part.ts +++ b/packages/blueprints-integration/src/documents/part.ts @@ -112,8 +112,10 @@ export interface HackPartMediaObjectSubscription { } /** The Part generated from Blueprint */ -export interface IBlueprintPart - extends IBlueprintMutatablePart { +export interface IBlueprintPart extends IBlueprintMutatablePart< + TPrivateData, + TPublicData +> { /** Id of the part from the gateway if this part does not map directly to an IngestPart. This must be unique for each part */ externalId: string @@ -175,8 +177,10 @@ export interface IBlueprintPart } /** The Part sent from Core */ -export interface IBlueprintPartDB - extends IBlueprintPart { +export interface IBlueprintPartDB extends IBlueprintPart< + TPrivateData, + TPublicData +> { _id: string /** The segment ("Title") this line belongs to */ segmentId: string diff --git a/packages/blueprints-integration/src/documents/piece.ts b/packages/blueprints-integration/src/documents/piece.ts index b6dffa110f7..e9f80b34da5 100644 --- a/packages/blueprints-integration/src/documents/piece.ts +++ b/packages/blueprints-integration/src/documents/piece.ts @@ -11,8 +11,10 @@ export enum IBlueprintPieceType { } /** A Single item in a "line": script, VT, cameras. Generated by Blueprint */ -export interface IBlueprintPiece - extends IBlueprintPieceGeneric { +export interface IBlueprintPiece extends IBlueprintPieceGeneric< + TPrivateData, + TPublicData +> { /** Timeline enabler. When the piece should be active on the timeline. */ enable: { start: number | 'now' // TODO - now will be removed from this eventually, but as it is not an acceptable value 99% of the time, that is not really breaking @@ -52,7 +54,9 @@ export interface IBlueprintPiece */ displayAbChannel?: boolean } -export interface IBlueprintPieceDB - extends IBlueprintPiece { +export interface IBlueprintPieceDB extends IBlueprintPiece< + TPrivateData, + TPublicData +> { _id: string } diff --git a/packages/blueprints-integration/src/documents/pieceInstance.ts b/packages/blueprints-integration/src/documents/pieceInstance.ts index 9cca9ff483b..cce68fe4ce5 100644 --- a/packages/blueprints-integration/src/documents/pieceInstance.ts +++ b/packages/blueprints-integration/src/documents/pieceInstance.ts @@ -29,10 +29,17 @@ export interface IBlueprintPieceInstance - extends IBlueprintPieceInstance { +export interface IBlueprintResolvedPieceInstance< + TPrivateData = unknown, + TPublicData = unknown, +> extends IBlueprintPieceInstance { /** * Calculated start point within the PartInstance */ diff --git a/packages/blueprints-integration/src/documents/rundown.ts b/packages/blueprints-integration/src/documents/rundown.ts index 361efe22d05..9daa30383a6 100644 --- a/packages/blueprints-integration/src/documents/rundown.ts +++ b/packages/blueprints-integration/src/documents/rundown.ts @@ -36,8 +36,7 @@ export interface IBlueprintRundown - extends IBlueprintRundown, - IBlueprintRundownDBData {} + extends IBlueprintRundown, IBlueprintRundownDBData {} /** Properties added to a rundown in Core */ export interface IBlueprintRundownDBData { diff --git a/packages/blueprints-integration/src/documents/rundownPiece.ts b/packages/blueprints-integration/src/documents/rundownPiece.ts index 2794b839c89..093e89c5c91 100644 --- a/packages/blueprints-integration/src/documents/rundownPiece.ts +++ b/packages/blueprints-integration/src/documents/rundownPiece.ts @@ -3,8 +3,10 @@ import { IBlueprintPieceGeneric } from './pieceGeneric.js' /** * A variant of a Piece, that is owned by the Rundown. */ -export interface IBlueprintRundownPiece - extends Omit, 'lifespan'> { +export interface IBlueprintRundownPiece extends Omit< + IBlueprintPieceGeneric, + 'lifespan' +> { /** When the piece should be active on the timeline. */ enable: { start: number @@ -22,7 +24,9 @@ export interface IBlueprintRundownPiece - extends IBlueprintRundownPiece { +export interface IBlueprintRundownPieceDB extends IBlueprintRundownPiece< + TPrivateData, + TPublicData +> { _id: string } diff --git a/packages/blueprints-integration/src/documents/segment.ts b/packages/blueprints-integration/src/documents/segment.ts index 0a929cd2236..a0308e8027c 100644 --- a/packages/blueprints-integration/src/documents/segment.ts +++ b/packages/blueprints-integration/src/documents/segment.ts @@ -60,7 +60,9 @@ export interface IBlueprintSegment - extends IBlueprintSegment { +export interface IBlueprintSegmentDB extends IBlueprintSegment< + TPrivateData, + TPublicData +> { _id: string } diff --git a/packages/blueprints-integration/src/index.ts b/packages/blueprints-integration/src/index.ts index 8a77c712c48..ac46527404c 100644 --- a/packages/blueprints-integration/src/index.ts +++ b/packages/blueprints-integration/src/index.ts @@ -9,7 +9,6 @@ export * from './ingest.js' export * from './ingest-types.js' export * from './lib.js' export * from './message.js' -export * from './migrations.js' export * from './package.js' export * from './packageInfo.js' export * from './documents/index.js' diff --git a/packages/blueprints-integration/src/ingest-types.ts b/packages/blueprints-integration/src/ingest-types.ts index 716c7472003..9e8efdf46d1 100644 --- a/packages/blueprints-integration/src/ingest-types.ts +++ b/packages/blueprints-integration/src/ingest-types.ts @@ -4,8 +4,11 @@ export interface SofieIngestPlaylist extends IngestPlaylist { /** Ingest cache of rundowns in this playlist. */ rundowns: SofieIngestRundown[] } -export interface SofieIngestRundown - extends IngestRundown { +export interface SofieIngestRundown< + TRundownPayload = unknown, + TSegmentPayload = unknown, + TPartPayload = unknown, +> extends IngestRundown { /** Array of segments in this rundown */ segments: SofieIngestSegment[] @@ -19,8 +22,10 @@ export interface SofieIngestRundown } -export interface SofieIngestSegment - extends IngestSegment { +export interface SofieIngestSegment extends IngestSegment< + TSegmentPayload, + TPartPayload +> { /** Array of parts in this segment */ parts: SofieIngestPart[] diff --git a/packages/blueprints-integration/src/ingest.ts b/packages/blueprints-integration/src/ingest.ts index dd97b3c3619..f1a69186207 100644 --- a/packages/blueprints-integration/src/ingest.ts +++ b/packages/blueprints-integration/src/ingest.ts @@ -12,8 +12,11 @@ export { } from '@sofie-automation/shared-lib/dist/peripheralDevice/ingest' /** The IngestRundown is extended with data from Core */ -export interface ExtendedIngestRundown - extends SofieIngestRundown { +export interface ExtendedIngestRundown< + TRundownPayload = unknown, + TSegmentPayload = unknown, + TPartPayload = unknown, +> extends SofieIngestRundown { coreData: IBlueprintRundownDBData | undefined } @@ -81,6 +84,8 @@ export enum IngestChangeType { Ingest = 'ingest', /** Indicate that this change is from user operations */ User = 'user', + /** Indicate that this change is from playout operations */ + Playout = 'playout', } /** @@ -188,6 +193,19 @@ export interface UserOperationChange Promise -export type ValidateFunctionSystem = ( - context: MigrationContextSystem, - afterMigration: boolean -) => Promise -export type ValidateFunction = ValidateFunctionSystem | ValidateFunctionCore - -export type MigrateFunctionCore = (input: MigrationStepInputFilteredResult) => Promise -export type MigrateFunctionSystem = ( - context: MigrationContextSystem, - input: MigrationStepInputFilteredResult -) => Promise -export type MigrateFunction = MigrateFunctionSystem | MigrateFunctionCore - -export type InputFunctionCore = () => MigrationStepInput[] -export type InputFunctionSystem = (context: MigrationContextSystem) => MigrationStepInput[] -export type InputFunction = InputFunctionSystem | InputFunctionCore - -interface MigrationContextWithTriggeredActions { - getAllTriggeredActions: () => Promise - getTriggeredAction: (triggeredActionId: string) => Promise - getTriggeredActionId: (triggeredActionId: string) => string - setTriggeredAction: (triggeredActions: IBlueprintTriggeredActions) => Promise - removeTriggeredAction: (triggeredActionId: string) => Promise -} - -export type MigrationContextSystem = MigrationContextWithTriggeredActions - -export interface MigrationStepBase< - TValidate extends ValidateFunction, - TMigrate extends MigrateFunction, - TInput extends InputFunction, -> { - /** Unique id for this step */ - id: string - /** If this step overrides another step. Note: It's only possible to override steps in previous versions */ - overrideSteps?: string[] - - /** - * The validate function determines whether the step is to be applied - * (it can for example check that some value in the database is present) - * The function should return falsy if step is fullfilled (ie truthy if migrate function should be applied, return value could then be a string describing why) - * The function is also run after the migration-script has been applied (and should therefore return false if all is good) - */ - validate: TValidate - - /** If true, this step can be run automatically, without prompting for user input */ - canBeRunAutomatically: boolean - /** - * The migration script. This is the script that performs the updates. - * Input to the function is the result from the user prompt (for manual steps) - * The miggration script is optional, and may be omitted if the user is expected to perform the update manually - * @param result Input from the user query - */ - migrate?: TMigrate - /** Query user for input, used in manual steps */ - input?: MigrationStepInput[] | TInput - - /** If this step depend on the result of another step. Will pause the migration before this step in that case. */ - dependOnResultFrom?: string -} -export interface MigrationStep< - TValidate extends ValidateFunction, - TMigrate extends MigrateFunction, - TInput extends InputFunction, -> extends MigrationStepBase { - /** The version this Step applies to */ - version: string -} - -export type MigrationStepCore = MigrationStep -export type MigrationStepSystem = MigrationStep diff --git a/packages/blueprints-integration/src/previews.ts b/packages/blueprints-integration/src/previews.ts index 439f5d9f036..ec2c934cbb4 100644 --- a/packages/blueprints-integration/src/previews.ts +++ b/packages/blueprints-integration/src/previews.ts @@ -97,6 +97,7 @@ export type PreviewContent = /** Show script content with timing words and metadata */ type: 'script' script?: string + scriptFormatted?: string firstWords?: string lastWords?: string comment?: string @@ -165,6 +166,7 @@ export interface ScriptPreview extends PreviewBase { type: PreviewType.Script fullText?: string + fullTextFormatted?: string lastWords?: string comment?: string lastModified?: number diff --git a/packages/blueprints-integration/src/timeline.ts b/packages/blueprints-integration/src/timeline.ts index 5b64a0987fb..57f4f23f6ce 100644 --- a/packages/blueprints-integration/src/timeline.ts +++ b/packages/blueprints-integration/src/timeline.ts @@ -5,6 +5,7 @@ export { TSR } export { TimelineObjHoldMode, + TimelineObjOnAirMode, TimelineObjectCoreExt, TimelineKeyframeCoreExt, } from '@sofie-automation/shared-lib/dist/core/model/Timeline' diff --git a/packages/blueprints-integration/src/triggers.ts b/packages/blueprints-integration/src/triggers.ts index bcaf6279fb6..eda88394aa2 100644 --- a/packages/blueprints-integration/src/triggers.ts +++ b/packages/blueprints-integration/src/triggers.ts @@ -123,6 +123,11 @@ export type IRundownPlaylistFilterLink = field: 'name' value: string } + | { + object: 'rundownPlaylist' + field: 'rehearsal' + value: boolean + } export type IGUIContextFilterLink = { object: 'view' @@ -167,6 +172,8 @@ export type IAdLibFilterLink = value: 'adLib' | 'adLibAction' | 'clear' | 'sticky' } +export type FilterType = (IRundownPlaylistFilterLink | IGUIContextFilterLink | IAdLibFilterLink)['object'] + export interface IAdlibPlayoutActionArguments { triggerMode: string } diff --git a/packages/corelib/eslint.config.mjs b/packages/corelib/eslint.config.mjs deleted file mode 100644 index b9e5a88fd88..00000000000 --- a/packages/corelib/eslint.config.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' - -export default generateEslintConfig({}) diff --git a/packages/corelib/jest.config.js b/packages/corelib/jest.config.js index 76ff2b14f1e..04b8ea8dd1c 100644 --- a/packages/corelib/jest.config.js +++ b/packages/corelib/jest.config.js @@ -6,6 +6,11 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.json', + diagnostics: { + ignoreCodes: [ + 151002, // hybrid module kind (Node16/18/Next) + ], + }, }, ], }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index bf370df72fc..8dfba527482 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -16,8 +16,7 @@ }, "homepage": "https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib#readme", "scripts": { - "lint:raw": "run -T eslint", - "lint": "run lint:raw .", + "lint": "run -T lint corelib", "unit": "run -T jest", "test": "run lint && run unit", "watch": "run -T jest --watch --coverage=false", @@ -37,30 +36,22 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.53.0-in-development", - "@sofie-automation/shared-lib": "1.53.0-in-development", + "@sofie-automation/blueprints-integration": "26.3.0-2", + "@sofie-automation/shared-lib": "26.3.0-2", "fast-clone": "^1.5.13", "i18next": "^21.10.0", - "influx": "^5.9.7", - "nanoid": "^3.3.8", + "influx": "^5.12.0", + "nanoid": "^3.3.11", "object-path": "^0.11.8", "prom-client": "^15.1.3", "timecode": "0.0.4", "tslib": "^2.8.1", - "type-fest": "^4.33.0", + "type-fest": "^4.41.0", "underscore": "^1.13.7" }, "peerDependencies": { "mongodb": "^6.12.0" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "lint-staged": { - "*.{js,css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx}": [ - "yarn lint:raw" - ] - }, "packageManager": "yarn@4.12.0" } diff --git a/packages/corelib/src/dataModel/ExpectedPackageWorkStatuses.ts b/packages/corelib/src/dataModel/ExpectedPackageWorkStatuses.ts index 8411fb5791b..e8894ef4e0f 100644 --- a/packages/corelib/src/dataModel/ExpectedPackageWorkStatuses.ts +++ b/packages/corelib/src/dataModel/ExpectedPackageWorkStatuses.ts @@ -17,7 +17,9 @@ export interface ExpectedPackageWorkStatus extends Omit { +export interface ExpectedPackageWorkStatusFromPackage extends Omit< + ExpectedPackageStatusAPI.WorkBaseInfoFromPackage, + 'id' +> { id: ExpectedPackageId } diff --git a/packages/corelib/src/dataModel/NrcsIngestDataCache.ts b/packages/corelib/src/dataModel/NrcsIngestDataCache.ts index b828720595a..bc4767bc4c8 100644 --- a/packages/corelib/src/dataModel/NrcsIngestDataCache.ts +++ b/packages/corelib/src/dataModel/NrcsIngestDataCache.ts @@ -15,8 +15,11 @@ export enum NrcsIngestCacheType { } export type IngestCacheData = IngestRundown | IngestSegment | IngestPart -export interface IngestRundownWithSource - extends IngestRundown { +export interface IngestRundownWithSource< + TRundownPayload = unknown, + TSegmentPayload = unknown, + TPartPayload = unknown, +> extends IngestRundown { rundownSource: RundownSource } diff --git a/packages/corelib/src/dataModel/Piece.ts b/packages/corelib/src/dataModel/Piece.ts index 45c5d7623a4..eccae63c7ce 100644 --- a/packages/corelib/src/dataModel/Piece.ts +++ b/packages/corelib/src/dataModel/Piece.ts @@ -51,8 +51,7 @@ export interface PieceGeneric extends Omit { timelineObjectsString: PieceTimelineObjectsBlob } export interface Piece - extends PieceGeneric, - Omit { + extends PieceGeneric, Omit { /** Timeline enabler. When the piece should be active on the timeline. */ enable: { start: number | 'now' // TODO - now will be removed from this eventually, but as it is not an acceptable value 99% of the time, that is not really breaking diff --git a/packages/corelib/src/dataModel/PieceInstance.ts b/packages/corelib/src/dataModel/PieceInstance.ts index 1847a559699..bbdaafff761 100644 --- a/packages/corelib/src/dataModel/PieceInstance.ts +++ b/packages/corelib/src/dataModel/PieceInstance.ts @@ -61,6 +61,9 @@ export interface PieceInstance { /** If this piece has been insterted during run of rundown (such as adLibs), then this is set to the timestamp it was inserted */ dynamicallyInserted?: Time + /** If this piece's lifespan has been changed to infinite during run of the rundown (adLib action, onTake, ...), then this is set to the timestamp it was changed */ + dynamicallyConvertedToInfinite?: Time + /** This is set when the duration needs to be overriden from some user action */ userDuration?: { /** The time relative to the part (milliseconds since start of part) */ diff --git a/packages/corelib/src/dataModel/Rundown.ts b/packages/corelib/src/dataModel/Rundown.ts index 2de2b3c214d..b288fe10dd0 100644 --- a/packages/corelib/src/dataModel/Rundown.ts +++ b/packages/corelib/src/dataModel/Rundown.ts @@ -87,7 +87,12 @@ export interface Rundown { } /** A description of where a Rundown originated from */ -export type RundownSource = RundownSourceNrcs | RundownSourceSnapshot | RundownSourceHttp | RundownSourceTesting +export type RundownSource = + | RundownSourceNrcs + | RundownSourceSnapshot + | RundownSourceHttp + | RundownSourceTesting + | RundownSourceRestApi /** A description of the external NRCS source of a Rundown */ export interface RundownSourceNrcs { @@ -113,6 +118,11 @@ export interface RundownSourceTesting { /** The ShowStyleVariant the Rundown is created for */ showStyleVariantId: ShowStyleVariantId } +/** A description of the source of a Rundown which was through the new HTTP ingest API */ +export interface RundownSourceRestApi { + type: 'restApi' + resyncUrl: string +} export function getRundownNrcsName(rundown: ReadonlyDeep> | undefined): string { if (rundown?.source?.type === 'nrcs' && rundown.source.nrcsName) { diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index e2850bc49bb..06cf6d3ff5f 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -94,6 +94,144 @@ export interface QuickLoopProps { forceAutoNext: ForceQuickLoopAutoNext } +export type RundownTTimerMode = RundownTTimerModeFreeRun | RundownTTimerModeCountdown | RundownTTimerModeTimeOfDay + +export interface RundownTTimerModeFreeRun { + readonly type: 'freeRun' +} +export interface RundownTTimerModeCountdown { + readonly type: 'countdown' + /** + * The original duration of the countdown in milliseconds, so that we know what value to reset to + */ + readonly duration: number + + /** + * If the countdown should stop at zero, or continue into negative values + */ + readonly stopAtZero: boolean +} +export interface RundownTTimerModeTimeOfDay { + readonly type: 'timeOfDay' + + /** + * The raw target string of the timer, as provided when setting the timer + * (e.g. "14:30", "2023-12-31T23:59:59Z", or a timestamp number) + */ + readonly targetRaw: string | number + + /** + * If the countdown should stop at zero, or continue into negative values + */ + readonly stopAtZero: boolean +} + +/** + * Timing state for a timer, optimized for efficient client rendering. + * When running, the client calculates current time from zeroTime. + * When paused, the duration is frozen and sent directly. + * pauseTime indicates when the timer should automatically pause (when current part ends and overrun begins). + * + * Client rendering logic: + * ```typescript + * if (state.paused === true) { + * // Manually paused by user or already pushing/overrun + * duration = state.duration + * } else if (state.pauseTime && now >= state.pauseTime) { + * // Auto-pause at overrun (current part ended) + * duration = state.zeroTime - state.pauseTime + * } else { + * // Running normally + * duration = state.zeroTime - now + * } + * ``` + */ +export type TimerState = + | { + /** Whether the timer is paused */ + paused: false + /** The absolute timestamp (ms) when the timer reaches/reached zero */ + zeroTime: number + /** Optional timestamp when the timer should pause (when current part ends) */ + pauseTime?: number | null + } + | { + /** Whether the timer is paused */ + paused: true + /** The frozen duration value in milliseconds */ + duration: number + /** Optional timestamp when the timer should pause (null when already paused/pushing) */ + pauseTime?: number | null + } + +/** + * Calculate the current duration for a timer state. + * Handles paused, auto-pause (pauseTime), and running states. + * + * @param state The timer state + * @param now Current timestamp in milliseconds + * @returns The current duration in milliseconds + */ +export function timerStateToDuration(state: TimerState, now: number): number { + if (state.paused) { + // Manually paused by user or already pushing/overrun + return state.duration + } else if (state.pauseTime && now >= state.pauseTime) { + // Auto-pause at overrun (current part ended) + return state.zeroTime - state.pauseTime + } else { + // Running normally + return state.zeroTime - now + } +} + +export type RundownTTimerIndex = 1 | 2 | 3 + +export interface RundownTTimer { + readonly index: RundownTTimerIndex + + /** A label for the timer */ + label: string + + /** The current mode of the timer, or null if not configured + * + * This defines how the timer behaves + */ + mode: RundownTTimerMode | null + + /** The current state of the timer, or null if not configured + * + * This contains the information needed to calculate the current time of the timer + */ + state: TimerState | null + + /** The projected time when we expect to reach the anchor part, for calculating over/under diff. + * + * Based on scheduled durations of remaining parts and segments up to the anchor. + * The over/under diff is calculated as the difference between this projection and the timer's target (state.zeroTime). + * + * Running means we are progressing towards the anchor (projection moves with real time) + * Paused means we are pushing (e.g. overrunning the current segment, so the anchor is being delayed) + * + * Calculated automatically when anchorPartId is set, or can be set manually by a blueprint if custom logic is needed. + */ + projectedState?: TimerState + + /** The target Part that this timer is counting towards (the "timing anchor") + * + * This is typically a "break" part or other milestone in the rundown. + * When set, the server calculates projectedState based on when we expect to reach this part. + * If not set, projectedState is not calculated automatically but can still be set manually by a blueprint. + */ + anchorPartId?: PartId + + /* + * Future ideas: + * allowUiControl: boolean + * display: { ... } // some kind of options for how to display in the ui + */ +} + export interface DBRundownPlaylist { _id: RundownPlaylistId /** External ID (source) of the playlist */ @@ -171,11 +309,23 @@ export interface DBRundownPlaylist { * Persistent state belong to blueprint playout methods * This can be accessed and modified by the blueprints in various methods */ - previousPersistentState?: TimelinePersistentState + privatePlayoutPersistentState?: TimelinePersistentState + /** + * Persistent state belong to blueprint playout methods, but exposed to APIs such as the LSG + * This can be accessed and modified by the blueprints in various methods, but is also exposed to APIs such as the LSG + */ + publicPlayoutPersistentState?: TimelinePersistentState + /** AB playback sessions calculated in the last timeline genertaion */ trackedAbSessions?: ABSessionInfo[] /** AB playback sessions assigned in the last timeline generation */ assignedAbSessions?: Record + + /** + * T-timers for the Playlist. + * This is a fixed size pool with 3 being chosen as a likely good amount, that can be used for any purpose. + */ + tTimers: [RundownTTimer, RundownTTimer, RundownTTimer] } // Information about a 'selected' PartInstance for the Playlist diff --git a/packages/corelib/src/dataModel/Studio.ts b/packages/corelib/src/dataModel/Studio.ts index 0f1722f5859..566799e6043 100644 --- a/packages/corelib/src/dataModel/Studio.ts +++ b/packages/corelib/src/dataModel/Studio.ts @@ -13,7 +13,10 @@ import { StudioRouteType, StudioAbPlayerDisabling, } from '@sofie-automation/shared-lib/dist/core/model/StudioRouteSet' -import { StudioPackageContainer } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' +import { + StudioPackageContainer, + StudioPackageContainerSettings, +} from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' import { IStudioSettings } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' export { MappingsExt, MappingExt, MappingsHash, IStudioSettings } @@ -74,9 +77,8 @@ export interface DBStudio { */ packageContainersWithOverrides: ObjectWithOverrides> - /** Which package containers is used for media previews in GUI */ - previewContainerIds: string[] - thumbnailContainerIds: string[] + /** Which package containers are used for media previews/thumbnails in GUI */ + packageContainerSettingsWithOverrides: ObjectWithOverrides peripheralDeviceSettings: StudioPeripheralDeviceSettings diff --git a/packages/corelib/src/dataModel/Timeline.ts b/packages/corelib/src/dataModel/Timeline.ts index 6372e08eb08..fafa8761df9 100644 --- a/packages/corelib/src/dataModel/Timeline.ts +++ b/packages/corelib/src/dataModel/Timeline.ts @@ -29,8 +29,10 @@ export enum TimelineContentTypeOther { GROUP = 'group', } -export interface OnGenerateTimelineObjExt - extends SetRequired, 'metaData'> { +export interface OnGenerateTimelineObjExt extends SetRequired< + OnGenerateTimelineObj, + 'metaData' +> { /** The id of the partInstance this object belongs to */ partInstanceId: PartInstanceId | null /** If this is from an infinite piece, the id of the infinite instance */ diff --git a/packages/corelib/src/playout/__tests__/infinites.test.ts b/packages/corelib/src/playout/__tests__/infinites.test.ts index 5c5883d1423..d3ed182f5ff 100644 --- a/packages/corelib/src/playout/__tests__/infinites.test.ts +++ b/packages/corelib/src/playout/__tests__/infinites.test.ts @@ -177,6 +177,47 @@ describe('Infinites', () => { }, ]) }) + test('piece dynamically converted to infinite should be continued', () => { + const playlistId = protectString('playlist0') + const rundownId = protectString('rundown0') + const segmentId = protectString('segment0') + const partId = protectString('part0') + const previousPartInstance = { rundownId, segmentId, partId } + const previousSegment = { _id: previousPartInstance.segmentId } + const previousPartPieces: PieceInstance[] = [ + { + ...createPieceInstanceAsInfinite( + 'one', + rundownId, + partId, + { start: 0 }, + 'one', + PieceLifespan.OutOnRundownEnd + ), + dynamicallyConvertedToInfinite: Date.now(), + }, + ] + const segment = { _id: segmentId } + const part = { rundownId, segmentId } + const instanceId = protectString('newInstance0') + const rundown = createRundown(rundownId, playlistId, 'Test Rundown', 'rundown0') + + const continuedInstances = runAndTidyResult( + previousPartInstance, + previousSegment, + previousPartPieces, + rundown, + segment, + part, + instanceId + ) + expect(continuedInstances).toEqual([ + { + _id: 'newInstance0_one_p_continue', + start: 0, + }, + ]) + }) test('ignore pieces that have stopped', () => { const playlistId = protectString('playlist0') const rundownId = protectString('rundown0') diff --git a/packages/corelib/src/playout/infinites.ts b/packages/corelib/src/playout/infinites.ts index 9d136974d4d..ad78a0e2e7b 100644 --- a/packages/corelib/src/playout/infinites.ts +++ b/packages/corelib/src/playout/infinites.ts @@ -181,7 +181,7 @@ export function getPlayheadTrackingInfinitesForPart( // Check if we should persist any adlib onEnd infinites if (canContinueAdlibOnEnds) { const piecesByInfiniteMode = groupByToMapFunc( - pieceInstances.filter((p) => p.dynamicallyInserted), + pieceInstances.filter((p) => p.dynamicallyInserted || p.dynamicallyConvertedToInfinite), (p) => p.piece.lifespan ) for (const mode0 of [ @@ -194,7 +194,9 @@ export function getPlayheadTrackingInfinitesForPart( | PieceLifespan.OutOnSegmentEnd | PieceLifespan.OutOnShowStyleEnd const pieces = (piecesByInfiniteMode.get(mode) || []).filter( - (p) => p.infinite && (p.infinite.fromPreviousPlayhead || p.dynamicallyInserted) + (p) => + p.infinite && + (p.infinite.fromPreviousPlayhead || p.dynamicallyInserted || p.dynamicallyConvertedToInfinite) ) // This is the piece we may copy across const candidatePiece = @@ -277,6 +279,7 @@ export function getPlayheadTrackingInfinitesForPart( function markPieceInstanceAsContinuation(previousInstance: ReadonlyDeep, instance: PieceInstance) { instance._id = protectString(`${instance._id}_continue`) instance.dynamicallyInserted = previousInstance.dynamicallyInserted + instance.dynamicallyConvertedToInfinite = previousInstance.dynamicallyConvertedToInfinite instance.adLibSourceId = previousInstance.adLibSourceId instance.reportedStartedPlayback = previousInstance.reportedStartedPlayback instance.plannedStartedPlayback = previousInstance.plannedStartedPlayback diff --git a/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts b/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts index c4ec6781621..55699dbda25 100644 --- a/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts +++ b/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts @@ -13,6 +13,12 @@ interface BasicType { valC: number valD?: string } + valE?: [ + { + valF: number + valG: string + }, + ] } describe('applyAndValidateOverrides', () => { @@ -159,6 +165,7 @@ describe('applyAndValidateOverrides', () => { valC: 5, valD: 'xyz', }, + valE: [{ valF: 27, valG: 'hij' }], } const inputObjWithOverrides: ObjectWithOverrides = { @@ -172,6 +179,7 @@ describe('applyAndValidateOverrides', () => { valC: 6, valD: 'uvw', }, + valE: [{ valF: 32, valG: 'klm' }], } const res = updateOverrides(inputObjWithOverrides, updateObj) @@ -185,11 +193,13 @@ describe('applyAndValidateOverrides', () => { valC: 5, valD: 'xyz', }, + valE: [{ valF: 27, valG: 'hij' }], }, overrides: [ { op: 'set', path: 'valB.valD', value: 'uvw' }, { op: 'set', path: 'valA', value: 'def' }, { op: 'set', path: 'valB.valC', value: 6 }, + { op: 'set', path: 'valE', value: [{ valF: 32, valG: 'klm' }] }, ], }) ) diff --git a/packages/corelib/src/settings/objectWithOverrides.ts b/packages/corelib/src/settings/objectWithOverrides.ts index 8103324298b..59a015d7164 100644 --- a/packages/corelib/src/settings/objectWithOverrides.ts +++ b/packages/corelib/src/settings/objectWithOverrides.ts @@ -150,23 +150,30 @@ function recursivelyGenerateOverrides( }) continue } - if (Array.isArray(rawValue) && !_.isEqual(curValue, rawValue)) { - outOverrides.push({ - op: 'set', - path: fullKeyPathString, - value: rawValue, - }) - } - if (typeof curValue === 'object' && curValue !== null && typeof rawValue === 'object' && rawValue !== null) { - recursivelyGenerateOverrides(curValue, rawValue, fullKeyPath, outOverrides) - continue - } - if (curValue !== rawValue) { - outOverrides.push({ - op: 'set', - path: fullKeyPathString, - value: rawValue, - }) + if (Array.isArray(rawValue)) { + if (!_.isEqual(curValue, rawValue)) + outOverrides.push({ + op: 'set', + path: fullKeyPathString, + value: rawValue, + }) + } else { + if ( + typeof curValue === 'object' && + curValue !== null && + typeof rawValue === 'object' && + rawValue !== null + ) { + recursivelyGenerateOverrides(curValue, rawValue, fullKeyPath, outOverrides) + continue + } + if (curValue !== rawValue) { + outOverrides.push({ + op: 'set', + path: fullKeyPathString, + value: rawValue, + }) + } } } for (const [rawKey, rawValue] of Object.entries(rawObj)) { diff --git a/packages/corelib/src/worker/ingest.ts b/packages/corelib/src/worker/ingest.ts index ad2c081939d..f23e3a2a5fd 100644 --- a/packages/corelib/src/worker/ingest.ts +++ b/packages/corelib/src/worker/ingest.ts @@ -3,6 +3,7 @@ import { BucketAdLibId, BucketId, ExpectedPackageId, + PartId, RundownId, SegmentId, ShowStyleBaseId, @@ -123,6 +124,11 @@ export enum IngestJobs { */ UserExecuteChangeOperation = 'userExecuteChangeOperation', + /** + * Playout executed a change operation + */ + PlayoutExecuteChangeOperation = 'playoutExecuteChangeOperation', + // For now these are in this queue, but if this gets split up to be per rundown, then a single bucket queue will be needed BucketItemImport = 'bucketItemImport', BucketItemRegenerate = 'bucketItemRegenerate', @@ -242,6 +248,12 @@ export interface UserExecuteChangeOperationProps extends IngestPropsBase { operation: { id: string; [key: string]: any } } +export interface PlayoutExecuteChangeOperationProps extends IngestPropsBase { + segmentId: SegmentId | null + partId: PartId | null + operation: unknown +} + export interface BucketItemImportProps { bucketId: BucketId showStyleBaseId: ShowStyleBaseId @@ -310,6 +322,7 @@ export type IngestJobFunc = { [IngestJobs.UserRemoveRundown]: (data: UserRemoveRundownProps) => void [IngestJobs.UserUnsyncRundown]: (data: UserUnsyncRundownProps) => void [IngestJobs.UserExecuteChangeOperation]: (data: UserExecuteChangeOperationProps) => void + [IngestJobs.PlayoutExecuteChangeOperation]: (data: PlayoutExecuteChangeOperationProps) => void [IngestJobs.BucketItemImport]: (data: BucketItemImportProps) => void [IngestJobs.BucketItemRegenerate]: (data: BucketItemRegenerateProps) => void diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 6eb045fc5e0..961c7b7dfd9 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -126,6 +126,12 @@ export enum StudioJobs { */ OnTimelineTriggerTime = 'onTimelineTriggerTime', + /** + * Recalculate T-Timer projections based on current playlist state + * Called after setNext, takes, and ingest changes to update timing anchor projections + */ + RecalculateTTimerProjections = 'recalculateTTimerProjections', + /** * Update the timeline with a regenerated Studio Baseline * Has no effect if a Playlist is active @@ -254,7 +260,8 @@ export interface ActivateRundownPlaylistProps extends RundownPlayoutPropsBase { } export type DeactivateRundownPlaylistProps = RundownPlayoutPropsBase export interface SetNextPartProps extends RundownPlayoutPropsBase { - nextPartId: PartId + nextPartId?: PartId + nextPartInstanceId?: PartInstanceId setManually?: boolean nextTimeOffset?: number } @@ -266,9 +273,9 @@ export interface QueueNextSegmentProps extends RundownPlayoutPropsBase { } export type QueueNextSegmentResult = { nextPartId: PartId } | { queuedSegmentId: SegmentId | null } export interface ExecuteActionProps extends RundownPlayoutPropsBase { - actionDocId: AdLibActionId | RundownBaselineAdLibActionId | BucketAdLibActionId + actionDocId: AdLibActionId | RundownBaselineAdLibActionId | BucketAdLibActionId | null actionId: string - userData: any + userData: any | null triggerMode?: string actionOptions?: { [key: string]: any } } @@ -380,6 +387,10 @@ export interface CleanupOrphanedExpectedPackageReferencesProps { rundownId: RundownId } +export interface TakeNextPartResult { + nextTakeTime: number +} + /** * Set of valid functions, of form: * `id: (data) => return` @@ -404,7 +415,7 @@ export type StudioJobFunc = { [StudioJobs.QueueNextSegment]: (data: QueueNextSegmentProps) => QueueNextSegmentResult [StudioJobs.ExecuteAction]: (data: ExecuteActionProps) => ExecuteActionResult [StudioJobs.ExecuteBucketAdLibOrAction]: (data: ExecuteBucketAdLibOrActionProps) => ExecuteActionResult - [StudioJobs.TakeNextPart]: (data: TakeNextPartProps) => void + [StudioJobs.TakeNextPart]: (data: TakeNextPartProps) => TakeNextPartResult [StudioJobs.DisableNextPiece]: (data: DisableNextPieceProps) => void [StudioJobs.RemovePlaylist]: (data: RemovePlaylistProps) => void [StudioJobs.RegeneratePlaylist]: (data: RegeneratePlaylistProps) => void @@ -412,6 +423,8 @@ export type StudioJobFunc = { [StudioJobs.OnPlayoutPlaybackChanged]: (data: OnPlayoutPlaybackChangedProps) => void [StudioJobs.OnTimelineTriggerTime]: (data: OnTimelineTriggerTimeProps) => void + [StudioJobs.RecalculateTTimerProjections]: () => void + [StudioJobs.UpdateStudioBaseline]: () => string | false [StudioJobs.CleanupEmptyPlaylists]: () => void diff --git a/packages/documentation/docs/about-sofie.md b/packages/documentation/docs/about-sofie.md index bb031408a1f..4edeccef038 100644 --- a/packages/documentation/docs/about-sofie.md +++ b/packages/documentation/docs/about-sofie.md @@ -5,18 +5,16 @@ sidebar_label: About Sofie sidebar_position: 1 --- -# NRK Sofie TV Automation System +# Sofie TV Automation System ![The producer's view in Sofie](https://raw.githubusercontent.com/Sofie-Automation/Sofie-TV-automation/main/images/Sofie_GUI_example.jpg) -_**Sofie**_ is a web-based TV automation system for studios and live shows, used in daily live TV news productions by the Norwegian public service broadcaster [**NRK**](https://www.nrk.no/about/) since September 2018. +_**Sofie**_ is a web-based TV automation system for studios and live shows. It has been used in daily live TV news productions since September 2018 by broadcasters such as [**NRK**](https://www.nrk.no/about/), the [**BBC**](https://www.bbc.com/aboutthebbc), and [**TV 2 (Norway)**](https://info.tv2.no/info/s/om-tv-2). ## Key Features - User-friendly, modern web-based GUI - State-based device control and playout of video, audio, and graphics -- Modular device-control architecture with support for several hardware \(and software\) setups -- Modular data-ingest architecture, supports MOS and Google spreadsheets +- Modular device-control architecture with support for various hardware and software setups +- Modular data-ingest architecture that supports MOS and Google spreadsheets - Plug-in architecture for programming shows - -_The NRK logo is a registered trademark of Norsk rikskringkasting AS. The license does not grant any right to use, in any way, any trademarks, service marks or logos of Norsk rikskringkasting AS._ diff --git a/packages/documentation/docs/for-developers/contribution-guidelines.md b/packages/documentation/docs/for-developers/contribution-guidelines.md index 00a5d894bd9..bc636057162 100644 --- a/packages/documentation/docs/for-developers/contribution-guidelines.md +++ b/packages/documentation/docs/for-developers/contribution-guidelines.md @@ -7,17 +7,21 @@ sidebar_position: 2 # Contribution Guidelines -_Last updated september 2024_ +_Last updated January 2026_ ## About the Sofie TV Studio Automation Project -The Sofie project includes a number of open source applications and libraries developed and maintained by the Norwegian public service broadcaster, [NRK](https://www.nrk.no/about/). Sofie has been used to produce live shows at NRK since September 2018. +The Sofie project includes a number of open source applications and libraries originally developed by the Norwegian public service broadcaster, [NRK](https://www.nrk.no/about/). Sofie has been used in daily live TV news productions since September 2018 by broadcasters such as [**NRK**](https://www.nrk.no/about/), the [**BBC**](https://www.bbc.com/aboutthebbc), and [**TV 2 (Norway)**](https://info.tv2.no/info/s/om-tv-2). -A list of the "Sofie repositories" [can be found here](libraries.md). NRK owns the copyright of the contents of the official Sofie repositories, including the source code, related files, as well as the Sofie logo. +A list of the "Sofie repositories" [can be found here](libraries.md). The Sofie Governance organisation owns the copyright of the contents of the official Sofie repositories, including the source code, related files, as well as the Sofie logo. -The Sofie team at NRK is responsible for development and maintenance. We also do thorough testing of each release to avoid regressions in functionality and ensure interoperability with the various hardware and software involved. +The Sofie Governance organisation is responsible for development and maintenance. We also do thorough testing of each release to avoid regressions in functionality and ensure interoperability with the various hardware and software involved. -The Sofie team welcomes open source contributions and will actively work towards enabling contributions to become mergeable into the Sofie repositories. However, as main stakeholder and maintainer we reserve the right to refuse any contributions. +The Sofie team welcomes open source contributions and will actively work towards enabling contributions to become mergeable into the Sofie repositories. However, we reserve the right to refuse any contributions. + +Sofie releases are targeted on a quarterly release cycle and are feature frozen six weeks before the release date, after which PRs that introduce new features are no longer accepted for that release. + +Three weeks before release, all PRs for that release should be merged to allow for testing and bug fixing before release. ## About Contributions @@ -29,7 +33,7 @@ Before you start, there are a few things you should know: **Minor changes** (most bug fixes and small features) can be submitted directly as pull requests to the appropriate official repo. -However, Sofie is a big project with many differing users and use cases. **Larger changes** may be difficult to merge into an official repository if NRK and other contributors have not been made aware of their existence beforehand. Since figuring out what side-effects a new feature or a change may have for other Sofie users can be tricky, we advise opening an RFC issue (_Request for Comments_) early in your process. Good moments to open an RFC include: +However, Sofie is a big project with many differing users and use cases. **Larger changes** may be difficult to merge into an official repository if the Sofie Governance team and other contributors have not been made aware of their existence beforehand. Since figuring out what side-effects a new feature or a change may have for other Sofie users can be tricky, we advise opening an RFC issue (_Request for Comments_) early in your process. Good moments to open an RFC include: - When a user need is identified and described - When you have a rough idea about how a feature may be implemented @@ -38,18 +42,19 @@ However, Sofie is a big project with many differing users and use cases. **Large To facilitate timely handling of larger contributions, there’s a workflow intended to keep an open dialogue between all interested parties: 1. Contributor opens an RFC (as a _GitHub issue_) in the appropriate repository. -2. NRK evaluates the RFC, usually within a week. -3. If needed, NRK establishes contact with the RFC author, who will be invited to a workshop where the RFC is discussed. Meeting notes are published publicly on the RFC thread. +2. The Sofie Technical Steering Committee (TSC) evaluates the RFC, usually within two weeks. +3. If needed, the TSC establishes contact with the RFC author, who will be invited to a workshop where the RFC is discussed. Meeting notes are published publicly on the RFC thread. 4. Discussions about the RFC continue as needed, either in workshops or in comments in the RFC thread. 5. The contributor references the RFC when a pull request is ready. -It will be very helpful if your RFC includes specific use-cases that you are facing. Providing a background on how your users are using Sofie can clear up situations in which certain phrases or processes may be ambiguous. If during your process you have already identified various solutions as favorable or unfavorable, offering this context will move the discussion further still. +It will be very helpful if your RFC includes specific use cases that you are facing. Providing a background on how your users are using Sofie can clear up situations in which certain phrases or processes may be ambiguous. If during your process you have already identified various solutions as favorable or unfavorable, offering this context will move the discussion further still. Via the RFC process, we're looking to maximize involvement from various stakeholders, so you probably don't need to come up with a very detailed design of your proposed change or feature in the RFC. An end-user oriented description will be most valuable in creating a constructive dialogue, but don't shy away from also adding a more technical description, if you find that will convey your ideas better. ### Base contributions on the in-development branch In order to facilitate merging, we ask that contributions are based on the latest (at the time of the pull request) _in-development_ branch (often named `release*`). + See **CONTRIBUTING.md** in each official repository for details on which branch to use as a base for contributions. ## Developer Guidelines @@ -66,6 +71,10 @@ All official Sofie repositories use TypeScript. When you contribute code, be sur Most of the projects use a linter (eslint) and a formatter (prettier). Before submitting a pull request, please make sure it conforms to the linting rules by running yarn lint. yarn lint --fix can fix most of the issues. +### Tests + +See **CONTRIBUTING.md** in each official repository for details on the level of unit tests required for contribution to that repository. + ### Documentation We rely on two types of documentation; the [Sofie documentation](https://sofie-automation.github.io/sofie-core/) ([source code](https://github.com/Sofie-Automation/sofie-core/tree/main/packages/documentation)) and inline code documentation. @@ -73,7 +82,7 @@ We rely on two types of documentation; the [Sofie documentation](https://sofie-a We don't aim to have the "absolute perfect documentation possible", BUT we do try to improve and add documentation to have a good-enough-to-be-comprehensible standard. We think that: - _What_ something does is not as important – we can read the code for that. -- _Why_ something does something, **is** important. Implied usage, side-effects, descriptions of the context etcetera... +- _Why_ something does something, **is** important. Implied usage, side-effects, descriptions of the context etc.... When you contribute, we ask you to also update any documentation where needed. @@ -83,7 +92,7 @@ When updating dependencies in a library, it is preferred to do so via `yarn upgr Be careful when bumping across major versions. -Also, each of the libraries has a minimum nodejs version specified in their package.json. Care must be taken when updating dependencies to ensure its compatibility is retained. +Also, each of the libraries has a minimum Node.js version specified in their package.json. Care must be taken when updating dependencies to ensure its compatibility is retained. ### Resolutions​ @@ -93,10 +102,10 @@ When updating other dependencies, it is a good idea to make sure that the resolu ### Logging -When logging, we try to adher to the following guideliness: +When logging, we try to adhere to the following guidelines: -Usage of `console.log` and `console.error` directly is discouraged (except for quick debugging locally). Instead, use one of the logger libraries (to output json logs which are easier to index). -When logging, use one of the **log level** described below: +Usage of `console.log` and `console.error` directly is discouraged (except for quick debugging locally). Instead, use one of the logger libraries (to output JSON logs which are easier to index). +When logging, use one of the **log levels** described below: | Level | Description | Examples | | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/packages/documentation/docs/for-developers/device-integrations/intro.md b/packages/documentation/docs/for-developers/device-integrations/intro.md index 35e256547ee..727613264a9 100644 --- a/packages/documentation/docs/for-developers/device-integrations/intro.md +++ b/packages/documentation/docs/for-developers/device-integrations/intro.md @@ -1,6 +1,6 @@ # Introduction -Device integrations in Sofie are part of the Timeline State Resolver (TSR) library. A device integration has a couple of responsibilities in the Sofie eco system. First and foremost it should establish a connection with a foreign device. It should also be able to convert Sofie's idea of what the device should be doing into commands to control the device. And lastly it should export interfaces to be used by the blueprints developer. +Device integrations in Sofie are part of the Timeline State Resolver (TSR) library. A device integration has a couple of responsibilities in the Sofie eco-system. First and foremost it should establish a connection with a foreign device. It should also be able to convert Sofie's idea of what the device should be doing into commands to control the device. And lastly it should export interfaces to be used by the blueprints developer. In order to understand all about writing TSR integrations there are some concepts to familiarise yourself with, in this documentation we will attempt to explain these. diff --git a/packages/documentation/docs/for-developers/device-integrations/tsr-plugins.md b/packages/documentation/docs/for-developers/device-integrations/tsr-plugins.md index 6682723f991..0258aa62208 100644 --- a/packages/documentation/docs/for-developers/device-integrations/tsr-plugins.md +++ b/packages/documentation/docs/for-developers/device-integrations/tsr-plugins.md @@ -1,6 +1,6 @@ # TSR Plugins -As of 1.53, it is possible to load additional device integrations into TSR as 'plugins'. This is intended to be an escape hatch when you need to make an integration for an internal system or for when an NDA with a device vendor does not allow for opensourcing. We still encourage anything which can be made opensource to be contributed back. +As of 26.03, it is possible to load additional device integrations into TSR as 'plugins'. This is intended to be an escape hatch when you need to make an integration for an internal system or for when an NDA with a device vendor does not allow for opensourcing. We still encourage anything which can be made opensource to be contributed back. ## Creating a plugin @@ -27,7 +27,7 @@ Some useful npm scripts you may wish to copy are: There are a few key properties that your plugin must conform to, the rest of the structure and how it gets generated is up to you. -1. It must be possible to `require(...)` your plugin folder. The resuling js must contain an export of the format `export const Devices: Record = {}` +1. It must be possible to `require(...)` your plugin folder. The resulting js must contain an export of the format `export const Devices: Record = {}` This is how the TSR process finds the entrypoint for your code, and allows you to define multiple device types. 2. There must be a `manifest.json` file at the root of your plugin folder. This should contain json in the form `Record` diff --git a/packages/documentation/docs/for-developers/for-blueprint-developers/intro.md b/packages/documentation/docs/for-developers/for-blueprint-developers/intro.md index a4b1ef62e6b..0dfe9486a1b 100644 --- a/packages/documentation/docs/for-developers/for-blueprint-developers/intro.md +++ b/packages/documentation/docs/for-developers/for-blueprint-developers/intro.md @@ -8,13 +8,30 @@ sidebar_position: 1 Documentation for this page is yet to be written. ::: -[Blueprints](../../user-guide/concepts-and-architecture.md#blueprints) are programs that run inside Sofie Core and interpret -data coming in from the Rundowns and transform that into playable elements. They use an API published in [@sofie-automation/blueprints-integration](https://sofie-automation.github.io/sofie-core/typedoc/modules/_sofie_automation_blueprints_integration.html) library to expose their functionality and communicate with Sofie Core. +[Blueprints](../../user-guide/concepts-and-architecture.md#blueprints) are JavaScript programs that run inside Sofie Core and interpret data coming in from the Rundowns and transform that into playable elements. They use an API published in [@sofie-automation/blueprints-integration](https://sofie-automation.github.io/sofie-core/typedoc/modules/_sofie_automation_blueprints_integration.html) [TypeScript](https://www.typescriptlang.org/) library to expose their functionality and communicate with Sofie Core. Technically, a Blueprint is a JavaScript object, implementing one of the `BlueprintManifestBase` interfaces. +Sofie doesn't have a built-in package manager or import, so all dependencies need to be bundled into a single `*.js` file bundle using a bundler such as [Rollup](https://rollupjs.org/) or [webpack](https://webpack.js.org/). The community has built a set of utilities called [SuperFlyTV/sofie-blueprint-tools](https://github.com/SuperFlyTV/sofie-blueprint-tools/) that acts as a nascent framework for building & bundling Blueprints written in TypeScript. + +:::info +Note that the Runtime Environment for Blueprints in Sofie is plain JavaScript at [ES2015 level](https://en.wikipedia.org/wiki/ECMAScript_version_history#6th_edition_%E2%80%93_ECMAScript_2015), so other ways of building Blueprints are also possible. +::: + Currently, there are three types of Blueprints: - [Show Style Blueprints](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.ShowStyleBlueprintManifest.html) - handling converting NRCS Rundown data into Sofie Rundowns and content. - [Studio Blueprints](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.StudioBlueprintManifest.html) - handling selecting ShowStyles for a given NRCS Rundown and assigning NRCS Rundowns to Sofie Playlists - [System Blueprints](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.SystemBlueprintManifest.html) - handling system provisioning and global configuration + +# Show Style Blueprints + +These blueprints interpret the data coming from the [NRCS](../../user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md), meaning that they need to support the particular data structures that a given Ingest Gateway uses to store incoming data from the Rundown editor. They will need to convert Rundown Pages, Cues, Items, pieces of show script and other types of objects into [Sofie concepts](../../user-guide/concepts-and-architecture.md) such as Segments, Parts, Pieces and AdLibs. + +# Studio Blueprints + +These blueprints provide a "baseline" Timeline that is being used by your Studio whenever there isn't a Rundown active. They also handle combining Rundowns into RundownPlaylists. Via the [`applyConfig`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie-automation_blueprints-integration.StudioBlueprintManifest.html#applyconfig) method, these Blueprints enable a _Configuration-as-Code_ approach to configuring connections to various elements of your Control Room and Studio. + +# System Blueprints + +These blueprints exist to allow a _Configuration-as-Code_ approach to an entire Sofie system. This is done via the [`applyConfig`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie-automation_blueprints-integration.SystemBlueprintManifest.html#applyconfig) providing personality information such as global system configuration or system-wide HotKeys via the Blueprints. \ No newline at end of file diff --git a/packages/documentation/docs/for-developers/for-blueprint-developers/prompter-text.md b/packages/documentation/docs/for-developers/for-blueprint-developers/prompter-text.md new file mode 100644 index 00000000000..f35eb684168 --- /dev/null +++ b/packages/documentation/docs/for-developers/for-blueprint-developers/prompter-text.md @@ -0,0 +1,74 @@ +# Prompter Text Formatting + +The Sofie Prompter supports formatted text using simple inline markers. This formatting is displayed both in the prompter view and in hover previews throughout the Sofie UI. + +Providing formatted text is optional, Sofie will use the un-formatted version when only that is provided. + +## Emphasis and Strong + +Wrap text to add emphasis or bold: + +- `*italic*` or `_italic_` → _italic_ +- `**bold**` or `__bold__` → **bold** + +```text +This is *emphasized text* and this is **strong text**. +``` + +## Invert Color + +Invert the text colour (swap foreground/background): + +```text +Show ~reversed~ for emphasis. +``` + +## Hidden Text + +Hide text from display using `|` or `$` — useful for notes or off-script remarks: + +```text +Begin the speech |remember to smile| then continue. +``` + +## Underline + +Use double markers `||` or `$$` to underline text: + +```text +This word is ||underlined|| for emphasis. +``` + +## Colour + +Apply colour using `[colour=#hex]...[/colour]`: + +```text +[colour=#ffff00]This text appears in yellow[/colour] +[colour=#ff0000]This text appears in red[/colour] +``` + +## Screen Marker + +Insert a screen marker for teleprompter control using `(X)`: + +```text +Begin speech (X) pause here, then continue. +``` + +## Escaping + +Prefix any special character with `\` to display it literally: + +```text +This is \*not italic\* and this is \~not reversed\~. +``` + +## Full Example + +```text +Good morning, *everyone*. +|Don't forget the greeting| Welcome to the ||annual conference||. +[colour=#ffff00]Please note[/colour] the schedule has changed. (X) +For questions, contact us at example\@email.com. +``` diff --git a/packages/documentation/docs/for-developers/for-blueprint-developers/sync-ingest-changes.md b/packages/documentation/docs/for-developers/for-blueprint-developers/sync-ingest-changes.md index 7c609400d29..05eceb4b0d0 100644 --- a/packages/documentation/docs/for-developers/for-blueprint-developers/sync-ingest-changes.md +++ b/packages/documentation/docs/for-developers/for-blueprint-developers/sync-ingest-changes.md @@ -12,11 +12,11 @@ In this blueprint method, you are able to update almost any of the properties th ### Tips -- You should make use of the `metaData` fields on each Part and Piece to help work out what has changed. At NRK, we store the parsed ingest data (after converting the MOS to an intermediary json format) for the Part here, so that we can do a detailed diff to figure out whether a change is safe to accept. +- You should make use of the `metaData` fields on each Part and Piece to help work out what has changed. At NRK, the parsed ingest data is stored (after converting the MOS to an intermediary json format) for the Part here, so that we can do a detailed diff to figure out whether a change is safe to accept. - You should track in `metaData` whether a part has been modified by an adlib-action in a way that makes this sync unsafe. -- At NRK, we differentiate the Pieces into `primary`, `secondary`, `adlib`. This allows us to control the updates more granularly. +- At NRK, Pieces are differentiated into `primary`, `secondary`, `adlib`. This allows more granular control of updates. - `newData.part` will be `undefined` when the PartInstance is orphaned. Generally, it's useful to differentiate the behavior of the implementation of this function based on `existingPartInstance.partInstance.orphaned` state diff --git a/packages/documentation/docs/for-developers/libraries.md b/packages/documentation/docs/for-developers/libraries.md index c131c5ebdcf..98711be84af 100644 --- a/packages/documentation/docs/for-developers/libraries.md +++ b/packages/documentation/docs/for-developers/libraries.md @@ -46,10 +46,10 @@ There are also a few typings-only libraries that define interfaces between appli ## Other Sofie-related Repositories -- [**CasparCG Server** \(NRK fork\)](https://github.com/nrkno/sofie-casparcg-server) Sofie-specific fork of CasparCG Server. +- [**CasparCG Server**](https://github.com/CasparCG/server) CasparCG Server. - [**CasparCG Launcher**](https://github.com/Sofie-Automation/sofie-casparcg-launcher) Launcher, controller, and logger for CasparCG Server. -- [**CasparCG Media Scanner** \(NRK fork\)](https://github.com/nrkno/sofie-casparcg-server) Sofie-specific fork of CasparCG Server 2.2 Media Scanner. +- [**CasparCG Media Scanner**](https://github.com/CasparCG/media-scanner) CasparCG Media Scanner. - [**Sofie Chef**](https://github.com/Sofie-Automation/sofie-chef) A simple Chromium based renderer, used for kiosk mode rendering of web pages. - [**Quantel Browser Plugin**](https://github.com/Sofie-Automation/sofie-quantel-browser-plugin) MOS-compatible Quantel video clip browser for use with Sofie. -- [**Sisyfos Audio Controller**](https://github.com/nrkno/sofie-sisyfos-audio-controller) _developed by [*olzzon*](https://github.com/olzzon/)_ +- [**Sisyfos Audio Controller**](https://github.com/Sofie-Automation/sofie-sisyfos-audio-controller) _developed by [*olzzon*](https://github.com/olzzon/)_ - [**Quantel Gateway**](https://github.com/Sofie-Automation/sofie-quantel-gateway) CORBA to REST gateway for _Quantel/ISA_ playback. diff --git a/packages/documentation/docs/user-guide/concepts-and-architecture.md b/packages/documentation/docs/user-guide/concepts-and-architecture.md index 5716bdebca9..76adc563187 100644 --- a/packages/documentation/docs/user-guide/concepts-and-architecture.md +++ b/packages/documentation/docs/user-guide/concepts-and-architecture.md @@ -156,7 +156,7 @@ Another benefit of basing the playout on a timeline is that when programming the ### How does it work? :::tip -Fun tip! The timeline in itself is a [separate library available on github](https://github.com/SuperFlyTV/supertimeline). +Fun tip! The timeline in itself is a [separate library available on GitHub](https://github.com/SuperFlyTV/supertimeline). You can play around with the timeline in the browser using [JSFiddle and the timeline-visualizer](https://jsfiddle.net/nytamin/rztp517u/)! ::: diff --git a/packages/documentation/docs/user-guide/configuration/settings-view.md b/packages/documentation/docs/user-guide/configuration/settings-view.md index fd9fbfa6e06..9fdde7b9a36 100644 --- a/packages/documentation/docs/user-guide/configuration/settings-view.md +++ b/packages/documentation/docs/user-guide/configuration/settings-view.md @@ -52,7 +52,7 @@ The clean up process in Sofie will search the database for unused data and index ## Studio -A _Studio_ in Sofie-terms is a physical location, with a specific set of devices and equipment. Only one show can be on air in a studio at the same time. +A _Studio_ in Sofie-terms is a physical location, with a specific set of devices and equipment. Only one show can be on air in a studio at the same time. The _studio_ settings are settings for that specific studio, and contains settings related to hardware and playout, such as: - **Attached devices** - the Gateways related to this studio @@ -113,7 +113,7 @@ Route Sets can also be configured with a _Default State_. This can be used to co ## Show style -A _Showstyle_ is related to the looks and logic of a _show_, which in contrast to the _studio_ is not directly related to the hardware. +A _Showstyle_ is related to the looks and logic of a _show_, which in contrast to the _studio_ is not directly related to the hardware. The Showstyle contains settings like - **Source Layers** - Groups different types of content in the GUI @@ -126,7 +126,7 @@ Please note the difference between _Source Layers_ and _timeline-layers_: [Pieces](../concepts-and-architecture.md#piece) are put onto _Source layers_, to group different types of content \(such as a VT or Camera\), they are therefore intended only as something to indicate to the user what is going to be played, not what is actually going to happen on the technical level. -[Timeline-objects](../concepts-and-architecture.md#timeline-object) \(inside of the [Pieces](../concepts-and-architecture.md#piece)\) are put onto timeline-layers, which are \(through the Mappings in the studio\) mapped to physical devices and outputs. +[Timeline-objects](../concepts-and-architecture.md#timeline-object) \(inside of the [Pieces](../concepts-and-architecture.md#piece)\) are put onto timeline-layers, which are \(through the Mappings in the studio\) mapped to physical devices and outputs. The exact timeline-layer is never exposed to the user, but instead used on the technical level to control playout. An example of the difference could be when playing a VT \(that's a Source Layer\), which could involve all of the timeline-layers _video_player0_, _audio_fader_video_, _audio_fader_host_ and _mixer_pgm._ @@ -169,7 +169,7 @@ Hotkeys are valid in the scope of a browser window and can be either a single ke To edit a given trigger, click on the trigger pill on the left of the Trigger-Action set. When hovering, a **+** sign will appear, allowing you to add a new trigger to the set. -Device Triggers are valid in the scope of a Studio and will be evaluated on the currently active Rundown in a given Studio. To use Device Triggers, you need to have at least a single [Input Gateway](../installation/installing-input-gateway) attached to a Studio and a Device configured in the Input Gateway. Once that's done, when selecting a **Device** trigger type in the pop-up, you can invoke triggers on your Input Device and you will see a preview of the input events shown at the bottom of the pop-up. You can select which of these events should be the trigger by clicking on one of the previews. Note, that some devices differentiate between _Up_ and _Down_ triggers, while others don't. Some may also have other activities that can be done _to_ a trigger. What they are and how they are identified is device-specific and is best discovered through interaction with the device. +Device Triggers are valid in the scope of a Studio and will be evaluated on the currently active Rundown in a given Studio. To use Device Triggers, you need to have at least a single [Input Gateway](../installation/installing-a-gateway/input-gateway.md) attached to a Studio and a Device configured in the Input Gateway. Once that's done, when selecting a **Device** trigger type in the pop-up, you can invoke triggers on your Input Device and you will see a preview of the input events shown at the bottom of the pop-up. You can select which of these events should be the trigger by clicking on one of the previews. Note, that some devices differentiate between _Up_ and _Down_ triggers, while others don't. Some may also have other activities that can be done _to_ a trigger. What they are and how they are identified is device-specific and is best discovered through interaction with the device. If you would like to set up combination Triggers, using Device Triggers on an Input Device that does not support them natively, you may want to look into [Shift Registers](#shift-registers) @@ -189,7 +189,7 @@ Clicking on the action and filter pills allows you to edit the action parameters ##### Shift Registers -Shift Register modification actions are a special type of an Action, that modifies an internal state memory of the [Input Gateway](../installation/installing-input-gateway.md) and allows combination triggers, pagination, etc. on devices that don't natively support them or combining multiple devices into a single Control Surface. Refer to _Input Gateway_ documentation for more information on Shift Registers. +Shift Register modification actions are a special type of an Action, that modifies an internal state memory of the [Input Gateway](../installation/installing-a-gateway/input-gateway.md) and allows combination triggers, pagination, etc. on devices that don't natively support them or combining multiple devices into a single Control Surface. Refer to _Input Gateway_ documentation for more information on Shift Registers. Shift Register actions have no effect in the browser, triggered from a _Hotkey_. diff --git a/packages/documentation/docs/user-guide/features/intro.md b/packages/documentation/docs/user-guide/features/intro.md new file mode 100644 index 00000000000..0e68787f702 --- /dev/null +++ b/packages/documentation/docs/user-guide/features/intro.md @@ -0,0 +1,18 @@ +--- +sidebar_position: 1 +--- +# Introduction + +This section documents the user-facing features of Sofie, that is: what is visible in the User Interface when connected to the Sofie Web App. For more information about the playout features of Sofie, see the [For Blueprint Developers](../../for-developers/for-blueprint-developers/intro) section. + +The _Rundowns_ view will display all the active rundowns that the _Sofie Core_ has access to. + +![Rundown View](/img/docs/getting-started/rundowns-in-sofie.png) + +The _Status_ view displays the current status for the attached devices and gateways. + +![Status View – Describes the state of _Sofie Core_](/img/docs/getting-started/status-page.jpg) + +The _Settings_ view contains various settings for the Studio, Show Styles, Blueprints etc. If the link to the settings view is not visible in your application, check your [Access Levels](access-levels.md). More info on specific parts of the _Settings_ view can be found in their corresponding guide sections. + +![Settings View – Describes how the _Sofie Core_ is configured](/img/docs/getting-started/settings-page.jpg) \ No newline at end of file diff --git a/packages/documentation/docs/user-guide/features/prompter.md b/packages/documentation/docs/user-guide/features/prompter.md index aba0e34ec04..85f69e3eb80 100644 --- a/packages/documentation/docs/user-guide/features/prompter.md +++ b/packages/documentation/docs/user-guide/features/prompter.md @@ -42,11 +42,11 @@ The prompter can be controlled by different types of controllers. The control mo | Default | Controlled by both mouse and keyboard | | `?mode=mouse` | Controlled by mouse only. [See configuration details](prompter.md#control-using-mouse-scroll-wheel) | | `?mode=keyboard` | Controlled by keyboard only. [See configuration details](prompter.md#control-using-keyboard) | -| `?mode=shuttlekeyboard` | Controlled by a Contour Design ShuttleXpress, X-keys Jog and Shuttle or any compatible, configured as keyboard-ish device. [See configuration details](prompter.md#control-using-contour-shuttlexpress-or-x-keys-modeshuttlekeyboard) | +| `?mode=shuttlekeyboard` | Controlled by a Contour Design ShuttleXpress, X-keys Jog and Shuttle or any compatible, configured as keyboard-ish device. [See configuration details](prompter.md#control-using-contour-shuttlexpress-or-x-keys-modeshuttlekeyboard) | | `?mode=shuttlewebhid` | Controlled by a Contour Design ShuttleXpress, using the browser's WebHID API [See configuration details](prompter.md#control-using-contour-shuttlexpress-via-webhid) | | `?mode=pedal` | Controlled by any MIDI device outputting note values between 0 - 127 of CC notes on channel 8. Analogue Expression pedals work well with TRS-USB midi-converters. [See configuration details](prompter.md#control-using-midi-input-modepedal) | -| `?mode=joycon` | Controlled by Nintendo Switch Joycon, using the HTML5 GamePad API. [See configuration details](prompter.md#control-using-nintendo-joycon-gamepad) | -| `?mode=xbox` | Controlled by Xbox controller, using the HTML5 GamePad API. [See configuration details](prompter.md#control-using-xbox-controller-modexbox) | +| `?mode=joycon` | Controlled by Nintendo Switch Joycon, using the HTML5 GamePad API. [See configuration details](prompter.md#control-using-nintendo-joycon-modejoycon) | +| `?mode=xbox` | Controlled by Xbox controller, using the HTML5 GamePad API. [See configuration details](prompter.md#control-using-xbox-controller-modexbox) | #### Control using mouse \(scroll wheel\) @@ -104,6 +104,21 @@ When opening the Prompter View for the first time, it is necessary to press the ![Contour ShuttleXpress input mapping](/img/docs/main/features/contour-shuttle-webhid.jpg) +**Customizable Button Mapping:** + +By default, the ShuttleXpress buttons execute built-in prompter actions. However, you can customize button behavior by mapping buttons to global adlib actions using the `shuttleWebHid_buttonMap` query parameter. + +| Query parameter | Type | Description | +| :--- | :--- | :--- | +| `shuttleWebHid_buttonMap` | Comma-separated strings | Maps ShuttleXpress buttons to global adlib actions. Each entry should be in the format `buttonIndex:actionId`, where `buttonIndex` is the button number (0-indexed) and `actionId` is the ID of a global adlib action defined in your blueprints. Each custom action is triggered once on button press (trigger mode: `pressed`) and once on button release (trigger mode: `released`). Multiple mappings are comma-separated. | + +**Example:** +``` +?mode=shuttlewebhid&shuttleWebHid_buttonMap=0:toggle_control_room_mics,1:make_coffee +``` + +Buttons without a custom mapping will use their default behavior. + #### #### Control using midi input \(_?mode=pedal_\) @@ -160,16 +175,16 @@ This mode uses the browsers Gamapad API and polls connected Joycons for their st The Joycons can operate in 3 modes, the L-stick, the R-stick or both L+R sticks together. Reconnections and jumping between modes works, with one known limitation: **Transition from L+R to a single stick blocks all input, and requires a reconnect of the sticks you want to use.** This seems to be a bug in either the Joycons themselves or in the Gamepad API in general. -| Query parameter | Type | Description | Default | -| :----------------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | +| Query parameter | Type | Description | Default | +| :----------------------- | :--------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | | `joycon_speedMap` | Array of numbers | Speeds to scroll by \(px. pr. frame - approx 60fps\) when scrolling forwards. The beginning of the forwards-range maps to the first number in this array, and the end of the forwards-range map to the end of this array. All values in between are being interpolated in a spline curve. | `[1, 2, 3, 4, 5, 8, 12, 30]` | -| `joycon_reverseSpeedMap` | Array of numbers | Same as `joycon_speedMap` but for the backwards range. | `[1, 2, 3, 4, 5, 8, 12, 30]` | -| `joycon_rangeRevMin` | number | The end of the backwards-range, full speed backwards. | `-1` | -| `joycon_rangeNeutralMin` | number | The beginning of the backwards-range. | `-0.25` | -| `joycon_rangeNeutralMax` | number | The minimum input to run forward, the start of the forward-range \(min speed\). This is also the end of any "deadband" you want filter out before starting moving forwards. | `0.25` | -| `joycon_rangeFwdMax` | number | The maximum input, the end of the forward-range \(max speed\) | `1` | -| `joycon_rightHandOffset` | number | A ratio to increase or decrease the R Joycon joystick sensitivity relative to the L Joycon. | `1.4` | -| `joycon_invertJoystick` | 0 / 1 | Invert the joystick direction. When enabled, pushing the joystick forward scrolls up instead of down. | `1` | +| `joycon_reverseSpeedMap` | Array of numbers | Same as `joycon_speedMap` but for the backwards range. | `[1, 2, 3, 4, 5, 8, 12, 30]` | +| `joycon_rangeRevMin` | number | The end of the backwards-range, full speed backwards. | `-1` | +| `joycon_rangeNeutralMin` | number | The beginning of the backwards-range. | `-0.25` | +| `joycon_rangeNeutralMax` | number | The minimum input to run forward, the start of the forward-range \(min speed\). This is also the end of any "deadband" you want filter out before starting moving forwards. | `0.25` | +| `joycon_rangeFwdMax` | number | The maximum input, the end of the forward-range \(max speed\) | `1` | +| `joycon_rightHandOffset` | number | A ratio to increase or decrease the R Joycon joystick sensitivity relative to the L Joycon. | `1.4` | +| `joycon_invertJoystick` | 0 / 1 | Invert the joystick direction. When enabled, pushing the joystick forward scrolls up instead of down. | `1` | - `joycon_rangeNeutralMin` has to be greater than `joycon_rangeRevMin` - `joycon_rangeNeutralMax` has to be greater than `joycon_rangeNeutralMin` @@ -226,11 +241,11 @@ The controller can be connected via Bluetooth or USB. **Note:** On macOS, Xbox c **Configuration parameters:** -| Query parameter | Type | Description | Default | -| :--------------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | +| Query parameter | Type | Description | Default | +| :--------------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------- | | `xbox_speedMap` | Array of numbers | Speeds to scroll by (px per frame, ~60fps) when scrolling forwards. Values are interpolated using a spline curve based on trigger pressure. | `[2, 3, 5, 6, 8, 12, 18, 45]` | | `xbox_reverseSpeedMap` | Array of numbers | Same as `xbox_speedMap` but for the backwards range (left trigger). | `[2, 3, 5, 6, 8, 12, 18, 45]` | -| `xbox_triggerDeadZone` | number | Dead zone for the triggers, to prevent accidental scrolling. Value between 0 and 1. | `0.1` | +| `xbox_triggerDeadZone` | number | Dead zone for the triggers, to prevent accidental scrolling. Value between 0 and 1. | `0.1` | You can turn on `?debug=1` to see how your trigger input maps to scroll speed. diff --git a/packages/documentation/docs/user-guide/features/sofie-views-and-screens.mdx b/packages/documentation/docs/user-guide/features/sofie-views-and-screens.mdx index f0202708e18..328d377d0b9 100644 --- a/packages/documentation/docs/user-guide/features/sofie-views-and-screens.mdx +++ b/packages/documentation/docs/user-guide/features/sofie-views-and-screens.mdx @@ -218,9 +218,9 @@ onto a playout system. ![Media Status View](/img/docs/main/features/media-status.png) By default, the Media items are sorted according to their position in the -rundown, and the rundowns are in the same order as in the [Lobby View] -(#lobby-view). You can change the sorting order by clicking on the buttons in -the table header. +rundown, and the rundowns are in the same order as in the +[Lobby View](#lobby-view). You can change the sorting order by clicking on +the buttons in the table header. The Rundown View also has a panel that presents this information in the [context of the current Rundown](#media-status-panel). @@ -254,8 +254,7 @@ The Configurable Screens section uses collapsible accordion panels that let you **Camera Screen Configuration** - Filter by specific Source Layer IDs (e.g., cameras, DVEs) - Filter by Studio Labels to show only relevant cameras -- Enable fullscreen mode for mobile devices -- Generates URL with `sourceLayerIds`, `studioLabels`, and `fullscreen` parameters +- Generates URL with `sourceLayerIds` and `studioLabels` parameters **Prompter Configuration** - Configure display options (mirroring, font size, margins, read marker position) @@ -271,6 +270,8 @@ Bookmark the "Available screens" view for your studio (e.g., `/countdowns/studio ## Sofie Screens +All Screens support a `?fullscreen=1` query parameter. When this parameter is present, the screen will display a semi-transparent overlay prompting the user to click anywhere to enter fullscreen mode. Once fullscreen is entered, the overlay disappears. If the user exits fullscreen, the overlay will reappear. + ### Prompter Screen `/prompter/:studioId` @@ -335,7 +336,6 @@ This screen can be configured using query parameters: | :--------------- | :----- | :--------------------------------------------------------------------------------------------------------- | :----------- | | `sourceLayerIds` | string | A comma-separated list of Source Layer IDs to be considered for display | _(show all)_ | | `studioLabels` | string | A comma-separated list of Studio Labels (Piece `.content.studioLabel` values) to be considered for display | _(show all)_ | -| `fullscreen` | 0 / 1 | Should the screen be shown fullscreen on the device on first user interaction | 0 | Example: [http://127.0.0.1/countdowns/studio0/camera?sourceLayerIds=camera0,dve0&studioLabels=1,KAM%201,K1,KAM1&fullscreen=1](http://127.0.0.1/countdowns/studio0/camera?sourceLayerIds=camera0,dve0&studioLabels=1,KAM%201,K1,KAM1&fullscreen=1) diff --git a/packages/documentation/docs/user-guide/further-reading.md b/packages/documentation/docs/user-guide/further-reading.md index ad6c64748e8..22c0d3b3e93 100644 --- a/packages/documentation/docs/user-guide/further-reading.md +++ b/packages/documentation/docs/user-guide/further-reading.md @@ -39,10 +39,10 @@ description: This guide has a lot of links. Here they are all listed by section. #### Installing CasparCG Server for Sofie -- NRK's version of [CasparCG Server](https://github.com/nrkno/sofie-casparcg-server/releases) on GitHub. -- [Media Scanner](https://github.com/Sofie-Automation/sofie-casparcg-launcher/releases) on GitHub. -- [CasparCG Launcher](https://github.com/Sofie-Automation/sofie-casparcg-launcher) on GitHub. -- [Microsoft Visual C++ 2015 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=52685) on Microsoft's website. +- [CasparCG Server](https://github.com/CasparCG/server/releases) on GitHub. +- [Media Scanner](https://github.com/CasparCG/media-scanner/releases) on GitHub. +- [CasparCG Launcher](https://github.com/Sofie-Automation/sofie-casparcg-launcher/releases) on GitHub. +- [Microsoft Visual C++ 2017 Redistributable](https://aka.ms/vc14/vc_redist.x64.exe) on Microsoft's website. - [Blackmagic Design's DeckLink Cards](https://www.blackmagicdesign.com/products/decklink/models) on Blackmagic Design's website. Check the [DeckLink cards](installation/installing-connections-and-additional-hardware/casparcg-server-installation.md#decklink-cards) section for compatibility. - [Installing a DeckLink Card](https://documents.blackmagicdesign.com/UserManuals/DesktopVideoManual.pdf) as a PDF. - [Blackmagic Design 'Desktop Video' Driver Download](https://www.blackmagicdesign.com/support/family/capture-and-playback) on Blackmagic Design's website. diff --git a/packages/documentation/docs/user-guide/installation/initial-sofie-core-setup.md b/packages/documentation/docs/user-guide/installation/initial-sofie-core-setup.md index 91ed165f896..12cef7df14e 100644 --- a/packages/documentation/docs/user-guide/installation/initial-sofie-core-setup.md +++ b/packages/documentation/docs/user-guide/installation/initial-sofie-core-setup.md @@ -1,12 +1,12 @@ --- -sidebar_position: 3 +sidebar_position: 30 --- # Initial Sofie Core Setup #### Prerequisites -- [Installed and running _Sofie Core_](installing-sofie-server-core.md) +* [Installed and running _Sofie Core_](quick-install.md) Once _Sofie Core_ has been installed and is running you can begin setting it up. The first step is to navigate to the _Settings page_. Please review the [Sofie Access Level](../features/access-levels.md) page for assistance getting there. diff --git a/packages/documentation/docs/user-guide/installation/installing-a-gateway/_category_.json b/packages/documentation/docs/user-guide/installation/installing-a-gateway/_category_.json index e83c1db9e5a..ab70e591ba6 100644 --- a/packages/documentation/docs/user-guide/installation/installing-a-gateway/_category_.json +++ b/packages/documentation/docs/user-guide/installation/installing-a-gateway/_category_.json @@ -1,4 +1,4 @@ { - "label": "Installing a Gateway", - "position": 5 -} + "label": "Installing a Gateway", + "position": 50 +} \ No newline at end of file diff --git a/packages/documentation/docs/user-guide/installation/installing-a-gateway/input-gateway.md b/packages/documentation/docs/user-guide/installation/installing-a-gateway/input-gateway.md new file mode 100644 index 00000000000..eeb3dc03600 --- /dev/null +++ b/packages/documentation/docs/user-guide/installation/installing-a-gateway/input-gateway.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 40 +--- + +# Input Gateway + +The Input Gateway handles control devices that are not capable of running a Web Browser. This allows Sofie to integrate directly with devices such as: Hardware Panels, GPI input, MIDI devices and external systems being able to send an HTTP Request. + +To install it, begin by downloading the latest release of [Input Gateway from GitHub](https://github.com/Sofie-Automation/sofie-input-gateway/releases). You can now run the `input-gateway.exe` file inside the extracted folder. A warning window may popup about the app being unrecognized. You can get around this by selecting _More Info_ and clicking _Run Anyways_. + +Much like [Package Manager](../installing-package-manager.md), the Sofie instance that Input Gateway needs to connect to is configured through command line arguments. A minimal configuration could look something like this. + +```bash +input-gateway.exe --host --port --https --id --token +``` + +If not connecting over HTTPS, remove the `--https` flag. + +Input Gateway can be launched from [CasparCG Launcher](../installing-connections-and-additional-hardware/casparcg-server-installation#installing-the-casparcg-launcher). This will make management and log collection easier on a production system. + +You can now open the _Sofie Core_, `http://localhost:3000`, and navigate to the _Settings page_. You will see your _Input Gateway_ under the _Devices_ section of the menu. In _Input Devices_ you can add devices that this instance of Input Gateway should handle. Some of the device integrations will allow you to customize the Feedback behavior. The _Device ID_ property will identify a given Input Device in the Studio, so this property can be used for fail-over purposes. + +## Supported devices and protocols + +Currently, input gateway supports: + +- Stream Deck panels +- Skaarhoj panels - _TCP Raw Panel_ mode +- X-Keys panels +- MIDI controllers +- OSC +- HTTP + +## Input Gateway-specific functions + +### Shift Registers + +Input Gateway supports the concept of _Shift Registers_. A Shift Register is an internal variable/state that can be modified using Actions, from within [Action Triggers](../../configuration/settings-view.md#actions). This allows for things such as pagination, _Hold Shift + Another Button_ scenarios, and others on input devices that don't support these features natively. _Shift Registers_ are also global for all devices attached to a single Input Gateway. This allows combining multiple Input devices into a single Control Surface. + +When one of the _Shift Registers_ is set to a value other than `0` (their default state), all triggers sent from that Input Gateway become prefixed with a serialized state of the state registers, making the combination of a _Shift Registers_ state and a trigger unique. + +If you would like to have the same trigger cause the same action in various Shift Register states, add multiple Triggers to the same Action, with different Shift Register combinations. + +Input Gateway supports an unlimited number of Shift Registers, Shift Register numbering starts at 0. + +### AdLib Tally + +Starting with version 0.5.0, Input Gateway can show additional information about the playout state of AdLibs. Select device integrations within Input Gateway support _Styles_ which allow elements of the HID devices to be specifically styled. These Style classes are matched with [Action Triggers](../../configuration/settings-view.md#action-triggers) using Style class names. You can configure additional _Style classes_ for when a given AdLib is "active" (currently playing) or "next" (i.e. will be playing after a take) appending a suffix `:active` and `:next` to a Style class name. + +### Further Reading + +- [Input Gateway Releases on GitHub](https://github.com/Sofie-Automation/sofie-input-gateway/releases) +- [Input Gateway GitHub Page for Developers](https://github.com/Sofie-Automation/sofie-input-gateway) diff --git a/packages/documentation/docs/user-guide/installation/installing-a-gateway/intro.md b/packages/documentation/docs/user-guide/installation/installing-a-gateway/intro.md index 461bb6804bf..58c96512ad4 100644 --- a/packages/documentation/docs/user-guide/installation/installing-a-gateway/intro.md +++ b/packages/documentation/docs/user-guide/installation/installing-a-gateway/intro.md @@ -1,26 +1,41 @@ --- sidebar_label: Introduction -sidebar_position: 1 +sidebar_position: 10 --- - # Introduction: Installing a Gateway #### Prerequisites -- [Installed and running Sofie Core](../installing-sofie-server-core.md) +* [Installed and running Sofie Core](../quick-install.md) -The _Sofie Core_ is the primary application for managing the broadcast, but it doesn't play anything out on it's own. A Gateway will establish the connection from _Sofie Core_ to other pieces of hardware or remote software. A basic setup may include the [Spreadsheet Gateway](rundown-or-newsroom-system-connection/installing-sofie-with-google-spreadsheet-support.md) which will ingest a rundown from Google Sheets then, use the [Playout Gateway](playout-gateway.md) send commands to a CasparCG Server graphics playout, an ATEM vision mixer, and / or the [Sisyfos audio controller](https://github.com/olzzon/sisyfos-audio-controller). +The _Sofie Core_ is the primary application for managing the broadcast, but it doesn't play anything out on it's own. A Gateway will establish the connection from _Sofie Core_ to other pieces of hardware or remote software. A basic setup may include the [Spreadsheet Gateway](rundown-or-newsroom-system-connection/google-spreadsheet.md) which will ingest a rundown from Google Sheets then, use the [Playout Gateway](playout-gateway.md) send commands to a CasparCG Server graphics playout, an ATEM vision mixer, and / or the [Sisyfos audio controller](https://github.com/olzzon/sisyfos-audio-controller). + -### Rundown & Newsroom Gateways +Setting up a gateway (also called Peripheral Device) from scratch generally is a five-step process: +1. Start the executable image and have it connect to Sofie Core +2. Assign the new Peripheral Device to a Studio +3. Configure the gateway inside the Sofie user interface, configure *sub-devices* \(MOS primary & secondary, video mixers, playout servers, HMI devices\) if applicable +4. Restart the gateway to apply the new settings +5. Verify connection on the *Status* page in Sofie + +:::tip +You can expect the initial connection in Step 1 to fail. This is expected. Peripheral Devices cannot be connected to Sofie unless they are assigned to a Studio. This initial connection is required to inform Sofie about the capabilities of the gateway and set up authorization tokens that will be expected by Sofie in subsequent connections. Do not be discouraged by the gateway shutting down or restarting and just follow the steps above as described. +::: -- [Google Spreadsheet Gateway](rundown-or-newsroom-system-connection/installing-sofie-with-google-spreadsheet-support.md) -- [iNEWS Gateway](rundown-or-newsroom-system-connection/inews-gateway.md) -- [MOS Gateway](rundown-or-newsroom-system-connection/mos-gateway.md) +### Gateways and their types and functions + +* [Playout Gateway](playout-gateway.md) - sends commands and modifies the state of devices in your Control Room and Studio: video servers, mixers, LED screens, lighting controllers & graphics systems +* [Package Manager](../installing-package-manager.md) - checks if media required for a successful production is where it should be, produces proxy versions for preview inside of Rundown View, does quality control of the media and provides feedback to the Blueprints and the User +* [Input Gateway](input-gateway.md) - receives signals from and provides support for *Human Interface Devices* devices such as Stream Decks, Skaarhoj panels and MIDI devices +* Live Status Gateway - provides support for external services that would like to know about the state of a Studio in Sofie, incl. currently playing Parts and Pieces, available AdLibs, etc. + +### Rundown & Newsroom Gateways -### Playout & Package Manager Gateways +* [Google Spreadsheet Gateway](rundown-or-newsroom-system-connection/google-spreadsheet.md) - supports creating Rundowns inside of Google Spreadsheet cloud service +* [iNEWS Gateway](rundown-or-newsroom-system-connection/inews-gateway.md) - integrates with Avid iNEWS via FTP +* [MOS Gateway](rundown-or-newsroom-system-connection/mos-gateway.md) - integrates with MOS-compatible NRCS systems (AP ENPS, CGI OpenMedia, Octopus Newsroom, Saga, among others) +* [Rundown Editor](../rundown-editor.md) - a minimal, self-contained Rundown creation utility -- [Playout Gateway](playout-gateway.md) -- [Package Manager](../installing-package-manager.md) -- [Input Gateway](../installing-input-gateway.md) diff --git a/packages/documentation/docs/user-guide/installation/installing-a-gateway/playout-gateway.md b/packages/documentation/docs/user-guide/installation/installing-a-gateway/playout-gateway.md index e900c2d6c66..5f4275a19bb 100644 --- a/packages/documentation/docs/user-guide/installation/installing-a-gateway/playout-gateway.md +++ b/packages/documentation/docs/user-guide/installation/installing-a-gateway/playout-gateway.md @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 30 --- # Playout Gateway diff --git a/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json b/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json index ae77b5c6a73..d0518625047 100644 --- a/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json +++ b/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json @@ -1,4 +1,4 @@ { "label": "Rundown or Newsroom System Connection", - "position": 4 -} + "position": 15 +} \ No newline at end of file diff --git a/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/installing-sofie-with-google-spreadsheet-support.md b/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/google-spreadsheet.md similarity index 100% rename from packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/installing-sofie-with-google-spreadsheet-support.md rename to packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/google-spreadsheet.md diff --git a/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md b/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md index cc144a45a37..23daffc28a1 100644 --- a/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md +++ b/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md @@ -1,15 +1,8 @@ # iNEWS Gateway -The iNEWS Gateway communicates with an iNEWS system to ingest and remain in sync with a rundown. +The iNEWS Gateway communicates with an iNEWS system to ingest and remain in sync with a rundown. The rundowns will update in real time and any changes made will be seen from within your Rundown View. -### Installing iNEWS for Sofie +The setup for the iNEWS Gateway is already in the Docker Compose file you downloaded earlier. Remove the _\#_ symbols from the start of the section labelled `inews-gateway:` and make sure that other ingest gateway sections have a _\#_ prefix on each line. -The iNEWS Gateway allows you to create rundowns from within iNEWS and sync them with the _Sofie Core_. The rundowns will update in real time and any changes made will be seen from within your Playout Timeline. +Although the iNEWS Gateway is available free of charge, an iNEWS license is not. Visit [Avid's website](https://www.avid.com/products/inews/how-to-buy) to find an iNEWS reseller that handles your geographic area. -An example setup for the iNEWS Gateway is included in the example Docker Compose file found in the [Quick install](../../installing-sofie-server-core.md) with the `inews-gateway` profile. - -You can activate the profile by setting `COMPOSE_PROFILES=inews-gateway` as an environment variable or by writing that to a file called `.env` in the same folder as the docker-compose file. For more information, see the [docker documentation on Compose profiles](https://docs.docker.com/compose/how-tos/profiles/). - -If you are not using the example docker-compose, please follow the [instructions listed on the GitHub page](https://github.com/tv2/inews-ftp-gateway). - -Although the iNEWS Gateway is available free of charge, an iNEWS license is not. Visit [Avid's website](https://www.avid.com/solutions/news-production) to find an iNEWS reseller that handles your geographic area. diff --git a/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md b/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md index e6d4860d780..2d5200d62eb 100644 --- a/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md +++ b/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md @@ -3,12 +3,19 @@ sidebar_position: 1 --- # Rundown & Newsroom Systems -Sofie Core doesn't talk directly to the newsroom systems, but instead via one of the Gateways. +NewsRoom Computer Systems (NRCS) are software suites that manage various parts of news production. Many of these systems support some sort of Rundown creation module that allows authoring live show Rundowns by organizing them into units and sub-units such as Pages, Items, Cues, etc. -The Google Spreadsheet Gateway, iNEWS Gateway, and the MOS \([Media Object Server Communications Protocol](http://mosprotocol.com/)\) Gateway which can handle interacting with any system that communicates via MOS, for example AP ENPS. +Sofie Core doesn't talk directly to the newsroom systems, but instead via one of the Ingest Gateways. The purpose of these Gateways is to act as adapters for the various protocols used by these systems, while keeping as much fidelity as possible in the incoming data. + +Some of the currently available options in the Sofie ecosystem include Google Docs Spreadsheet Gateway, iNEWS Gateway, and the MOS Gateway which can handle interacting with any system that communicates via MOS \([Media Object Server Communications Protocol](http://mosprotocol.com/)\). + +[Rundown Editor](../../rundown-editor.md) is a special case of an Ingest Gateway that acts as a simple Rundown Editor itself. ### Further Reading -- [MOS Protocol Overview & Documentation](http://mosprotocol.com/) -- [iNEWS on Avid's Website](https://www.avid.com/solutions/news-production) -- [ENPS on The Associated Press' Website](https://workflow.ap.org/) +* [MOS Protocol Overview & Documentation](http://mosprotocol.com/) +* [iNEWS on Avid's Website](https://www.avid.com/products/inews/how-to-buy) +* [ENPS on The Associated Press' Website](https://www.ap.org/enps/support) + + + diff --git a/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md b/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md index e4d9ed36541..94179ad1757 100644 --- a/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md +++ b/packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md @@ -2,31 +2,18 @@ The MOS Gateway communicates with a device that supports the [MOS protocol](http://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOS-Protocol-2.8.4-Current.htm) to ingest and remain in sync with a rundown. It can connect to any editorial system \(NRCS\) that uses version 2.8.4 of the MOS protocol, such as ENPS, and sync their rundowns with the _Sofie Core_. The rundowns are kept updated in real time and any changes made will be seen in the Sofie GUI. -The setup for the MOS Gateway is handled in the Docker Compose in the [Quick Install](../../installing-sofie-server-core.md) page. +MOS 2.8.4 uses TCP Sockets to send XML messages between the NRCS and the Automation Systems. This is done via two open ports on the Automation System side (the *upper* and *lower* port) and two ports on the NRCS side (*upper* and *lower* as well). -An example setup for the MOS Gateway is included in the example Docker Compose file found in the [Quick install](../../installing-sofie-server-core.md) with the `mos-gateway` profile. +The setup for the MOS Gateway is handled in the Docker Compose in the [Quick Install](../../quick-install.md) page. Remove the _\#_ symbols from the start of the section labelled `mos-gateway:` and make sure that other ingest gateway sections have a _\#_ prefix. -You can activate the profile by setting `COMPOSE_PROFILES=mos-gateway` as an environment variable or by writing that to a file called `.env` in the same folder as the docker-compose file. For more information, see the [docker documentation on Compose profiles](https://docs.docker.com/compose/how-tos/profiles/). +You will also need to configure your NRCS to connect to Sofie. Refer to your NRCS's documentation on how that needs to be done. -Development of the MOS gateway is done as a package in the [sofie-core repository on GitHub](https://github.com/nrkno/sofie-core/tree/master/packages/mos-gateway). +After the Gateway is deployed, you will need to assign it to a Studio and you will need to go into *Settings* 🡒 *Studios* 🡒 *Your studio name* -> *Peripheral Devices* 🡒 *MOS gateway* 🡒 Edit and configure the MOS ID that this Gateway will use when talking to the NRCS. This needs to match the configuration within your NRCS. -One thing to note if managing the mos-gateway manually: It needs a few ports open \(10540, 10541\) for MOS-messages to be pushed to it from the NCS. - -## Status Reporting +Then, in the *Ingest Devices* section of the *Peripheral Devices* page, use the **+** button to add a new *MOS device*. In *Peripheral Device ID* select *MOS gateway* and in *Device Type* select *MOS Device*. You will then be able to provide the MOS ID of your Primary and Secondary NRCS servers and enter their Hostname/IP Address and Upper and Lower Port information. :::warning -Behaviour of this has changed In R53 as part of expanding the reporting ability. -If you were using this prior to that change, you can restore previous behaviour by enabling `Write Statuses to NRCS` and `Only send PLAY statuses` in the MOS gateway settings. -::: - -Sofie is able to report statuses back to stories and objects in the NRCS. It does this by having the blueprints define some properties on the Part during ingest, and the mos-gateway to consolidate this and send messages. - -:::tip -This functionality requires blueprints which set some properties to enable the various states and behaviours. You can read more about that in the [developer guide](../../../../for-developers/for-blueprint-developers/mos-statuses.md) +One thing to note if managing the `mos-gateway` manually: It needs a few ports open \(10540, 10541 by default\) for MOS-messages to be pushed to it from the NRCS. If the defaults are changed in Peripheral Device settings, this needs to be reflected by Docker configuration changes. ::: -### Gateway settings -- `Write Statuses to NRCS` - This is the core setting that must be enabled for any statuses to be checked or sent. -- `Send when in Rehearsal mode` - By default statuses are not reported when in rehearsal mode. -- `Only send PLAY statuses` - In some environments it can be desirable to only send `PLAY` messages and not `STOP`. Enabling this will stop Sofie from sending anything other than `PLAY` diff --git a/packages/documentation/docs/user-guide/installation/installing-blueprints.md b/packages/documentation/docs/user-guide/installation/installing-blueprints.md index 17adb5c68f5..a56fdce59a9 100644 --- a/packages/documentation/docs/user-guide/installation/installing-blueprints.md +++ b/packages/documentation/docs/user-guide/installation/installing-blueprints.md @@ -1,19 +1,19 @@ --- -sidebar_position: 4 +sidebar_position: 40 --- # Installing Blueprints #### Prerequisites -- [Installed and running Sofie Core](installing-sofie-server-core.md) +- [Installed and running Sofie Core](quick-install.md) - [Initial Sofie Core Setup](initial-sofie-core-setup.md) Blueprints are little plug-in programs that runs inside _Sofie_. They are the logic that determines how _Sofie_ interacts with rundowns, hardware, and media. -Blueprints are custom scripts that you can download or create yourself. There is a set of example Blueprints for the [Rundown Editor](https://github.com/SuperFlyTV/sofie-automation-rundown-editor) or the [Spreadsheet Gateway](https://github.com/SuperFlyTV/spreadsheet-gateway) available for use here: [https://github.com/SuperFlyTV/sofie-demo-blueprints](https://github.com/SuperFlyTV/sofie-demo-blueprints). +Blueprints are custom JavaScript scripts that you create yourself \(or download an existing one\). There are a set of example Blueprints for the Spreadsheet Gateway and Rundown Editor available for use here: [https://github.com/SuperFlyTV/sofie-demo-blueprints](https://github.com/SuperFlyTV/sofie-demo-blueprints). You can learn more about them in the [Blueprints section](../../for-developers/for-blueprint-developers/intro.md) -To begin installing any Blueprint, navigate to the _Settings page_. ( [http://localhost:3000/settings/?admin=1](http://localhost:3000/settings/?admin=1>) ), otherwise see the [Sofie Access Level](../features/access-levels.md) page for assistance getting there. +To begin installing any Blueprint, navigate to the _Settings page_. Getting there is covered in the [Access Levels](../features/access-levels.md) page. ![The Settings Page](/img/docs/getting-started/settings-page.jpg) @@ -25,13 +25,13 @@ There are 3 types of blueprints: System, Studio and Show Style: _System Blueprints handles some basic functionality on how the Sofie system will operate._ -After you've uploaded the your system-blueprint js-file, click _Assign_ in the blueprint-page to assign it as system-blueprint. +After you've uploaded your System Blueprint JS bundle, click _Assign_ in the blueprint-page to assign it as system-blueprint. ### Studio Blueprint _Studio Blueprints determine how Sofie will interact with the hardware in your studio._ -After you've uploaded the your studio-blueprint js-file, navigate to a Studio in the settings and assign the new Blueprint to it \(under the label _Blueprint_ \). +After you've uploaded your Studio Blueprint JS bundle, navigate to a Studio in the settings and assign the new Blueprint to it \(under the label _Blueprint_ \). After having installed the Blueprint, the Studio's baseline will need to be reloaded. On the Studio page, click the button _Reload Baseline_. This will also be needed whenever you have changed any settings. @@ -39,4 +39,8 @@ After having installed the Blueprint, the Studio's baseline will need to be relo _Show Style Blueprints determine how your show will look / feel._ -After you've uploaded the your show-style-blueprint js-file, navigate to a Show Style in the settings and assign the new Blueprint to it \(under the label _Blueprint_ \). +After you've uploaded your Show Style Blueprint JS bundle, navigate to a Show Style in the settings and assign the new Blueprint to it \(under the label _Blueprint_ \). + +### Further Reading + +- [Community Blueprints Supporting Spreadsheet Gateway and Rundown Editor](https://github.com/SuperFlyTV/sofie-demo-blueprints) diff --git a/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/README.md b/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/README.md index 57ebb625d0e..7310b1e577d 100644 --- a/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/README.md +++ b/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/README.md @@ -2,40 +2,34 @@ #### Prerequisites -- [Installed and running Sofie Core](../installing-sofie-server-core.md) -- [Installed Playout Gateway](../installing-a-gateway/playout-gateway.md) -- [Installed and configured Studio Blueprints](../installing-blueprints.md#installing-a-studio-blueprint) +* [Installed and running Sofie Core](../quick-install.md) +* [Installed Playout Gateway](../installing-a-gateway/playout-gateway.md) +* [Installed and configured Studio Blueprints](../installing-blueprints.md#installing-a-studio-blueprint) The following pages are broken up by equipment type that is supported by Sofie's Gateways. ## Playout & Recording - -- [CasparCG Graphics and Video Server](casparcg-server-installation.md) - _Graphics / Playout / Recording_ -- [Blackmagic Design's HyperDeck](https://www.blackmagicdesign.com/products/hyperdeckstudio) - _Recording_ -- Quantel Solutions - _Playout_ \( Now part of [Grass Valley Group](https://www.grassvalley.com/products/playout/) \) -- [Vizrt](https://www.vizrt.com/) Graphics Solutions - _Graphics / Playout_ +* [CasparCG Graphics and Video Server](casparcg-server-installation.md) - _Graphics / Playout / Recording_ +* [Blackmagic Design's HyperDeck](https://www.blackmagicdesign.com/products/hyperdeckstudio) - _Recording_ +* [Quantel](http://www.quantel.com) Solutions - _Playout_ +* [Vizrt](https://www.vizrt.com/) Graphics Solutions - _Graphics / Playout_ ## Vision Mixers - -- [Blackmagic's ATEM](https://www.blackmagicdesign.com/products/atem) hardware vision mixers -- [vMix](https://www.vmix.com/) software vision mixer \(coming soon\) +* [Blackmagic's ATEM](https://www.blackmagicdesign.com/products/atem) hardware vision mixers +* [vMix](https://www.vmix.com/) software vision mixer \(coming soon\) ## Audio Mixers - -- [Sisyfos](https://github.com/olzzon/sisyfos-audio-controller) audio controller -- [Lawo sound mixers*,*](https://www.lawo.com/applications/broadcast-production/audio-consoles.html) _using emberplus protocol_ -- Generic OSC \(open sound control\) +* [Sisyfos](https://github.com/olzzon/sisyfos-audio-controller) audio controller +* [Lawo sound mixers_,_](https://www.lawo.com/applications/broadcast-production/audio-consoles.html) _using emberplus protocol_ +* Generic OSC \(open sound control\) ## PTZ Cameras - -- [Panasonic PTZ](https://pro-av.panasonic.net/en/products/ptz_camera_systems.html) cameras +* [Panasonic PTZ](https://pro-av.panasonic.net/en/products/ptz_camera_systems.html) cameras ## Lights - -- [Pharos](https://www.pharoscontrols.com/) light control +* [Pharos](https://www.pharoscontrols.com/) light control ## Other - -- Generic OSC \(open sound control\) -- Generic HTTP requests \(to control http-REST interfaces\) -- Generic TCP-socket +* Generic OSC \(open sound control\) +* Generic HTTP requests \(to control http-REST interfaces\) +* Generic TCP-socket diff --git a/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/_category_.json b/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/_category_.json index 4d800e9a88f..aea5cfb8179 100644 --- a/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/_category_.json +++ b/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/_category_.json @@ -1,4 +1,4 @@ { - "label": "Installing Connections and Additional Hardware", - "position": 6 -} + "label": "Installing Connections and Additional Hardware", + "position": 60 +} \ No newline at end of file diff --git a/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md b/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md index f5b845d77ef..be682ca1d55 100644 --- a/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md +++ b/packages/documentation/docs/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md @@ -206,7 +206,7 @@ A window will open and display the status for the server and scanner. You can st Now that your CasparCG Server software is running, you can connect it to the _Sofie Core_. Navigate back to the _Settings page_ and in the menu, select the _Playout Gateway_. If the _Playout Gateway's_ status does not read _Good_, then please review the [Installing and Setting up the Playout Gateway](../installing-a-gateway/playout-gateway.md) section of this guide. -Under the Sub Devices section, you can add a new device with the _+_ button. Then select the pencil \( edit \) icon on the new device to open the sub device's settings. Select the _Device Type_ option and choose _CasparCG_ from the drop down menu. Some additional fields will be added to the form. +Under the Sub Devices section, you can add a new device with the _+_ button. Then select the pencil \( edit \) icon on the new device to open the sub device's settings. Select the _Device Type_ option and choose _CasparCG_ from the drop-down menu. Some additional fields will be added to the form. The _Host_ and _Launcher Host_ fields will be _localhost_. The _Port_ will be CasparCG's TCP port responsible for handling the AMCP commands. It defaults to 5052 in the `casparcg.config` file. The _Launcher Port_ will be the CasparCG Launcher's port for handling HTTP requests. It will default to 8005 and can be changed in the _Launcher's settings page_. Once all four fields are filled out, you can click the check mark to save the device. @@ -214,8 +214,8 @@ In the _Attached Sub Devices_ section, you should now see the status of the Casp ## Further Reading -- [CasparCG Server Releases](https://github.com/nrkno/sofie-casparcg-server/releases) on GitHub. -- [Media Scanner Releases](https://github.com/nrkno/sofie-media-scanner/releases) on GitHub. +- [CasparCG Server Releases](https://github.com/CasparCG/server/releases) on GitHub. +- [Media Scanner Releases](https://github.com/CasparCG/media-scanner/releases) on GitHub. - [CasparCG Launcher](https://github.com/Sofie-Automation/sofie-casparcg-launcher) on GitHub. - [Microsoft Visual C++ 2015 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=52685) on Microsoft's website. - [Blackmagic Design's DeckLink Cards](https://www.blackmagicdesign.com/products/decklink/models) on Blackmagic's website. Check the [DeckLink cards](casparcg-server-installation.md#decklink-cards) section for compatibility. diff --git a/packages/documentation/docs/user-guide/installation/installing-package-manager.md b/packages/documentation/docs/user-guide/installation/installing-package-manager.md index bbdcad49ee2..a7dafa5a6f6 100644 --- a/packages/documentation/docs/user-guide/installation/installing-package-manager.md +++ b/packages/documentation/docs/user-guide/installation/installing-package-manager.md @@ -1,12 +1,12 @@ --- -sidebar_position: 7 +sidebar_position: 70 --- # Installing Package Manager ### Prerequisites -- [Installed and running Sofie Core](installing-sofie-server-core.md) +- [Installed and running Sofie Core](quick-install.md) - [Initial Sofie Core Setup](initial-sofie-core-setup.md) - [Installed and configured Demo Blueprints](https://github.com/SuperFlyTV/sofie-demo-blueprints) - [Installed, configured, and running CasparCG Server](installing-connections-and-additional-hardware/casparcg-server-installation.md) (Optional) @@ -23,11 +23,6 @@ If you feel like you need multiple, then you likely want to run Package Manager ::: -:::caution - -The Package Manager worker process is primarily tested on Windows only. It does run on Linux (without support for network shares), but has not been extensively tested. - -::: ## Installation For Development (Quick Start) diff --git a/packages/documentation/docs/user-guide/installation/installing-sofie-server-core.md b/packages/documentation/docs/user-guide/installation/installing-sofie-server-core.md index ab3b96c34fd..7ee0c7ed29e 100644 --- a/packages/documentation/docs/user-guide/installation/installing-sofie-server-core.md +++ b/packages/documentation/docs/user-guide/installation/installing-sofie-server-core.md @@ -1,232 +1,23 @@ --- -sidebar_position: 2 +sidebar_position: 35 --- -# Quick install +# Installing Sofie Core -## Installing for testing \(or production\) +Our **[Quick install guide](quick-install.md)** provides a quick and easy way of deploying the various pieces of software needed for a production-quality deployment of Sofie using `docker compose`. This section provides some more insights for users choosing to install Sofie via alternative methods. -### **Prerequisites** +The preferred way to install Sofie Core for production is using Docker via our officially published images inside Docker Hub: [https://hub.docker.com/u/sofietv](https://hub.docker.com/u/sofietv). Note that some of the images mentioned in this documentation are community-maintained and as such are not published by the `sofietv` Docker Hub organization. -* **Linux**: Install [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/) and [docker-compose](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-18-04). -* **Windows**: Install [Docker for Windows](https://hub.docker.com/editions/community/docker-ce-desktop-windows). +More advanced ways of deploying Sofie are possible and actively used by Sofie users, including [Podman](https://podman.io/), [Kubernetes](https://kubernetes.io/), [Salt](https://saltproject.io/), [Ansible](https://github.com/ansible/ansible) among others. Any deployment system that uses [OCI App Containers](https://opencontainers.org/) should be suitable. -### Installation +Sofie and it's Blueprint system is specifically built around the concept of Infrastructure-as-Code and Configuration-as-Code and we strongly advise using that methodology in production, rather than the manual route of using the User Interface for configuration. -This docker-compose file automates the basic setup of the [Sofie-Core application](../../for-developers/libraries.md#main-application), the backend database and different Gateway options. - -```yaml -# This is NOT recommended to be used for a production deployment. -# It aims to quickly get an evaluation version of Sofie running and serve as a basis for how to set up a production deployment. -name: Sofie -services: - db: - hostname: mongo - image: mongo:8 - restart: always - entrypoint: ['/usr/bin/mongod', '--replSet', 'rs0', '--bind_ip_all'] - # the healthcheck avoids the need to initiate the replica set - healthcheck: - test: test $$(mongosh --quiet --eval "try {rs.initiate()} catch(e) {rs.status().ok}") -eq 1 - interval: 10s - start_period: 30s - ports: - - '27017:27017' - volumes: - - db-data:/data/db - networks: - - sofie - - # Fix Ownership Snapshots mount - # Because docker volumes are owned by root by default - # And our images follow best-practise and don't run as root - change-vol-ownerships: - image: node:22-alpine - user: 'root' - volumes: - - sofie-store:/mnt/sofie-store - entrypoint: ['sh', '-c', 'chown -R node:node /mnt/sofie-store'] - - core: - hostname: core - image: sofietv/tv-automation-server-core:release53 - restart: always - ports: - - '3000:3000' # Same port as meteor uses by default - environment: - PORT: '3000' - MONGO_URL: 'mongodb://db:27017/meteor' - MONGO_OPLOG_URL: 'mongodb://db:27017/local' - ROOT_URL: 'http://localhost:3000' - SOFIE_STORE_PATH: '/mnt/sofie-store' - networks: - - sofie - volumes: - - sofie-store:/mnt/sofie-store - depends_on: - change-vol-ownerships: - condition: service_completed_successfully - db: - condition: service_healthy - - playout-gateway: - image: sofietv/tv-automation-playout-gateway:release53 - restart: always - environment: - DEVICE_ID: playoutGateway - CORE_HOST: core - CORE_PORT: '3000' - networks: - - sofie - depends_on: - - core - - live-status-gateway: - image: sofietv/tv-automation-live-status-gateway:release53 - restart: always - ports: - - '8080:8080' - environment: - DEVICE_ID: liveStatusGateway - CORE_HOST: core - CORE_PORT: '3000' - networks: - - sofie - - lan_access - depends_on: - - core - - package-manager: - image: sofietv/package-manager-package-manager:latest - restart: always - environment: - DEVICE_ID: packageManager - CORE_HOST: core - CORE_PORT: '3000' - PACKAGE_MANAGER_URL: ws://package-manager:8060 - WORKFORCE_URL: ws://package-manager-workforce:8070 - networks: - - sofie - depends_on: - - core - - package-manager-workforce - - package-manager-http-server: - image: sofietv/package-manager-http-server:latest - restart: always - ports: - - '8081:8080' - environment: - HTTP_SERVER_BASE_PATH: /mnt/package-manager-store - networks: - - sofie - volumes: - - package-manager-store:/mnt/package-manager-store - - package-manager-workforce: - image: sofietv/package-manager-workforce:latest - restart: always - ports: - - '8070:8070' - networks: - - sofie - - # Choose one of the following images, depending on which type of ingest gateway is wanted. - - spreadsheet-gateway: - image: superflytv/sofie-spreadsheet-gateway:latest - restart: always - environment: - DEVICE_ID: spreadsheetGateway - CORE_HOST: core - CORE_PORT: '3000' - networks: - - sofie - depends_on: - - core - profiles: [spreadsheet-gateway] - - mos-gateway: - image: sofietv/tv-automation-mos-gateway:release53 - restart: always - ports: - - '10540:10540' # MOS Lower port - - '10541:10541' # MOS Upper port - # - '10542:10542' # MOS query port - not used - environment: - DEVICE_ID: mosGateway - CORE_HOST: core - CORE_PORT: '3000' - networks: - - sofie - depends_on: - - core - profiles: [mos-gateway] - - inews-gateway: - image: tv2media/inews-ftp-gateway:1.37.0-in-testing.20 - restart: always - command: yarn start -host core -port 3000 -id inewsGateway - networks: - - sofie - depends_on: - - core - profiles: [inews-gateway] - - # rundown-editor: - # image: ghcr.io/superflytv/sofie-automation-rundown-editor:v2.2.4 - # restart: always - # ports: - # - '3010:3010' - # environment: - # PORT: '3010' - # networks: - # - sofie - # depends_on: - # - core - -networks: - sofie: - -volumes: - db-data: - sofie-store: - package-manager-store: -``` - -Create a `Sofie` folder, copy the above content, and save it as `docker-compose.yaml` within the `Sofie` folder. - -Visit [Rundowns & Newsroom Systems](installing-a-gateway/rundown-or-newsroom-system-connection/intro.md) to see which _Ingest Gateway_ can be used in your specific production environment. If you don't have an NRCS that you would like to integrate with, you can use the [Rundown Editor](rundown-editor) as a simple Rundown creation utility. - -Select the ingest gateway by creating using docker compose profiles. Create a file called `.env` in the same folder as docker compose with the contents: - -``` -COMPOSE_PROFILES=ingest-profile-name -``` - -But replacing `ingest-profile-name` with one of `spreadsheet-gateway`, `mos-gateway` or `inews-gateway`, or a comma separated list of more than one. For more information, see the [docker documentation on Compose profiles](https://docs.docker.com/compose/how-tos/profiles/). - -Then open a terminal, `cd your-sofie-folder` and `sudo docker-compose up` \(just `docker-compose up` on Windows or MacOS\). This will download MongoDB and Sofie components' container images and start them up. The installation will be done when your terminal window will be filled with messages coming from `playout-gateway_1` and `core_1`. - -Once the installation is done, Sofie should be running on [http://localhost:3000](http://localhost:3000). Next, you need to make sure that the Playout Gateway and Ingest Gateway are connected to the default Studio that has been automatically created. Open the Sofie User Interface with [Configuration Access level](../features/access-levels#browser-based) by opening [http://localhost:3000/?admin=1](http://localhost:3000/?admin=1) in your Web Browser and navigate to _Settings_ 🡒 _Studios_ 🡒 _Default Studio_ 🡒 _Peripheral Devices_. In the _Parent Devices_ section, create a new Device using the **+** button, rename the device to _Playout Gateway_ and select _Playout gateway_ from the _Peripheral Device_ drop down menu. Repeat this process for your _Ingest Gateway_ or _Sofie Rundown Editor_. - -:::note -Starting with Sofie version 1.52.0, `sofietv` container images will run as UID 1000. +:::tip +While Sofie is using cloud-native technologies, it's workloads do not follow typical patterns seen in cloud software. When optimizing Sofie performance for production, make sure not to optimize for the amount of operations per second, but rather for fastest response time on a single request. ::: -### Tips for running in production - -There are some things not covered in this guide needed to run _Sofie_ in a production environment: - -- Logging: Collect, store and track error messages. [Kibana](https://www.elastic.co/kibana) and [logstash](https://www.elastic.co/logstash) is one way to do it. -- NGINX: It is customary to put a load-balancer in front of _Sofie Core_. -- Memory and CPU usage monitoring. - -## Installing for Development - -Installation instructions for installing Sofie-Core or the various gateways are available in the README file in their respective github repos. +## Basic structure -Common prerequisites are [Node.js](https://nodejs.org/) and [Yarn](https://yarnpkg.com/). -Links to the repos are listed at [Applications & Libraries](../../for-developers/libraries.md). +On a foundational level, Sofie Core is a [Meteor](https://docs.meteor.com/), [Node.js](https://nodejs.org/) web application that uses [MongoDB](https://www.mongodb.com) for its data persistence. -[_Sofie Core_ GitHub Page for Developers](https://github.com/Sofie-Automation/sofie-core) +Both the Sofie Gateways and User Agents using the Web User Interface connect to it via DDP, a WebSocket-based, Meteor-specific protocol. This protocol is used both for RPC and shared state synchronization. diff --git a/packages/documentation/docs/user-guide/installation/intro.md b/packages/documentation/docs/user-guide/installation/intro.md index 6e1cb0f7da4..bcf3dd99481 100644 --- a/packages/documentation/docs/user-guide/installation/intro.md +++ b/packages/documentation/docs/user-guide/installation/intro.md @@ -1,35 +1,25 @@ --- -sidebar_position: 1 +sidebar_position: 10 --- - # Getting Started -_Sofie_ can be installed in many different ways, depending on which platforms, needs, and features you desire. The _Sofie_ system consists of several applications that work together to provide a complete broadcast automation system. Each of these components' installation will be covered in this guide. Additional information about the products or services mentioned alongside the Sofie Installation can be found on the [Further Reading](../further-reading.md). - -There are four minimum required components to get a Sofie system up and running. First you need the [_Sofie Core_](installing-sofie-server-core.md), which is the brains of the operation. Then a set of [_Blueprints_](installing-blueprints.md) to handle and interpret incoming and outgoing data. Next, an [_Ingest Gateway_](installing-a-gateway/rundown-or-newsroom-system-connection/intro.md) to fetch the data for the Blueprints. Then finally, a [_Playout Gateway_](installing-a-gateway/playout-gateway.md) to send the data to your playout device of choice. - -## Sofie Core View - -The _Rundowns_ view will display all the active rundowns that the _Sofie Core_ has access to. +_Sofie_ can be installed in many different ways, depending on which platforms, needs, and features you desire. The _Sofie_ system consists of several applications that work together to provide complete broadcast automation system. Each of these components' installation will be covered in this guide. Additional information about the products or services mentioned alongside the Sofie Installation can be found on the [Further Reading](../further-reading.md). -![Rundown View](/img/docs/getting-started/rundowns-in-sofie.png) +:::tip Quick Install +If you're looking to quickly evaluate Sofie to see if it's a good match for your needs, you can jump into our **[Quick Install guide](./quick-install.md)**. +::: -The _Status_ views displays the current status for the attached devices and gateways. - -![Status View – Describes the state of _Sofie Core_](/img/docs/getting-started/status-page.jpg) - -The _Settings_ views contains various settings for the studio, show styles, blueprints etc.. If the link to the settings view is not visible in your application, check your [Access Levels](../features/access-levels.md). More info on specific parts of the _Settings_ view can be found in their corresponding guide sections. - -![Settings View – Describes how the _Sofie Core_ is configured](/img/docs/getting-started/settings-page.jpg) +There are four minimum required components to get a Sofie system up and running. First you need the [_Sofie Core_](quick-install.md), which is the brains of the operation. Then a set of [_Blueprints_](installing-blueprints.md) to handle and interpret incoming and outgoing data. Next, an [_Ingest Gateway_](installing-a-gateway/rundown-or-newsroom-system-connection/intro.md) to fetch the data for the Blueprints. Then finally, a [_Playout Gateway_](installing-a-gateway/playout-gateway.md) to send commands and change the state of your playout devices while you run your show. ## Sofie Core Overview -The _Sofie Core_ is the primary application for managing the broadcast but, it doesn't play anything out on it's own. You need to use Gateways to establish the connection from the _Sofie Core_ to other pieces of hardware or remote software. +The _Sofie Core_ is the primary application for managing the broadcast but, it doesn't play anything out on it's own. You need to use Gateways to establish the connection from the _Sofie Core_ to other pieces of hardware or remote software. ### Gateways -Gateways are separate applications that bridge the gap between the _Sofie Core_ and other pieces of hardware or services. At minimum, you will need a _Playout Gateway_ so your timeline can interact with your playout system of choice. To install the _Playout Gateway_, visit the [Installing a Gateway](installing-a-gateway/intro.md) section of this guide and for a more in-depth look, please see [Gateways](../concepts-and-architecture.md#gateways). +Gateways are separate applications that bridge the gap between the _Sofie Core_ and other pieces of hardware or software services. At a minimum, you will need a _Playout Gateway_ so your timeline can interact with your playout system of choice. To install the _Playout Gateway_, visit the [Installing a Gateway](installing-a-gateway/intro.md) section of this guide and for a more in-depth look, please see [Gateways](../concepts-and-architecture.md#gateways). ### Blueprints -Blueprints can be described as the logic that determines how a studio and show should interact with one another. They interpret the data coming in from the rundowns and transform them into a rich set of playable elements \(_Segments_, _Parts_, _AdLibs,_ etcetera\). The _Sofie Core_ has three main blueprint types, _System Blueprints_, _Studio Blueprints_, and _Showstyle Blueprints_. Installing _Sofie_ does not require you understand what these blueprints do, just that they are required for the _Sofie Core_ to work. If you would like to gain a deeper understand of how _Blueprints_ work, please visit the [Blueprints](#blueprints) section. +Blueprints can be described as the logic that determines how a studio and show should interact with one another. They interpret the data coming in from the rundowns and transform them into a rich set of playable elements \(_Segments_, _Parts_, _AdLibs,_ etc.\). The _Sofie Core_ has three main blueprint types, _System Blueprints_, _Studio Blueprints_, and _Showstyle Blueprints_. Installing _Sofie_ does not require you understand what these blueprints do, just that they are required for the _Sofie Core_ to work. If you would like to gain a deeper understanding of how _Blueprints_ work, please visit the [Blueprints](../../for-developers/for-blueprint-developers/intro.md) section. + diff --git a/packages/documentation/docs/user-guide/installation/quick-install.md b/packages/documentation/docs/user-guide/installation/quick-install.md new file mode 100644 index 00000000000..d9fc1331d15 --- /dev/null +++ b/packages/documentation/docs/user-guide/installation/quick-install.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 20 +--- + +# Quick install + +## Installing for testing \(or production\) + +### **Prerequisites** + +* **Linux**: Install [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/) and [docker-compose](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-18-04). +* **Windows**: Install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and use an *Ubuntu* terminal to install Docker and docker-compose. + +### Installation + +This docker-compose file automates the basic setup of the [Sofie-Core application](../../for-developers/libraries.md#main-application), the backend database and different Gateway options. + +```yaml +# This is NOT recommended to be used for a production deployment. +# It aims to quickly get an evaluation version of Sofie running and serve as a basis for how to set up a production deployment. +services: + db: + hostname: mongo + image: mongo:6.0 + restart: always + entrypoint: ['/usr/bin/mongod', '--replSet', 'rs0', '--bind_ip_all'] + # the healthcheck avoids the need to initiate the replica set + healthcheck: + test: test $$(mongosh --quiet --eval "try {rs.initiate()} catch(e) {rs.status().ok}") -eq 1 + interval: 10s + start_period: 30s + ports: + - '27017:27017' + volumes: + - db-data:/data/db + networks: + - sofie + + # Fix Ownership Snapshots mount + # Because docker volumes are owned by root by default + # And our images follow best-practise and don't run as root + change-vol-ownerships: + image: node:22-alpine + user: 'root' + volumes: + - sofie-store:/mnt/sofie-store + entrypoint: ['sh', '-c', 'chown -R node:node /mnt/sofie-store'] + + core: + hostname: core + image: sofietv/tv-automation-server-core:release52 + restart: always + ports: + - '3000:3000' # Same port as meteor uses by default + environment: + PORT: '3000' + MONGO_URL: 'mongodb://db:27017/meteor' + MONGO_OPLOG_URL: 'mongodb://db:27017/local' + ROOT_URL: 'http://localhost:3000' + SOFIE_STORE_PATH: '/mnt/sofie-store' + networks: + - sofie + volumes: + - sofie-store:/mnt/sofie-store + depends_on: + change-vol-ownerships: + condition: service_completed_successfully + db: + condition: service_healthy + + playout-gateway: + image: sofietv/tv-automation-playout-gateway:release52 + restart: always + environment: + DEVICE_ID: playoutGateway0 + CORE_HOST: core + CORE_PORT: '3000' + networks: + - sofie + - lan_access + depends_on: + - core + + # Choose one of the following images, depending on which type of ingest gateway is wanted. + + # spreadsheet-gateway: + # image: superflytv/sofie-spreadsheet-gateway:latest + # restart: always + # environment: + # DEVICE_ID: spreadsheetGateway0 + # CORE_HOST: core + # CORE_PORT: '3000' + # networks: + # - sofie + # depends_on: + # - core + + # mos-gateway: + # image: sofietv/tv-automation-mos-gateway:release52 + # restart: always + # ports: + # - "10540:10540" # MOS Lower port + # - "10541:10541" # MOS Upper port + # # - "10542:10542" # MOS query port - not used + # environment: + # DEVICE_ID: mosGateway0 + # CORE_HOST: core + # CORE_PORT: '3000' + # networks: + # - sofie + # depends_on: + # - core + + # inews-gateway: + # image: tv2media/inews-ftp-gateway:1.37.0-in-testing.20 + # restart: always + # command: yarn start -host core -port 3000 -id inewsGateway0 + # networks: + # - sofie + # depends_on: + # - core + + # rundown-editor: + # image: ghcr.io/superflytv/sofie-automation-rundown-editor:v2.2.4 + # restart: always + # ports: + # - '3010:3010' + # environment: + # PORT: '3010' + # networks: + # - sofie + # depends_on: + # - core + +networks: + sofie: + lan_access: + driver: bridge + +volumes: + db-data: + sofie-store: +``` + +Create a `Sofie` folder, copy the above content, and save it as `docker-compose.yaml` within the `Sofie` folder. + +Visit [Rundowns & Newsroom Systems](installing-a-gateway/rundown-or-newsroom-system-connection/intro.md) to see which _Ingest Gateway_ can be used in your specific production environment. If you don't have an NRCS that you would like to integrate with, you can use the [Rundown Editor](rundown-editor) as a simple Rundown creation utility. Navigate to the _ingest-gateway_ section of `docker-compose.yaml` and select which type of _ingest-gateway_ you'd like installed by uncommenting it. Save your changes. + +Open a terminal, execute `cd Sofie` and `sudo docker-compose up` \(or just `docker-compose up` on Windows\). This will download MongoDB and Sofie components' container images and start them up. The installation will be done when your terminal window will be filled with messages coming from `playout-gateway_1` and `core_1`. + +Once the installation is done, Sofie should be running on [http://localhost:3000](http://localhost:3000). Next, you need to make sure that the Playout Gateway and Ingest Gateway are connected to the default Studio that has been automatically created. Open the Sofie User Interface with [Configuration Access level](../features/access-levels#browser-based) by opening [http://localhost:3000/?admin=1](http://localhost:3000/?admin=1) in your Web Browser and navigate to _Settings_ 🡒 _Studios_ 🡒 _Default Studio_ 🡒 _Peripheral Devices_. In the _Parent Devices_ section, create a new Device using the **+** button, rename the device to _Playout Gateway_ and select _Playout gateway_ from the _Peripheral Device_ drop-down menu. Repeat this process for your _Ingest Gateway_ or _Sofie Rundown Editor_. + +:::note +Starting with Sofie version 1.52.0, `sofietv` container images will run as UID 1000. +::: + +### Tips for running in production + +There are some things not covered in this guide needed to run _Sofie_ in a production environment: + +- Logging: Collect, store and track error messages. [Kibana](https://www.elastic.co/kibana) and [logstash](https://www.elastic.co/logstash) is one way to do it. +- NGINX: It is customary to put a load-balancer in front of _Sofie Core_. +- Memory and CPU usage monitoring. + +## Installing for Development + +Installation instructions for installing Sofie-Core or the various gateways are available in the README file in their respective GitHub repos. + +Common prerequisites are [Node.js](https://nodejs.org/) and [Yarn](https://yarnpkg.com/). +Links to the repos are listed at [Applications & Libraries](../../for-developers/libraries.md). + +[_Sofie Core_ GitHub Page for Developers](https://github.com/Sofie-Automation/sofie-core) diff --git a/packages/documentation/docs/user-guide/installation/rundown-editor.md b/packages/documentation/docs/user-guide/installation/rundown-editor.md index 4293431ac4e..686f7750db1 100644 --- a/packages/documentation/docs/user-guide/installation/rundown-editor.md +++ b/packages/documentation/docs/user-guide/installation/rundown-editor.md @@ -1,5 +1,5 @@ --- -sidebar_position: 8 +sidebar_position: 80 --- # Sofie Rundown Editor diff --git a/packages/documentation/docs/user-guide/intro.md b/packages/documentation/docs/user-guide/intro.md index 9774ce18d96..e2e7ed4787b 100644 --- a/packages/documentation/docs/user-guide/intro.md +++ b/packages/documentation/docs/user-guide/intro.md @@ -28,14 +28,14 @@ The Playout Gateway controls the devices and keeps track of their state and stat ### _State-based Playout_ -Sofie uses a state-based architecture to control playout. This means that each element in the show can be programmed independently - there's no need to take into account what has happened previously in the show; Sofie will make sure that the video is loaded and that the audio fader is tuned to the correct position, no matter what was played out previously. +Sofie is using a state-based architecture to control playout. This means that each element in the show can be programmed independently - there's no need to take into account what has happened previously in the show; Sofie will make sure that the video is loaded and that the audio fader is tuned to the correct position, no matter what was played out previously. This allows the producer to skip ahead or move backwards in a show, without the fear of things going wrong on air. ### Modular Data Ingest -Sofie features a modular ingest data-flow, allowing multiple types of input data to base rundowns on. Currently there is support for [MOS-based](http://mosprotocol.com) systems such as ENPS and iNEWS, as well as [Google Spreadsheets](installation/installing-a-gateway/rundown-or-newsroom-system-connection/installing-sofie-with-google-spreadsheet-support), and more are in development. +Sofie features a modular ingest data-flow, allowing multiple types of input data to base rundowns on. Currently there is support for [MOS-based](http://mosprotocol.com) systems such as ENPS and iNEWS, as well as [Google Spreadsheets](installation/installing-a-gateway/rundown-or-newsroom-system-connection/google-spreadsheet.md), and more is in development. ### Blueprints -The [Blueprints](concepts-and-architecture.md#blueprints) are plugins to _Sofie_, which allow for customization and tailor-made show designs. -The way these blueprints are created varies based on the characteristics of the input data (rundowns), the desired show design, and the specific devices being controlled. +The [Blueprints](concepts-and-architecture.md#blueprints) are plugins to _Sofie_, which allows for customization and tailor-made show designs. +The blueprints are made different depending on how the input data \(rundowns\) look like, how the show-design look like, and what devices to control. diff --git a/packages/documentation/docs/user-guide/supported-devices.md b/packages/documentation/docs/user-guide/supported-devices.md index 0bee545156d..c6d28c131d2 100644 --- a/packages/documentation/docs/user-guide/supported-devices.md +++ b/packages/documentation/docs/user-guide/supported-devices.md @@ -19,7 +19,6 @@ We support almost all features of these devices except fairlight audio, camera c ## CasparCG Server -Tested and developed against [a fork of version 2.4](https://github.com/nrkno/sofie-casparcg-server) - Video playback - Graphics playback diff --git a/packages/documentation/docusaurus.config.js b/packages/documentation/docusaurus.config.js index ee4d3de2f1c..34cf510e12a 100644 --- a/packages/documentation/docusaurus.config.js +++ b/packages/documentation/docusaurus.config.js @@ -6,7 +6,7 @@ const darkCodeTheme = themes.dracula module.exports = { title: 'Sofie TV Automation Documentation', tagline: - 'Sofie is a web-based, open\xa0source TV\xa0automation system for studios and live shows, used in daily live\xa0TV\xa0news productions by the Norwegian public\xa0service broadcaster NRK since September\xa02018.', + 'Sofie is a web-based, open-source TV automation system for studios and live shows. Since September 2018, it has been used in daily live TV news productions by broadcasters such as NRK, the BBC, and TV 2 (Norway).', url: 'https://sofie-automation.github.io', baseUrl: '/sofie-core/', onBrokenLinks: 'throw', @@ -125,15 +125,6 @@ module.exports = { docs: { sidebarPath: require.resolve('./sidebars.js'), editUrl: 'https://github.com/Sofie-Automation/sofie-core/edit/main/packages/documentation/', - // default to the 'next' docs - lastVersion: 'current', - versions: { - // Override the rendering of the 'next' docs to be 'latest' - current: { - label: 'Latest', - banner: 'none', - }, - }, }, // blog: { // showReadingTime: true, diff --git a/packages/documentation/package.json b/packages/documentation/package.json index f89ced26c1b..5bcc19685cd 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/documentation/src/components/GitHubReleases.jsx b/packages/documentation/src/components/GitHubReleases.jsx index ec79754b888..68102da3125 100644 --- a/packages/documentation/src/components/GitHubReleases.jsx +++ b/packages/documentation/src/components/GitHubReleases.jsx @@ -4,7 +4,7 @@ import IconExternalLink from '@docusaurus/theme-classic/lib/theme/Icon/ExternalL const GITHUB_API_URL = 'https://api.github.com' export default function GitHubReleases({ org, repo, releaseLabel, state }) { - const [isReady, setIsReady] = useState(0) // 0 - not ready, 1 - loaded, 2 - failed permamently + const [isReady, setIsReady] = useState(0) // 0 - not ready, 1 - loaded, 2 - failed permanently const [releases, setReleases] = useState([]) useEffect(() => { diff --git a/packages/documentation/versioned_docs/version-1.50.0/for-developers/device-integrations/intro.md b/packages/documentation/versioned_docs/version-1.50.0/for-developers/device-integrations/intro.md index 5d49b12343e..7985cddb3e4 100644 --- a/packages/documentation/versioned_docs/version-1.50.0/for-developers/device-integrations/intro.md +++ b/packages/documentation/versioned_docs/version-1.50.0/for-developers/device-integrations/intro.md @@ -9,7 +9,7 @@ In order to understand all about writing TSR integrations there are some concept - [TSR Types package](./tsr-types) - [TSR Actions](./tsr-actions) -But to start of we will explain the general structure of the TSR. Any user of the TSR will interface primarily with the Conductor class. Primarily the user will input device configurations, mappings and timelines into the TSR. The timeline describes the entire state of all of the devices over time. It does this by putting objects on timeline layers. Every timeline layer maps to a specific part of the device, this is configured throught the mappings. +But to start off we will explain the general structure of the TSR. Any user of the TSR will interface primarily with the Conductor class. Primarily the user will input device configurations, mappings and timelines into the TSR. The timeline describes the entire state of all of the devices over time. It does this by putting objects on timeline layers. Every timeline layer maps to a specific part of the device, this is configured through the mappings. The timeline is converted into disctinct states at different points in time, and these states are fed to the individual integrations. As an integration developer you shouldn't have to worry about keeping track of this. It is most important that you expose \(a\) a method to convert from a Timeline State to a Device State, \(b\) a method for diffing 2 device states and (c) a way to send commands to the device. We'll dive deeper into this in [TSR Integration API](./tsr-api). diff --git a/packages/documentation/versioned_docs/version-1.51.0/for-developers/device-integrations/intro.md b/packages/documentation/versioned_docs/version-1.51.0/for-developers/device-integrations/intro.md index a3da6b440cd..8470916a37d 100644 --- a/packages/documentation/versioned_docs/version-1.51.0/for-developers/device-integrations/intro.md +++ b/packages/documentation/versioned_docs/version-1.51.0/for-developers/device-integrations/intro.md @@ -9,7 +9,7 @@ In order to understand all about writing TSR integrations there are some concept - [TSR Types package](./tsr-types) - [TSR Actions](./tsr-actions) -But to start of we will explain the general structure of the TSR. Any user of the TSR will interface primarily with the Conductor class. Primarily the user will input device configurations, mappings and timelines into the TSR. The timeline describes the entire state of all of the devices over time. It does this by putting objects on timeline layers. Every timeline layer maps to a specific part of the device, this is configured throught the mappings. +But to start off we will explain the general structure of the TSR. Any user of the TSR will interface primarily with the Conductor class. Primarily the user will input device configurations, mappings and timelines into the TSR. The timeline describes the entire state of all of the devices over time. It does this by putting objects on timeline layers. Every timeline layer maps to a specific part of the device, this is configured through the mappings. The timeline is converted into disctinct states at different points in time, and these states are fed to the individual integrations. As an integration developer you shouldn't have to worry about keeping track of this. It is most important that you expose \(a\) a method to convert from a Timeline State to a Device State, \(b\) a method for diffing 2 device states and (c) a way to send commands to the device. We'll dive deeper into this in [TSR Integration API](./tsr-api). diff --git a/packages/documentation/versioned_docs/version-1.52.0/about-sofie.md b/packages/documentation/versioned_docs/version-1.52.0/about-sofie.md new file mode 100644 index 00000000000..bb031408a1f --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/about-sofie.md @@ -0,0 +1,22 @@ +--- +title: About Sofie +hide_table_of_contents: true +sidebar_label: About Sofie +sidebar_position: 1 +--- + +# NRK Sofie TV Automation System + +![The producer's view in Sofie](https://raw.githubusercontent.com/Sofie-Automation/Sofie-TV-automation/main/images/Sofie_GUI_example.jpg) + +_**Sofie**_ is a web-based TV automation system for studios and live shows, used in daily live TV news productions by the Norwegian public service broadcaster [**NRK**](https://www.nrk.no/about/) since September 2018. + +## Key Features + +- User-friendly, modern web-based GUI +- State-based device control and playout of video, audio, and graphics +- Modular device-control architecture with support for several hardware \(and software\) setups +- Modular data-ingest architecture, supports MOS and Google spreadsheets +- Plug-in architecture for programming shows + +_The NRK logo is a registered trademark of Norsk rikskringkasting AS. The license does not grant any right to use, in any way, any trademarks, service marks or logos of Norsk rikskringkasting AS._ diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/api-documentation.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/api-documentation.md new file mode 100644 index 00000000000..6af8e95f979 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/api-documentation.md @@ -0,0 +1,8 @@ +--- +sidebar_position: 6 +--- + +# API Documentation + +The Sofie Blueprints API and the Sofie Peripherals API documentation is automatically generated and available through +[sofie-automation.github.io/sofie-core/typedoc](https://sofie-automation.github.io/sofie-core/typedoc). diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/api-stability.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/api-stability.md new file mode 100644 index 00000000000..5368c979ac9 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/api-stability.md @@ -0,0 +1,26 @@ +--- +title: API Stability +sidebar_position: 11 +--- + +Sofie has various APIs for talking between components, and for external systems to interact with. + +We classify each api into one of two categories: + +## Stable + +This is a collection of APIs which we intend to avoid introducing any breaking change to unless necessary. This is so external systems can rely on this API without needing to be updated in lockstep with Sofie, and hopefully will make sense to developers who are not familiar with Sofie's inner workings. + +In version 1.50, a new REST API was introduced. This can be found at `/api/v1.0`, and is designed to allow an external system to interact with Sofie using simplified abstractions of Sofie internals. + +The _Live Status Gateway_ is also part of this stable API, intended to allow for reactively retrieving data from Sofie. Internally it is translating the internal APIs into a stable version. + +:::note +You can find the _Live Status Gateway_ in the `packages` folder of the [Sofie Core](https://github.com/Sofie-Automation/sofie-core) repository. +::: + +## Internal + +This covers everything we expose over DDP, the `/api/0` endpoint and any other http endpoints. + +These are intended for use between components of Sofie, which should be updated together. The DDP api does have breaking changes in most releases. We use the `server-core-integration` library to manage these typings, and to ensure that compatible versions are used together. diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/contribution-guidelines.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/contribution-guidelines.md new file mode 100644 index 00000000000..11071791583 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/contribution-guidelines.md @@ -0,0 +1,108 @@ +--- +description: >- + The Sofie team happily encourage contributions to the Sofie project, and + kindly ask you to observe these guidelines when doing so. +sidebar_position: 2 +--- + +# Contribution Guidelines + +_Last updated september 2024_ + +## About the Sofie TV Studio Automation Project + +The Sofie project includes a number of open source applications and libraries developed and maintained by the Norwegian public service broadcaster, [NRK](https://www.nrk.no/about/). Sofie has been used to produce live shows at NRK since September 2018. + +A list of the "Sofie repositories" [can be found here](libraries.md). NRK owns the copyright of the contents of the official Sofie repositories, including the source code, related files, as well as the Sofie logo. + +The Sofie team at NRK is responsible for development and maintenance. We also do thorough testing of each release to avoid regressions in functionality and ensure interoperability with the various hardware and software involved. + +The Sofie team welcomes open source contributions and will actively work towards enabling contributions to become mergeable into the Sofie repositories. However, as main stakeholder and maintainer we reserve the right to refuse any contributions. + +## About Contributions + +Thank you for considering contributing to the Sofie project! + +Before you start, there are a few things you should know: + +### “Discussions Before Pull Requests” + +**Minor changes** (most bug fixes and small features) can be submitted directly as pull requests to the appropriate official repo. + +However, Sofie is a big project with many differing users and use cases. **Larger changes** may be difficult to merge into an official repository if NRK and other contributors have not been made aware of their existence beforehand. Since figuring out what side-effects a new feature or a change may have for other Sofie users can be tricky, we advise opening an RFC issue (_Request for Comments_) early in your process. Good moments to open an RFC include: +* When a user need is identified and described +* When you have a rough idea about how a feature may be implemented +* When you have a sketch of how a feature could look like to the user + +To facilitate timely handling of larger contributions, there’s a workflow intended to keep an open dialogue between all interested parties: + +1. Contributor opens an RFC (as a _GitHub issue_) in the appropriate repository. +2. NRK evaluates the RFC, usually within a week. +3. If needed, NRK establishes contact with the RFC author, who will be invited to a workshop where the RFC is discussed. Meeting notes are published publicly on the RFC thread. +4. Discussions about the RFC continue as needed, either in workshops or in comments in the RFC thread. +5. The contributor references the RFC when a pull request is ready. + +It will be very helpful if your RFC includes specific use-cases that you are facing. Providing a background on how your users are using Sofie can clear up situations in which certain phrases or processes may be ambiguous. If during your process you have already identified various solutions as favorable or unfavorable, offering this context will move the discussion further still. + +Via the RFC process, we're looking to maximize involvement from various stakeholders, so you probably don't need to come up with a very detailed design of your proposed change or feature in the RFC. An end-user oriented description will be most valuable in creating a constructive dialogue, but don't shy away from also adding a more technical description, if you find that will convey your ideas better. + +### Base contributions on the in-development branch + +In order to facilitate merging, we ask that contributions are based on the latest (at the time of the pull request) _in-development_ branch (often named `release*`). +See **CONTRIBUTING.md** in each official repository for details on which branch to use as a base for contributions. + +## Developer Guidelines + +### Pull Requests + +We encourage you to open PRs early! If it’s still in development, open the PR as a draft. + +### Types + +All official Sofie repositories use TypeScript. When you contribute code, be sure to keep it as strictly typed as possible. + +### Code Style & Formatting + +Most of the projects use a linter (eslint) and a formatter (prettier). Before submitting a pull request, please make sure it conforms to the linting rules by running yarn lint. yarn lint --fix can fix most of the issues. + +### Documentation + +We rely on two types of documentation; the [Sofie documentation](https://sofie-automation.github.io/sofie-core/) ([source code](https://github.com/Sofie-Automation/sofie-core/tree/main/packages/documentation)) and inline code documentation. + +We don't aim to have the "absolute perfect documentation possible", BUT we do try to improve and add documentation to have a good-enough-to-be-comprehensible standard. We think that: + +- _What_ something does is not as important – we can read the code for that. +- _Why_ something does something, **is** important. Implied usage, side-effects, descriptions of the context etcetera... + +When you contribute, we ask you to also update any documentation where needed. + +### Updating Dependencies​ + +When updating dependencies in a library, it is preferred to do so via `yarn upgrade-interactive --latest` whenever possible. This is so that the versions in `package.json` are also updated as we have no guarantee that the library will work with versions lower than that used in the `yarn.lock` file, even if it is compatible with the semver range in `package.json`. After this, a `yarn upgrade` can be used to update any child dependencies + +Be careful when bumping across major versions. + +Also, each of the libraries has a minimum nodejs version specified in their package.json. Care must be taken when updating dependencies to ensure its compatibility is retained. + +### Resolutions​ + +We sometimes use the `yarn resolutions` property in `package.json` to fix security vulnerabilities in dependencies of libraries that haven't released a fix yet. If adding a new one, try to make it as specific as possible to ensure it doesn't have unintended side effects. + +When updating other dependencies, it is a good idea to make sure that the resolutions defined still apply and are correct. + +### Logging + +When logging, we try to adher to the following guideliness: + +Usage of `console.log` and `console.error` directly is discouraged (except for quick debugging locally). Instead, use one of the logger libraries (to output json logs which are easier to index). +When logging, use one of the **log level** described below: + +| Level | Description | Examples | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `silly` | For very detailed logs (rarely used). | - | +| `debug` | Logging of info that could be useful for developers when debugging certain issues in production. | `"payload: {>JSON<} "`

`"Reloading data X from DB"` | +| `verbose` | Logging of common events. | `"File X updated"` | +| `info` | Logging of significant / uncommon events.

_Note: If an event happens often or many times, use `verbose` instead._ | `"Initializing TSR..."`

`"Starting nightly cronjob..."`

`"Snapshot X restored"`

`"Not allowing removal of current playing segment 'xyz', making segment unsynced instead"`

`"PeripheralDevice X connected"` | +| `warn` | Used when something unexpected happened, but not necessarily due to an application bug.

These logs don't have to be acted upon directly, but could be useful to provide context to a dev/sysadmin while troubleshooting an issue. | `"PeripheralDevice X disconnected"`

`"User Error: Cannot activate Rundown (Rundown not found)" `

`"mosRoItemDelete NOT SUPPORTED"` | +| `error` | Used when something went _wrong_, preventing something from functioning.

A logged `error` should always result in a sysadmin / developer looking into the issue.

_Note: Don't use `error` for things that are out of the app's control, such as user error._ | `"Cannot read property 'length' of undefined"`

`"Failed to save Part 'X' to DB"` | +| `crit` | Fatal errors (rarely used) | - | diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/data-model.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/data-model.md new file mode 100644 index 00000000000..f835ecbb4f4 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/data-model.md @@ -0,0 +1,132 @@ +--- +title: Data Model +sidebar_position: 9 +--- + +Sofie persists the majority of its data in a MongoDB database. This allows us to use Typescript friendly documents, +without needing to worry too much about the strictness of schemas, and allows us to watch for changes happening inside +the database as a way of ensuring that updates are reactive. + +Data is typically pushed to the UI or the gateways through [Publications](./publications) over the DDP connection that Meteor provides. + +## Collection Ownership + +Each collection in MongoDB is owned by a different area of Sofie. In some cases, changes are also made by another area, but we try to keep this to a minimum. +In every case, any layout changes and any scheduled cleanup are performed by the Meteor layer for simplicity. + +### Meteor + +This category of collections is rather loosely defined, as it ends up being everything that doesn't belong somewhere else + +This consists of anything that is configurable from the Sofie UI, anything needed solely for the UI and some other bits. Additionally, there are some collections which are populated by other portions of a Sofie system, such as by Package Manager, through an API over DDP. +Currently, there is not a very clearly defined flow for modifying these documents, with the UI often making changes directly with minimal or no validation. + +This includes: + +- [Blueprints](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Blueprint.ts) +- [Buckets](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/Buckets.ts) +- [CoreSystem](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/CoreSystem.ts) +- [Evaluations](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/Evaluations.ts) +- [ExternalMessageQueue](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ExternalMessageQueue.ts) +- [ExpectedPackageWorkStatuses](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ExpectedPackageWorkStatuses.ts) +- [MediaObjects](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/MediaObjects.ts) +- [MediaWorkFlows](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/MediaWorkFlows.ts) +- [MediaWorkFlowSteps](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/MediaWorkFlowSteps.ts) +- [Organizations](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/Organization.ts) +- [PackageInfos](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PackageInfos.ts) +- [PackageContainerPackageStatuses](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PackageContainerPackageStatus.ts) +- [PackageContainerStatuses](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PackageContainerStatus.ts) +- [PeripheralDeviceCommands](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PeripheralDeviceCommand.ts) +- [PeripheralDevices](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PeripheralDevice.ts) +- [RundownLayouts](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/RundownLayouts.ts) +- [ShowStyleBase](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ShowStyleBase.ts) +- [ShowStyleVariant](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ShowStyleVariant.ts) +- [Snapshots](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/Snapshots.ts) +- [Studio](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Studio.ts) +- [TriggeredActions](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/TriggeredActions.ts) +- [TranslationsBundles](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/TranslationsBundles.ts) +- [UserActionsLog](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/UserActionsLog.ts) +- [Users](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/Users.ts) +- [Workers](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/Workers.ts) +- [WorkerThreads](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/WorkerThreads.ts) + +### Ingest + +This category of collections is owned by the ingest [worker threads](./worker-threads-and-locks.md), and models a Rundown based on how it is defined by the NRCS. + +These collections are not exposed as writable in Meteor, and are only allowed to be written to by the ingest worker threads. +There is an exception to both of these; Meteor is allowed to write to it as part of migrations, and cleaning up old documents. While the playout worker is allowed to modify certain Segments that are labelled as being owned by playout. + +The collections which are owned by the ingest workers are: + +- [AdLibActions](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/AdLibActions.ts) +- [AdLibPieces](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/AdLibPieces.ts) +- [BucketAdLibActions](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/BucketAdLibActions.ts) +- [BucketAdLibPieces](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/BucketAdLibPieces.ts) +- [ExpectedMediaItems](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ExpectedMediaItems.ts) +- [ExpectedPackages](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ExpectedPackages.ts) +- [ExpectedPlayoutItems](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ExpectedPlayoutItems.ts) +- [IngestDataCache](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/IngestDataCache.ts) +- [Parts](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Parts.ts) +- [Pieces](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Pieces.ts) +- [RundownBaselineAdLibActions](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/RundownBaselineAdLibActions.ts) +- [RundownBaselineAdLibPieces](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/RundownBaselineAdLibPieces.ts) +- [RundownBaselineObjects](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/RundownBaselineObjects.ts) +- [Rundowns](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Rundowns.ts) +- [Segments](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Segments.ts) + +These collections model a Rundown from the NRCS in a Sofie form. Almost all of these contain documents which are largely generated by blueprints. +Some of these collections are used by Package Manager to initiate work, while others form a view of the Rundown for the users, and are used as part of the model for playout. + +### Playout + +This category of collections is owned by the playout [worker threads](./worker-threads-and-locks.md), and is used to model the playout of a Rundown or set of Rundowns. + +During the final stage of an ingest operation, there is a period where the ingest worker acquires a `PlaylistLock`, so that it can ensure that the RundownPlaylist the Rundown is a part of is updated with any necessary changes following the ingest operation. During this lock, it will also attempt to [sync any ingest changes](./for-blueprint-developers/sync-ingest-changes) to the PartInstances and PieceInstances, if supported by the blueprints. + +As before, Meteor is allowed to write to these collections as part of migrations, and cleaning up old documents. + +The collections which can only be modified inside of a `PlaylistLock` are: + +- [PartInstances](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PartInstances.ts) +- [PieceInstances](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PieceInstances.ts) +- [RundownPlaylists](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/RundownPlaylists.ts) +- [Timelines](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Timelines.ts) +- [TimelineDatastore](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/TimelineDatastore.ts) + +These collections are used in combination with many of the ingest collections, to drive playout. + +#### RundownPlaylist + +RundownPlaylists are a Sofie invention designed to solve one problem; in some NRCS it is beneficial to build a show across multiple Rundowns, which should then be concatenated for playout. +In particular, MOS has no concept of a Playlist, only Rundowns, and it was here where we need to be able to combine multiple Rundowns. + +This functionality can be used to either break down long shows into managable chunks, or to indicate a different type of show between the each portion. + +Because of this, RundownPlaylists are largely missing from the ingest side of Sofie. We do not expose them in the ingest APIs, or do anything with them throughout the majority of the blueprints generating a Rundown. +Instead, we let the blueprints specify that a Rundown should be part of a RundownPlaylist by setting the `playlistExternalId` property, where multiple Rundowns in a Studio with the same id will be grouped into a RundownPlaylist. +If this property is not used, we automatically generate a RundownPlaylist containing the Rundown by itself. + +It is during the final stages of an ingest operation, where the RundownPlaylist will be generated (with the help of blueprints), if it is necessary. +Another benefit to this approach, is that it allows for very cheaply and easily moving Rundowns between RundownPlaylists, even safely affecting a RundownPlaylist that is currently on air. + +#### Part vs PartInstance and Piece vs PieceInstance + +In the early days of Sofie, we had only Parts and Pieces, no PartInstances and PieceInstances. + +This quickly became costly and complicated to handle cases where the user used Adlibs in Sofie. Some of the challenges were: + +- When a Part is deleted from the NRCS and that part is on air, we don't want to delete it in Sofie immediately +- When a Part is modified in the NRCS and that part is on air, we may not want to apply all of the changes to playout immediately +- When a Part has finished playback and is set-as-next again, we need to make sure to discard any changes made by the previous playout, and restore it to as if was refreshly ingested (including the changes we ignored while it was on air) +- When creating an adlib part, we need to be sure that an ingest operation doesn't attempt to delete it, until playout is finished with it. +- After using an adlib in a part, we need to remove the piece it created when we set-as-next again, or reset the rundown +- When an earlier part is removed, where an infinite piece has spanned into the current part, we may not want to remove that infinite piece + +Our solution to some of this early on was to not regenerate certain Parts when receiving ingest operations for them, and to defer it until after that Part was off air. While this worked, it was not optimal to re-run ingest operations like that while doing a take. This also required the blueprint api to generate a single part in each call, which we were starting to find limiting. This was also problematic when resetting a rundown, as that would often require rerunning ingest for the whole rundown, making it a notably slow operation. + +At this point in time, Adlib Actions did not exist in Sofie. They are able to change almost every property of a Part of Piece that ingest is able to define, which makes the resetting process harder. + +PartInstances and PieceInstances were added as a way for us to make a copy of each Part and Piece, as it was selected for playout, so that we could allow ingest without risking affecting playout, and to simplify the cleanup performed. The PartInstances and PieceInstances are our record of how the Rundown was played, which we can utilise to output metadata such as for chapter markers on a web player. In earlier versions of Sofie this was tracked independently with an `AsRunLog`, which resulted in odd issues such as having `AsRunLog` entries which referred to a Part which no longer existed, or whose content was very different to how it was played. + +Later on, this separation has allowed us to more cleanly define operations as ingest or playout, and allows us to run them in parallel with more confidence that they won't accidentally wipe out each others changes. Previously, both ingest and playout operations would be modifying documents in the Piece and Part collections, making concurrent operations unsafe as they could be modifying the same Part or Piece. diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/_category_.json b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/_category_.json new file mode 100644 index 00000000000..5f6541c2b5f --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Device Integrations", + "position": 5 +} \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/intro.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/intro.md new file mode 100644 index 00000000000..928514cc1fa --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/intro.md @@ -0,0 +1,18 @@ +# Introduction + +Device integrations in Sofie are part of the Timeline State Resolver (TSR) library. A device integration has a couple of responsibilities in the Sofie eco system. First and foremost it should establish a connection with a foreign device. It should also be able to convert Sofie's idea of what the device should be doing into commands to control the device. And lastly it should export interfaces to be used by the blueprints developer. + +In order to understand all about writing TSR integrations there are some concepts to familiarise yourself with, in this documentation we will attempt to explain these. + +- [Options and mappings](./options-and-mappings) +- [TSR Integration API](./tsr-api) +- [TSR Types package](./tsr-types) +- [TSR Actions](./tsr-actions) + +But to start of we will explain the general structure of the TSR. Any user of the TSR will interface primarily with the Conductor class. Primarily the user will input device configurations, mappings and timelines into the TSR. The timeline describes the entire state of all of the devices over time. It does this by putting objects on timeline layers. Every timeline layer maps to a specific part of the device, this is configured throught the mappings. + +The timeline is converted into disctinct states at different points in time, and these states are fed to the individual integrations. As an integration developer you shouldn't have to worry about keeping track of this. It is most important that you expose \(a\) a method to convert from a Timeline State to a Device State, \(b\) a method for diffing 2 device states and (c) a way to send commands to the device. We'll dive deeper into this in [TSR Integration API](./tsr-api). + +:::info +The information in this section is not a conclusive guide on writing an integration, it should be use more as a guide to use while looking at a TSR integration such as the [OSC integration](https://github.com/Sofie-Automation/sofie-timeline-state-resolver/tree/main/packages/timeline-state-resolver/src/integrations/osc). +::: diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/options-and-mappings.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/options-and-mappings.md new file mode 100644 index 00000000000..343b3821e59 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/options-and-mappings.md @@ -0,0 +1,11 @@ +# Options and mappings + +For an end user to configure the system from the Sofie UI we have to expose options and mappings from the TSR. This is done through [JSON config schemas](../json-config-schema) in the `$schemas` folder of your integration. + +## Options + +Options are for any configuration the user needs to make for your device integration to work well. Things like IP addresses and ports go here. + +## Mappings + +A mappings is essentially an addresses into the device you are integrating with. For example, a mapping for CasparCG contains a channel and a layer. And a mapping for an Atem can be a mix effect or a downstream keyer. It is entirely possible for the user to define 2 mappings pointing to the same bit of hardware so keep that in mind while writing your integration. The granularity of the mappings influences both how you write your device as well as the shape of the timeline objects. If, for example, we had not included the layer number in the CasparCG mapping, we would have had to define this separately on every timeline object. \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-actions.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-actions.md new file mode 100644 index 00000000000..791c6f5a26c --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-actions.md @@ -0,0 +1,11 @@ +# TSR Actions + +Sometimes a state based model isn't enough and you just need to fire an action. In Sofie we try to be strict about any playout operations needing to be state based, i.e. doing a transition operation on a vision mixer should be a result of a state change, not an action. However, there are things that are easier done with actions. For example cleaning up a playlist on a graphics server or formatting a disk on a recorder. For these scenarios we have added TSR Actions. + +TSR Actions can be triggered through the UI by a user, through blueprints when the rundown is activated or deactivated or through adlib actions. + +When implementing the TSR Actions API you should start by defining a JSON schema outlying the action id's and payload your integration will consume. Once you've done this you're ready to implement the actions as callbacks on the `actions` property of your integration. + +:::warning +Beware that if your action changes the state of the device you should handle this appropriately by resetting the resolver +::: diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-api.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-api.md new file mode 100644 index 00000000000..e68424455e4 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-api.md @@ -0,0 +1,28 @@ +# TSR Integration API + +:::info +As of version 1.50, there still exists a legacy API for device integrations. In this documentation we will only consider the more modern variant informally known as the _StateHandler_ format. +::: + +## Setup and status + +There are essentially 2 parts to the TSR API, the first thing you need to do is set up a connection with the device you are integrating with. This is done in the `init` method. It takes a parameter with the Device options as specified in the config schema. Additionally a `terminate` call is to be implemented to tear down the connection and prepare any timers to be garbage collected. + +Regarding status there are 2 important methods to be implemented, one is a getter for the `connected` status of the integration and the other is `getStatus` which should inform a TSR user of the status of device. You can add messages in this status as well. + +## State and commands + +The second part is where the bulk of the work happens. First your implementation for `convertTimelineStateToDeviceState` will be called with a Timeline State and the mappings for your integration. You are ought to return a "Device State" here which is an object representing the state of your device as inferred from the Timeline State and mappings. Then the next implementation is of the `diffStates` method, which will be called with 2 Device States as you've generated them earlier. The purpose of this method is to generate commands such that a state change from Device State A to Device State B can be executed. Hence it is called a "diff". The last important method here is `sendCommand` which will be called with the commands you've generated earlier when the TSR wants to transitition from State A to State B. + +Another thing to implement is the `actions` property. You can leave it as an empty object initially or read more about it in [TSR Actions](./tsr-actions.md). + +## Logging and emitting events + +Logging is done through an event emitter as is described in the DeviceEvents interface. You should also emit an event any time the connection status should change. There is an event you can emit to rerun the resolving process in TSR as well, this will more or less create new Timeline States from the timeline, diff them and see if they should be executed. + +## Best practices + + - The `init` method is asynchronous but you should not use it to wait for timeouts in your connection to reject it. Instead the rest of your integration should gracefully deal with a (initially) disconnected device. + - The result of the `getStatus` method is displayed in the UI of Sofie so try to put helpful information in the messages and only elevate to a "bad" status if something is really wrong, like being fully disconnected from a device. + - Be aware for side effects in your implementations of `convertTimelineStateToDeviceState` and `diffStates` they are _not_ guaranteed to be chronological and the states changes may never actually be executed. + - If you need to do any time aware commands (such as seeking in a media file) use the time from the Timeline State to do your calculations for these \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-types.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-types.md new file mode 100644 index 00000000000..0c9d2e5108c --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-types.md @@ -0,0 +1,7 @@ +# TSR Types + +The TSR monorepo contains a types package called `timeline-state-resolver-types`. The intent behind this package is that you may want to generate a Timeline in a place where you don't want to import the TSR library for performance reasons. Blueprints are a good example of this since the webpack setup does not deal well with importing everything. + +## What you should know about this + +When the TSR is built the types for the Mappings, Options and Actions for your integration will be auto generated under `src/generated`. In addition to this you should describe the content property of the timeline objects in a file using interfaces. If you're adding a new integration also add it to the `DeviceType` enum as described in `index.ts`. diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/_category_.json b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/_category_.json new file mode 100644 index 00000000000..c4c3c8c2424 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "For Blueprint Developers", + "position": 4 +} \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/_part-timings-demo.jsx b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/_part-timings-demo.jsx new file mode 100644 index 00000000000..98cb9f4275c --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/_part-timings-demo.jsx @@ -0,0 +1,173 @@ +import React, { useState } from 'react' + +/** + * This is a demo showing the interactions between the part and piece groups on the timeline. + * The maths should be the same as in `meteor/lib/rundown/timings.ts`, but in a simplified form + */ + +const MS_TO_PIXEL_CONSTANT = 0.1 + +const viewPortStyle = { + width: '100%', + backgroundSize: '40px 40px', + backgroundImage: + 'linear-gradient(to right, grey 1px, transparent 1px), linear-gradient(to bottom, grey 1px, transparent 1px)', + overflowX: 'hidden', + display: 'flex', + flexDirection: 'column', + position: 'relative', +} + +export function PartTimingsDemo() { + const [postrollA1, setPostrollA1] = useState(0) + const [postrollA2, setPostrollA2] = useState(0) + const [prerollB1, setPrerollB1] = useState(0) + const [prerollB2, setPrerollB2] = useState(0) + const [outTransitionDuration, setOutTransitionDuration] = useState(0) + const [inTransitionBlockDuration, setInTransitionBlockDuration] = useState(0) + const [inTransitionContentsDelay, setInTransitionContentsDelay] = useState(0) + const [inTransitionKeepaliveDuration, setInTransitionKeepaliveDuration] = useState(0) + + // Arbitrary point in time for the take to be based around + const takeTime = 2400 + + const outTransitionTime = outTransitionDuration - inTransitionKeepaliveDuration + + // The amount of time needed to preroll Part B before the 'take' point + const partBPreroll = Math.max(prerollB1, prerollB2) + const prerollTime = partBPreroll - inTransitionContentsDelay + + // The amount to delay the part 'switch' to, to ensure the outTransition has time to complete as well as any prerolls for part B + const takeOffset = Math.max(0, outTransitionTime, prerollTime) + const takeDelayed = takeTime + takeOffset + + // Calculate the part A objects + const pieceA1 = { time: 0, duration: takeDelayed + inTransitionKeepaliveDuration + postrollA1 } + const pieceA2 = { time: 0, duration: takeDelayed + inTransitionKeepaliveDuration + postrollA2 } + const partA = { time: 0, duration: Math.max(pieceA1.duration, pieceA2.duration) } // part stretches to contain the piece + + // Calculate the transition objects + const pieceOutTransition = { + time: partA.time + partA.duration - outTransitionDuration - Math.max(postrollA1, postrollA2), + duration: outTransitionDuration, + } + const pieceInTransition = { time: takeDelayed, duration: inTransitionBlockDuration } + + // Calculate the part B objects + const partBBaseDuration = 2600 + const partB = { time: takeTime, duration: partBBaseDuration + takeOffset } + const pieceB1 = { time: takeDelayed + inTransitionContentsDelay - prerollB1, duration: partBBaseDuration + prerollB1 } + const pieceB2 = { time: takeDelayed + inTransitionContentsDelay - prerollB2, duration: partBBaseDuration + prerollB2 } + const pieceB3 = { time: takeDelayed + inTransitionContentsDelay + 300, duration: 200 } + + return ( +
+
+ + + + + + + + + + + + + + + +
+ + {/* Controls */} + + + + + + + + + +
+
+ ) +} + +function TimelineGroup({ duration, time, name, color }) { + return ( +
+ {name} +
+ ) +} + +function TimelineMarker({ time, title }) { + return ( +
+   +
+ ) +} + +function InputRow({ label, max, value, setValue }) { + return ( + + {label} + + setValue(parseInt(e.currentTarget.value))} + /> + + + ) +} diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/ab-playback.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/ab-playback.md new file mode 100644 index 00000000000..1a78316f770 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/ab-playback.md @@ -0,0 +1,236 @@ +# AB Playback + +:::info +Prior to 1.50 of Sofie, this was implemented in Blueprints and not natively in Sofie-core +::: + +_AB Playback_ is a common technique for clip playback. The aim is to be able to play multiple clips back to back, alternating which player is used for each clip. +At first glance it sounds simple to handle, but it quickly becomes complicated when we consider the need to allow users to run adlibs and that the system needs to seamlessly update pre-programmed clips when this happens. + +To avoid this problem, we take an approach of labelling pieces as needing an AB assignment and leaving timeline objects to have some unresolved values during the ingest blueprint operations, and we perform the AB resolving when building the timeline for playout. + +There are other challenges to the resolving to think about too, which make this a challenging area to tackle, and not something that wants to be considered when starting out with blueprints. Some of these challenges are: + +- Users get confused if the player of a clip changes without a reason +- Reloading an already loaded clip can be costly, so should be avoided when possible +- Adlibbing a clip, or changing what Part is nexted can result in needing to move what player a clip has assigned +- Postroll or preroll is often needed +- Some studios can have less players available than ideal. (eg, going back to back between two clips, and a clip is playing on the studio monitor) + +## Defining Piece sessions + +An AB-session is a request for an AB player for the lifetime of the object or Piece. The resolver operates on these sessions, to identify when players are needed and to identify which objects and Pieces are linked and should use the same Player. + +In order for the AB resolver to know what AB sessions there are on the timeline, and how they all relate to each other, we define `abSessions` properties on various objects when defining Pieces and their content during the `getSegment` blueprint method. + +The AB resolving operates by looking at all the Pieces on the timeline, and plotting all the requested abSessions out in time. It will then iterate through each of these sessions in time order and assign them in order to the available players. +Note: The sessions of TimelineObjects are not considered at this point, except for those in lookahead. + +Both Pieces and TimelineObjects accept an array of AB sessions, and are capable of using multiple AB pools on the same object. Eg, choosing a clip player and the DVE to play it through. + +:::warning +The sessions of TimelineObjects are not considered during the resolver stage, except for lookahead objects. +If a TimelineObject has an `abSession` set, its parent Piece must declare the same session. +::: + +For example: + +```ts +const partExternalId = 'id-from-nrcs' +const piece: Piece = { + externalId: partExternalId, + name: 'My Piece', + + abSessions: [{ + sessionName: partExternalId, + poolName: 'clip' + }], + + ... +} +``` + +This declares that this Piece requires a player from the 'clip' pool, with a unique sessionName. + +:::info +The `sessionName` property is an identifier for a session within the Segment. +Any other Pieces or TimelineObjects that want to share the session should use the same sessionName. Unrelated sessions must use a different name. +::: + +## Enabling AB playback resolving + +To enable AB playback for your blueprints, the `getAbResolverConfiguration` method of a ShowStyle blueprint must be implemented. This informs Sofie that you want the AB playback logic to run, and configures the behaviour. + +A minimal implementation of this is: + +```ts +getAbResolverConfiguration: (context: IShowStyleContext): ABResolverConfiguration => { + return { + resolverOptions: { + idealGapBefore: 1000, + nowWindow: 2000, + }, + pools: { + clip: [1, 2], + }, + } +} +``` + +The `resolverOptions` property defines various configuration that will affect how sessions are assigned to players. +The `pools` property defines the AB pools in your system, along with the ids of the players in the pools. These do not have to be sequential starting from 1, and can be any numbers you wish. The order used here will define the order the resolver will assign to. + +## Updating the timeline from the assignments + +There are 3 possible strategies for applying the assignments to timeline objects. The applying and ab-resolving is done just before `onTimelineGenerate` from your blueprints is called. + +### TimelineObject Keyframes + +The simplest approach is to use timeline keyframes, which can be labelled as belong to an abSession. These keyframes must be generated during ingest. + +This strategy works best for changing inputs on a video-mixer or other scenarios where a property inside of a timeline object needs changing. + +```ts +let obj = { + id: '', + enable: { start: 0 }, + layer: 'atem_me_program', + content: { + deviceType: TSR.DeviceType.ATEM, + type: TSR.TimelineContentTypeAtem.ME, + me: { + input: 0, // placeholder + transition: TSR.AtemTransitionStyle.CUT, + }, + }, + keyframes: [ + { + id: `mp_1`, + enable: { while: '1' }, + disabled: true, + content: { + input: 10, + }, + preserveForLookahead: true, + abSession: { + pool: 'clip', + index: 1, + }, + }, + { + id: `mp_2`, + enable: { while: '1' }, + disabled: true, + content: { + input: 11, + }, + preserveForLookahead: true, + abSession: { + pool: 'clip', + index: 2, + }, + }, + ], + abSessions: [ + { + pool: 'clip', + name: 'abcdef', + }, + ], +} +``` + +This object demonstrates how keyframes can be used to perform changes based on an assigned ab player session. The object itself must be labelled with the `abSession`, in the same way as the Piece is. +Each keyframe can be labelled with an `abSession`, with only one from the pool being left active. If `disabled` is set on the keyframe, that will be unset, and the other keyframes for the pool will be removed. + +Setting `disabled: true` is not strictly necessary, but ensures that the keyframe will be inactive in case that ab-pool is not processed. +In this example we are setting `preserveForLookahead` so that the keyframes are present on lookahead objects. If not set, then the keyframes will be removed by lookahead. + +### TimelineObject layer changing + +Another apoproach is to move objects between timeline layers. For example, player 1 is on CasparCG channel 1, with player 2 on CasparCG channel 2. This requires a different mapping for each layer. + +This strategy works best for playing a clip, where the whole object needs to move to different mappings. + +To enable this, the `ABResolverConfiguration` object returned from `getAbResolverConfiguration` can have a set of rules defined with the `timelineObjectLayerChangeRules` property. + +For example: + +```ts +getAbResolverConfiguration: (context: IShowStyleContext): ABResolverConfiguration => { + return { + resolverOptions: { + idealGapBefore: 1000, + nowWindow: 2000, + }, + pools: { + clip: [1, 2], + }, + timelineObjectLayerChangeRules: { + ['casparcg_player_clip_pending']: { + acceptedPoolNames: [AbSessionPool.CLIP], + newLayerName: (playerId: number) => `casparcg_player_clip_${playerId}`, + allowsLookahead: true, + }, + }, + } +} +``` + +And a timeline object: + +```ts +const clipObject: TimelineObjectCoreExt<> = { + id: '', + enable: { start: 0 }, + layer: 'casparcg_player_clip_pending', + content: { + deviceType: TSR.DeviceType.CASPARCG, + type: TSR.TimelineContentTypeCasparCg.MEDIA, + file: 'AMB', + }, + abSessions: [ + { + pool: 'clip', + name: 'abcdef', + }, + ], +} +``` + +This will result in the timeline object being moved to `casparcg_player_clip_1` if the clip is assigned to player 1, or `casparcg_player_clip_2` if the clip is assigned to player 2. + +This is also compatible with lookahead. To do this, the `casparcg_player_clip_pending` mapping should be created with the lookahead configuration set there, this should be of type `ABSTRACT`. The AB resolver will detect this lookahead object and it will get an assignment when a player is available. Lookahead should not be enabled for the `casparcg_player_clip_1` and other final mappings, as lookahead is run before AB so it will not find any objects on those layers. + +### Custom behaviour + +Sometimes, something more complex is needed than what the other options allow for. To support this, the `ABResolverConfiguration` object has an optional property `customApplyToObject`. It is advised to use the other two approaches when possible. + +```ts +getAbResolverConfiguration: (context: IShowStyleContext): ABResolverConfiguration => { + return { + resolverOptions: { + idealGapBefore: 1000, + nowWindow: 2000, + }, + pools: { + clip: [1, 2], + }, + customApplyToObject: ( + context: ICommonContext, + poolName: string, + playerId: number, + timelineObject: OnGenerateTimelineObj + ) => { + // Your own logic here + + return false + }, + } +} +``` + +Inside this function you are able to make any changes you like to the timeline object. +Return true if the object was changed, or false if it is unchanged. This allows for logging whether Sofie failed to modify an object for an ab assignment. + +For example, we use this to remap audio channels deep inside of some Sisyfos timeline objects. It is not possible for us to do this with keyframes due to the keyframes being applied with a shallow merge for the Sisyfos TSR device. diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/hold.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/hold.md new file mode 100644 index 00000000000..040e241a6e6 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/hold.md @@ -0,0 +1,52 @@ +# Hold + +_Hold_ is a feature in Sofie to allow for a special form of take between two parts. It allows for the new part to start with some portions of the old part being retained, with the next 'take' stopping the remaining portions of the old part and not performing a true take. + +For example, it could be setup to hold back the video when going between two clips, creating what is known in film editing as a [split edit](https://en.wikipedia.org/wiki/Split_edit) or [J-cut](https://en.wikipedia.org/wiki/J_cut). The first _Take_ would start the audio from an _A-Roll_ (second clip), but keep the video playing from a _B-Roll_ (first clip). The second _Take_ would stop the first clip entirely, and join the audio and video for the second clip. + +![A timeline of a J-Cut in a Non-Linear Video Editor](/img/docs/video_edit_hold_j-cut.png) + +## Flow + +While _Hold_ is active or in progress, an indicator is shown in the header of the UI. +![_Hold_ in Rundown View header](/img/docs/rundown-header-hold.png) + +It is not possible to run any adlibs while a hold is active, or to change the nexted part. Once it is in progress, it is not possible to abort or cancel the _Hold_ and it must be run to completion. If the second part has an autonext and that gets reached before the _Hold_ is completed, the _Hold_ will be treated as completed and the autonext will execute as normal. + +When the part to be held is playing, with the correct part as next, the flow for the users is: + +- Before + - Part A is playing + - Part B is nexted +- Activate _Hold_ (By hotkey or other user action) + - Part A is playing + - Part B is nexted +- Perform a take into the _Hold_ + - Part B is playing + - Portions of Part A remain playing +- Perform a take to complete the _Hold_ + - Part B is playing + +Before the take into the _Hold_, it can be cancelled in the same way it was activated. + +## Supporting Hold in blueprints + +:::note +The functionality here is a bit limited, as it was originally written for one particular use-case and has not been expanded to support more complex scenarios. +Some unanswered questions we have are: + +- Should _Hold_ be rewritten to be done with adlib-actions instead to allow for more complex scenarios? +- Should there be a way to more intelligently check if _Hold_ can be done between two Parts? (perhaps a new blueprint method?) + ::: + +The blueprints have to label parts as supporting _Hold_. +You can do this with the [`holdMode`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.IBlueprintPart.html#holdMode) property, and labelling it possible to _Hold_ from or to the part. + +Note: If the user manipulates what part is set as next, they will be able to do a _Hold_ between parts that are not sequential in the Rundown. + +You also have to label Pieces as something to extend into the _Hold_. Not every piece will be wanted, so it is opt-in. +You can do this with the [`extendOnHold`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.IBlueprintPiece.html#extendOnHold) property. The pieces will get extended in the same way as infinite pieces, but limited to only be extended into the one part. The usual piece collision and priority logic applies. + +Finally, you may find that there are some timeline objects that you don't want to use inside of the extended pieces, or there are some objects in the part that you don't want active while the _Hold_ is. +You can mark an object with the [`holdMode`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.TimelineObjectCoreExt.html#holdMode) property to specify its presence during a _Hold_. +The `HoldMode.ONLY` mode tells the object to only be used when in a _Hold_, which allows for doing some overrides in more complex scenarios. diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/intro.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/intro.md new file mode 100644 index 00000000000..a4b1ef62e6b --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/intro.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 1 +--- + +# Introduction + +:::caution +Documentation for this page is yet to be written. +::: + +[Blueprints](../../user-guide/concepts-and-architecture.md#blueprints) are programs that run inside Sofie Core and interpret +data coming in from the Rundowns and transform that into playable elements. They use an API published in [@sofie-automation/blueprints-integration](https://sofie-automation.github.io/sofie-core/typedoc/modules/_sofie_automation_blueprints_integration.html) library to expose their functionality and communicate with Sofie Core. + +Technically, a Blueprint is a JavaScript object, implementing one of the `BlueprintManifestBase` interfaces. + +Currently, there are three types of Blueprints: + +- [Show Style Blueprints](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.ShowStyleBlueprintManifest.html) - handling converting NRCS Rundown data into Sofie Rundowns and content. +- [Studio Blueprints](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.StudioBlueprintManifest.html) - handling selecting ShowStyles for a given NRCS Rundown and assigning NRCS Rundowns to Sofie Playlists +- [System Blueprints](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.SystemBlueprintManifest.html) - handling system provisioning and global configuration diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/lookahead.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/lookahead.md new file mode 100644 index 00000000000..f1d10c34381 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/lookahead.md @@ -0,0 +1,96 @@ +# Lookahead + +Lookahead allows Sofie to look into future Parts and Pieces, in order to preload or preview what is coming up. The aim is to fill in the gaps between your TimelineObjects with lookahead versions of these objects. +In this way, it can be used to provide functionality such as an AUX on your vision mixer showing the next cut, or to load the next clip into the media player. + +## Defining + +Lookahead can be enabled by configuring a few properties on a mapping: + +```ts +/** What method core should use to create lookahead objects for this layer */ +lookahead: LookaheadMode +/** The minimum number lookahead objects to create from future parts for this layer. Default = 1 */ +lookaheadDepth?: number +/** Maximum distance to search for lookahead. Default = undefined */ +lookaheadMaxSearchDistance?: number +``` + +With `LookaheadMode` defined as: + +```ts +export enum LookaheadMode { + /** + * Disable lookahead for this layer + */ + NONE = 0, + /** + * Preload content with a secondary layer. + * This requires support from the TSR device, to allow for preloading on a resource at the same time as it being on air. + * For example, this allows for your TimelineObjects to control the foreground of a CasparCG layer, with lookahead controlling the background of the same layer. + */ + PRELOAD = 1, + /** + * Fill the gaps between the planned objects on a layer. + * This is the primary lookahead mode, and appears to TSR devices as a single layer of simple objects. + */ + WHEN_CLEAR = 3, +} +``` + +If undefined, `lookaheadMaxSearchDistance` currently has a default distance of 10 parts. This number was chosen arbitrarily, and could change in the future. Be careful when choosing a distance to not set it too high. All the Pieces from the parts being searched have to be loaded from the database, which can come at a noticeable cost. + +If you are doing [AB Playback](./ab-playback.md), or performing some other processing of the timeline in `onTimelineGenerate`, you may benefit from increasing the value of `lookaheadDepth`. In the case of AB Playback, you will likely want to set it to the number of players available in your pool. + +Typically, TimelineObjects do not need anything special to support lookahead, other than a sensible `priority` value. Lookahead objects are given a priority between `0` and `0.1`. Generally, your baseline objects should have a priority of `0` so that they are overridden by lookahead, and any objects from your Parts and Pieces should have a priority of `1` or higher, so that they override lookahead objects. + +If there are any keyframes on TimelineObjects that should be preserved when being converted to a lookahead object, they will need the `preserveForLookahead` property set. + +## How it works + +Lookahead is calculated while the timeline is being built, and searches based on the playhead, rather than looking at the planned Parts. + +The searching operates per-layer first looking at the current PartInstance, then the next PartInstance and then any Parts after the next PartInstance in the rundown. Any Parts marked as `invalid` or `floated` are ignored. This is what allows lookahead to be dynamic based on what the User is doing and intending to play. + +It is searching Parts in that order, until it has either searched through the `lookaheadMaxSearchDistance` number of Parts, or has found at least `lookaheadDepth` future timeline objects. + +Any pieces marked as `pieceType: IBlueprintPieceType.InTransition` will be considered only if playout intends to use the transition. +If an object is found in both a normal piece with `{ start: 0 }` and in an InTransition piece, then the objects from the normal piece will be ignored. + +These objects are then processed and added to the timeline. This is done in one of two ways: + +1. As timed objects. + If the object selected for lookahead is already on the timeline (it is in the current part, or the next part and autonext is enabled), then timed lookahead objects are generated. These objects are to fill in the gaps, and get their `enable` object to reference the objects on the timeline that they are filling between. + The `lookaheadDepth` setting of the mapping is ignored for these objects. + +2. As future objects. + If the object selected for lookahead is not on the timeline, then simpler objects are generated. Instead, these get an enable of either `{ while: '1' }`, or set to start after the last timed object on that layer. This lets them fill all the time after any other known objects. + The `lookaheadDepth` setting of the mapping is respected for these objects, with this number defining the **minimum** number future objects that will be produced. These future objects are inserted with a decreasing `priority`, starting from 0.1 decreasing down to but never reaching 0. + When using the `WHEN_CLEAR` lookahead mode, all but the first will be set as `disabled`, to ensure they aren't considered for being played out. These `disabled` objects can be used by `onTimelineGenerate`, or they will be dropped from the timeline if left `disabled`. + When there are multiple future objects on a layer, only the first is useful for playout directly, but the others are often utilised for [AB Playback](./ab-playback.md) + +Some additional changes done when processing each lookahead timeline object: + +- The `id` is processed to be unique +- The `isLookahead` property is set as true +- If the object has any keyframes, any not marked with `preserveForLookahead` are removed +- The object is removed from any group it was contained within +- If the lookahead mode used is `PRELOAD`, then the layer property is changed, with the `lookaheadForLayer` property set to indicate the layer it is for. + +The resulting objects are appended to the timeline and included in the call to `onTimelineGenerate` and the [AB Playback](./ab-playback.md) resolving. + +## Advanced Scenarios + +Because the lookahead objects are included in the timeline to `onTimelineGenerate`, this gives you the ability to make changes to the lookahead output. + +[AB Playback](./ab-playback.md) started out as being implemented inside of `onTimelineGenerate` and relies on lookahead objects being produced before reassigning them to other mappings. + +If any objects found by lookahead have a class `_lookahead_start_delay`, they will be given a short delay in their start time. This is a hack introduced to workaround a timing issue. At some point this will be removed once a proper solution is found. + +Sometimes it can be useful to have keyframes which are only applied when in lookahead. That can be achieved by setting `preserveForLookahead`, making the keyframe be disabled, and then re-enabling it inside `onTimelineGenerate` at the correct time. + +It is possible to implement a 'next' AUX on your vision mixer by: + +- Setup this mapping with `lookaheadDepth: 1` and `lookahead: LookaheadMode.WHEN_CLEAR` +- Each Part creates a TimelineObject on this mapping. Crucially, these have a priority of 0. +- Lookahead will run and will insert its objects overriding your predefined ones (because of its higher priority). Resulting in the AUX always showing the lookahead object. diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/manipulating-ingest-data.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/manipulating-ingest-data.md new file mode 100644 index 00000000000..3b01e885cba --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/manipulating-ingest-data.md @@ -0,0 +1,139 @@ +# Manipulating Ingest Data + +In Sofie we receive the rundown from an NRCS in the form of the `IngestRundown`, `IngestSegment` and `IngestPart` types. ([Source Code](https://github.com/Sofie-Automation/sofie-core/blob/master/packages/shared-lib/src/peripheralDevice/ingest.ts)) +These are passed into the `getRundown` or `getSegment` blueprints methods to transform them into a Rundown that Sofie can display and play. + +At times it can be useful to manipulate this data before it gets passed into these methods. This wants to be done before `getSegment` in order to limit the scope of the re-generation needed. We could have made it so that `getSegment` is able to view the whole `IngestRundown`, but that would mean that any change to the `IngestRundown` would require re-generating every segment. This would be costly and could have side effects. + +A new method `processIngestData` was added to transform the `NRCSIngestRundown` into a `SofieIngestRundown`. The types of the two are the same, so implementing the `processIngestData` method is optional, with the default being to pass through the NRCS rundown unchanged. (There is an exception here for MOS, which is explained below). + +The basic implementation of this method which simply propagates nrcs changes is: + +```ts +function processIngestData( + context: IProcessIngestDataContext, + mutableIngestRundown: MutableIngestRundown, + nrcsIngestRundown: IngestRundown, + previousNrcsIngestRundown: IngestRundown | undefined, + changes: NrcsIngestChangeDetails | UserOperationChange +) { + if (changes.source === 'ingest') { + blueprintContext.defaultApplyIngestChanges(mutableIngestRundown, nrcsIngestRundown, changes) + } +} +``` + +In this method, the key part is the `mutableIngestRundown` which is the `IngestRundown` that will get used for `getRundown` and `getSegment` later. It is a class with various mutator methods which allows Sofie to cheaply check what has changed and know what needs to be regenerated. (We did consider performing deep diffs, but were concerned about the cost of diffing these very large rundown objects). +This object internally contains an `IngestRundown`. + +The `nrcsIngestRundown` parameter is the full `IngestRundown` as seen by the NRCS. The `previousNrcsIngestRundown` parameter is the `nrcsIngestRundown` from the previous call. This is to allow you to perform any comparisons between the data that may be useful. + +The `changes` object is a structure that defines what the NRCS provided changes for. The changes have already been applied onto the `nrcsIngestRundown`, this provides a description of what/where the changes were applied to. + +Finally, the `blueprintContext.defaultApplyIngestChanges` call is what performs the 'magic'. Inside of this it is interpreting the `changes` object, and calling the appropriate methods on `mutableIngestRundown`. It is expected that this logic should be able to handle most use cases, but there may be some where they need something custom, so it is completely possible to reimplement inside blueprints. + +So far this has ignored that the `changes` object can be of type `UserOperationChange`; this is explained below. + +## Modifying NRCS Ingest Data + +MOS does not have Segments, to handle this Sofie creates a Segment and Part for each MOS Story, expecting them to be grouped later if needed. + +In the past Sofie has had a hardcoded grouping logic, based on how NRK define this as a prefix in the Part names. Obviously this doesn't work for everyone, so this needed to be made more customisable. (This is still the default behaviour when `processIngestData` is not implemented) + +To perform the NRK grouping behaviour the following implementation can be used: + +```ts +function processIngestData( + context: IProcessIngestDataContext, + mutableIngestRundown: MutableIngestRundown, + nrcsIngestRundown: IngestRundown, + previousNrcsIngestRundown: IngestRundown | undefined, + changes: NrcsIngestChangeDetails | UserOperationChange +) { + if (changes.source === 'ingest') { + // Group parts by interpreting the slug to be in the form `SEGMENTNAME;PARTNAME` + const groupedResult = context.groupMosPartsInRundownAndChangesWithSeparator( + nrcsIngestRundown, + previousNrcsIngestRundown, + ingestRundownChanges.changes, + ';' // Backwards compatibility + ) + + context.defaultApplyIngestChanges( + mutableIngestRundown, + groupedResult.nrcsIngestRundown, + groupedResult.ingestChanges + ) + } +} +``` + +There is also a helper method for doing your own logic: + +```ts +function processIngestData( + context: IProcessIngestDataContext, + mutableIngestRundown: MutableIngestRundown, + nrcsIngestRundown: IngestRundown, + previousNrcsIngestRundown: IngestRundown | undefined, + changes: NrcsIngestChangeDetails | UserOperationChange +) { + if (changes.source === 'ingest') { + // Group parts by some custom logic + const groupedResult = context.groupPartsInRundownAndChanges( + nrcsIngestRundown, + previousNrcsIngestRundown, + ingestRundownChanges.changes, + (segments) => { + // TODO - perform the grouping here + return segmentsAfterMyChanges + } + ) + + context.defaultApplyIngestChanges( + mutableIngestRundown, + groupedResult.nrcsIngestRundown, + groupedResult.ingestChanges + ) + } +} +``` + +Both of these return a modified `nrcsIngestRundown` with the changes applied, and a new `changes` object which is similarly updated to match the new layout. + +You can of course do any portions of this yourself if you desire. + +## User Edits + +In some cases, it can be beneficial to allow the user to perform some editing of the Rundown from within the Sofie UI. AdLibs and AdLib Actions can allow for some of this to be done in the current and next Part, but this is limited and doesn't persist when re-running the Part. + +The idea here is that the UI will be given some descriptors on operations it can perform, which will then make calls to `processIngestData` so that they can be applied to the IngestRundown. Doing it at this level allows things to persist and for decisions to be made by blueprints over how to merge the changes when an update for a Part is received from the NRCS. + +This page doesn't go into how to define the editor for the UI, just how to handle the operations. + +There are a few Sofie defined definitions of operations, but it is also expected that custom operations will be defined. You can check the Typescript types for the builtin operations that you might want to handle. + +For example, it could be possible for Segments to be locked, so that any NRCS changes for them are ignored. + +```ts +function processIngestData( + context: IProcessIngestDataContext, + mutableIngestRundown: MutableIngestRundown, + nrcsIngestRundown: IngestRundown, + previousNrcsIngestRundown: IngestRundown | undefined, + changes: NrcsIngestChangeDetails | UserOperationChange +) { + if (changes.source === 'ingest') { + for (const segment of mutableIngestRundown.segments) { + delete ingestRundownChanges.changes.segmentChanges[segment.externalId] + // TODO - does this need to revert nrcsIngestRundown too? + } + + blueprintContext.defaultApplyIngestChanges(mutableIngestRundown, nrcsIngestRundown, changes) + } else if (changes.source === 'user') { + if (changes.operation.id === 'lock-segment') { + mutableIngestRundown.getSegment(changes.operationTarget.segmentExternalId)?.setUserEditState('locked', true) + } + } +} +``` diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/part-and-piece-timings.mdx b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/part-and-piece-timings.mdx new file mode 100644 index 00000000000..8c2b6e8e694 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/part-and-piece-timings.mdx @@ -0,0 +1,141 @@ +import { PartTimingsDemo } from './_part-timings-demo' + +# Part and Piece Timings + +Parts and pieces are the core groups that form the timeline, and define start and end caps for the other timeline objects. + +When referring to the timeline in this page, we mean the built timeline objects that is sent to playout-gateway. +It is made of the previous PartInstance, the current PartInstance and sometimes the next PartInstance. + +### The properties + +These are stripped down interfaces, containing only the properties that are relevant for the timeline generation: + +```ts +export interface IBlueprintPart { + /** Should this item should progress to the next automatically */ + autoNext?: boolean + /** How much to overlap on when doing autonext */ + autoNextOverlap?: number + + /** Timings for the inTransition, when supported and allowed */ + inTransition?: IBlueprintPartInTransition + + /** Should we block the inTransition when starting the next Part */ + disableNextInTransition?: boolean + + /** Timings for the outTransition, when supported and allowed */ + outTransition?: IBlueprintPartOutTransition + + /** Expected duration of the line, in milliseconds */ + expectedDuration?: number +} + +/** Timings for the inTransition, when supported and allowed */ +export interface IBlueprintPartInTransition { + /** Duration this transition block a take for. After this time, another take is allowed which may cut this transition off early */ + blockTakeDuration: number + /** Duration the previous part be kept playing once the transition is started. Typically the duration of it remaining in-vision */ + previousPartKeepaliveDuration: number + /** Duration the pieces of the part should be delayed for once the transition starts. Typically the duration until the new part is in-vision */ + partContentDelayDuration: number +} + +/** Timings for the outTransition, when supported and allowed */ +export interface IBlueprintPartOutTransition { + /** How long to keep this part alive after taken out */ + duration: number +} + +export interface IBlueprintPiece { + /** Timeline enabler. When the piece should be active on the timeline. */ + enable: { + start: number | 'now' // 'now' is only valid from adlib-actions when inserting into the current part + duration?: number + } + + /** Whether this piece is a special piece */ + pieceType: IBlueprintPieceType + + /// from IBlueprintPieceGeneric: + + /** Whether and how the piece is infinite */ + lifespan: PieceLifespan + + /** + * How long this piece needs to prepare its content before it will have an effect on the output. + * This allows for flows such as starting a clip playing, then cutting to it after some ms once the player is outputting frames. + */ + prerollDuration?: number +} + +/** Special types of pieces. Some are not always used in all circumstances */ +export enum IBlueprintPieceType { + Normal = 'normal', + InTransition = 'in-transition', + OutTransition = 'out-transition', +} +``` + +### Concepts + +#### Piece Preroll + +Often, a Piece will need some time to do some preparation steps on a device before it should be considered as active. A common example is playing a video, as it often takes the player a couple of frames before the first frame is output to SDI. +This can be done with the `prerollDuration` property on the Piece. A general rule to follow is that it should not have any visible or audible effect on the output until `prerollDuration` has elapsed into the piece. + +When the timeline is built, the Pieces get their start times adjusted to allow for every Piece in the part to have its preroll time. If you look at the auto-generated pieceGroup timeline objects, their times will rarely match the times specified by the blueprints. Additionally, the previous Part will overlap into the Part long enough for the preroll to complete. + +Try the interactive to see how the prerollDuration properties interact. + +#### In Transition + +The in transition is a special Piece that can be played when taking into a Part. It is represented as a Piece, partly to show the user the transition type and duration, and partly to allow for timeline changes to be applied when the timeline generation thinks appropriate. + +When the `inTransition` is set on a Part, it will be applied when taking into that Part. During this time, any Pieces with `pieceType: IBlueprintPieceType.InTransition` will be added to the timeline, and the `IBlueprintPieceType.Normal` Pieces in the Part will be delayed based on the numbers from `inTransition` + +Try the interactive to see how the an inTransition affects the Piece and Part layout. + +#### Out Transition + +The out transition is a special Piece that gets played when taking out of the Part. It is intended to allow for some 'visual cleanup' before the take occurs. + +In effect, when `outTransition` is set on a Part, the take out of the Part will be delayed by the duration defined. During this time, any pieces with `pieceType: IBlueprintPieceType.OutTransition` will be added to the timeline and will run until the end of the Part. + +Try the interactive to see how this affects the Parts. + +### Piece postroll + +Sometimes rather than extending all the pieces and playing an out transition piece on top we want all pieces to stop except for 1, this has the same goal of 'visual cleanup' as the out transition but works slightly different. The main concept is that an out transition delays the take slightly but with postroll the take executes normally however the pieces with postroll will keep playing for a bit after the take. + +When the `postrollDuration` is set on a piece the part group will be extended slightly allowing pieces to play a little longer, however any piece that do not have postroll will end at their regular time. + +#### Autonext + +Autonext is a way for a Part to be made a fixed length. After playing for its `expectedDuration`, core will automatically perform a take into the next part. This is commonly used for fullscreen videos, to exit back to a camera before the video freezes on the last frame. It is enabled by setting the `autoNext: true` on a Part, and requires `expectedDuration` to be set to a duration higher than `1000`. + +In other situations, it can be desirable for a Part to overlap the next one for a few seconds. This is common for Parts such as a title sequence or bumpers, where the sequence ends with an keyer effect which should reveal the next Part. +To achieve this you can set `autoNextOverlap: 1000 // ms` to make the parts overlap on the timeline. In doing so, the in transition for the next Part will be ignored. + +The `autoNextOverlap` property can be thought of an override for the intransition on the next part defined as: + +```ts +const inTransition = { + blockTakeDuration: 1000, + partContentDelayDuration: 0, + previousPartKeepaliveDuration: 1000, +} +``` + +#### Infinites + +Pieces with an infinite lifespan (ie, not `lifespan: PieceLifespan.WithinPart`) get handled differently to other pieces. + +Only one pieceGroup is created for an infinite Piece which is present in multiple of the current, next and previous Parts. +The Piece calculates and tracks its own started playback times, which is preserved and reused in future takes. On the timeline it lives outside of the partGroups, but still gets the same caps applied when appropriate. + +### Interactive timings demo + +Use the sliders below to see how various Preroll and In & Out Transition timing properties interact with each other. + + diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/sync-ingest-changes.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/sync-ingest-changes.md new file mode 100644 index 00000000000..7c609400d29 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/sync-ingest-changes.md @@ -0,0 +1,23 @@ +--- +title: Sync Ingest Changes +--- + +Since PartInstances and PieceInstances were added to Sofie, the default behaviour in Sofie is to not propagate any ingest changes from a Part onto its PartInstances. + +This is a safety net as without a detailed understanding of the Part and the change, we can't know whether it is safe to make on air. Without this, it would be possible for the user to change a clip name in the NRCS, and for Sofie to happily propagate that could result in a sudden change of clip mid sentence, or black if the clip needed to be copied to the playout server. This gets even more complicated when we consider that an adlib-action could have already modified a PartInstance, with changes that should likely not be overwritten with the newly ingested Part. + +Instead, this propagation can be implemented by a ShowStyle blueprint in the `syncIngestUpdateToPartInstance` method, in this way the implementation can be tailored to understand the change and its potential impact. This method is able to update the previous, current and next PartInstances. Any PartInstances older than the previous is no longer being used on the timeline so is now simply a record of how it was played and updating it would have no benefit. Sofie never has any further than the next PartInstance generated, so for any Part after that the Part is all that exists for it, so any changes will be used when it becomes the next. + +In this blueprint method, you are able to update almost any of the properties that are available to you both during ingest, and during adlib actions. It is possible the leave the Part in a broken state after this, so care must be taken to ensure it is not. If the call to your method throws an uncaught error, the changes you have made so far will be discarded but the rest of the ingest operation will continue as normal. + +### Tips + +- You should make use of the `metaData` fields on each Part and Piece to help work out what has changed. At NRK, we store the parsed ingest data (after converting the MOS to an intermediary json format) for the Part here, so that we can do a detailed diff to figure out whether a change is safe to accept. + +- You should track in `metaData` whether a part has been modified by an adlib-action in a way that makes this sync unsafe. + +- At NRK, we differentiate the Pieces into `primary`, `secondary`, `adlib`. This allows us to control the updates more granularly. + +- `newData.part` will be `undefined` when the PartInstance is orphaned. Generally, it's useful to differentiate the behavior of the implementation of this function based on `existingPartInstance.partInstance.orphaned` state + +- `playStatus: previous` means that the currentPartInstance is `orphaned: adlib-part` and thus possibly depends on an already past PartInstance for some of it's properties. Therefore the blueprint is allowed to modify the most recently played non-adlibbed PartInstance using ingested data. diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/timeline-datastore.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/timeline-datastore.md new file mode 100644 index 00000000000..ae18c75c05f --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/timeline-datastore.md @@ -0,0 +1,85 @@ +# Timeline Datastore + +The timeline datastore is a key-value store that can be used in conjunction with the timeline. The benefit of modifying values in the datastore is that the timings in the timeline are not modified so we can skip a lot of complicated calculations which reduces the system response time. An example usecase of the datastore feature is a fastpath for cutting cameras. + +## API + +In order to use the timeline datastore feature 2 API's are to be used. The timeline object has to contain a reference to a key in the datastore and the blueprints have to add a value for that key to the datastore. These references are added on the content field. + +### Timeline API + +```ts +/** + * An object containing references to the datastore + */ +export interface TimelineDatastoreReferences { + /** + * localPath is the path to the property in the content object to override + */ + [localPath: string]: { + /** Reference to the Datastore key where to fetch the value */ + datastoreKey: string + /** + * If true, the referenced value in the Datastore is only applied after the timeline-object has started (ie a later-started timeline-object will not be affected) + */ + overwrite: boolean + } +} +``` + +### Timeline API example + +```ts +const tlObj = { + id: 'obj0', + enable: { start: 1000 }, + layer: 'layer0', + content: { + deviceType: DeviceType.Atem, + type: TimelineObjectAtem.MixEffect, + + $references: { + 'me.input': { + datastoreKey: 'camInput', + overwrite: true, + }, + }, + + me: { + input: 1, + transition: TransitionType.Cut, + }, + }, +} +``` + +### Blueprints API + +Values can be added and removed from the datastore through the adlib actions API. + +```ts +interface DatastoreActionExecutionContext { + setTimelineDatastoreValue(key: string, value: unknown, mode: DatastorePersistenceMode): Promise + removeTimelineDatastoreValue(key: string): Promise +} + +enum DatastorePersistenceMode { + Temporary = 'temporary', + indefinite = 'indefinite', +} +``` + +The data persistence mode work as follows: + +- Temporary: this key-value pair may be cleaned up if it is no longer referenced to from the timeline, in practice this will currently only happen during deactivation of a rundown +- This key-value pair may _not_ be automatically removed (it can still be removed by the blueprints) + +The above context methods may be used from the usual adlib actions context but there is also a special path where none of the usual cached data is available, as loading the caches may take some time. The `executeDataStoreAction` method is executed just before the `executeAction` method. + +## Example use case: camera cutting fast path + +Assuming a set of blueprints where we can cut camera's a on a vision mixer's mix effect by using adlib pieces, we want to add a fast path where the camera input is changed through the datastore first and then afterwards we add the piece for correctness. + +1. If you haven't yet, convert the current camera adlibs to adlib actions by exporting the `IBlueprintActionManifest` as part of your `getRundown` implementation and implementing an adlib action in your `executeAction` handler that adds your camera piece. +2. Modify any camera pieces (including the one from your adlib action) to contain a reference to the datastore (See the timeline API example) +3. Implement an `executeDataStoreAction` handler as part of your blueprints, when this handler receives the action for your camera adlib it should call the `setTimelineDatastoreValue` method with the key you used in the timeline object (In the example it's `camInput`), the new input for the vision mixer and the `DatastorePersistenceMode.Temporary` persistence mode. diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/intro.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/intro.md new file mode 100644 index 00000000000..6b5caa33caa --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/intro.md @@ -0,0 +1,15 @@ +--- +sidebar_label: Introduction +sidebar_position: 1 +--- + +# For Developers + +The pages below are intended for developers of any of the Sofie-related repos and/or blueprints. + +A read-through of the [Concepts & Architectures](../user-guide/concepts-and-architecture.md) is recommended, before diving too deep into development. + +- [Libraries](libraries.md) +- [Contribution Guidelines](contribution-guidelines.md) +- [For Blueprint Developers](for-blueprint-developers/intro.md) +- [API Documentation](api-documentation.md) diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/json-config-schema.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/json-config-schema.md new file mode 100644 index 00000000000..6567cbc6761 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/json-config-schema.md @@ -0,0 +1,218 @@ +--- +sidebar_label: JSON Config Schema +sidebar_position: 7 +--- + +# JSON Config Schema + +So that Sofie does not have to be aware of every type of gateway that may connect to it, each gateway provides a manifest describing itself and the configuration fields that it has. + +Since version 1.50, this is done using [JSON Schemas](https://json-schema.org/). This allows schemas to be written, with typescript interfaces generated from the schema, and for the same schema to be used to render a flexible UI. +We recommend using [json-schema-to-typescript](https://github.com/bcherny/json-schema-to-typescript) to generate typescript interfaces. + +Only a subset of the JSON Schema specification is supported, and some additional properties are used for the UI. + +We expect this subset to grow over time as more sections are found to be useful to us, but we may proceed cautiously to avoid constantly breaking other applications that use TSR and these schemas. + +## Non-standard properties + +We use some non-standard properties to help the UI render with friendly names. + +### `ui:category` + +Note: Only valid for blueprint configuration. + +Category of the property + +### `ui:title` + +Title of the property + +### `ui:description` + +Description/hint for the property + +### `ui:summaryTitle` + +If set, when in a table this property will be used as part of the summary with this label + +### `ui:zeroBased` + +If an integer property, whether to treat it as zero-based + +### `ui:displayType` + +Override the presentation with a special mode. + +Currently only valid for: + +- object properties. Valid values are 'json'. +- string properties. Valid values are 'base64-image'. +- boolean properties. Valid values are 'switch'. + +### `tsEnumNames` + +This is primarily for `json-schema-to-typescript`. + +Names of the enum values as generated for the typescript enum, which we display in the UI instead of the raw values + +### `ui:sofie-enum` & `ui:sofie-enum:filter` + +Note: Only valid for blueprint configuration. + +Sometimes it can be useful to reference other values. This property can be used on string fields, to let Sofie generate a dropdown populated with values valid in the current context. + +#### `mappings` + +Valid for both show-style and studio blueprint configuration + +This will provide a dropdown of all mappings in the studio, or studios where the show-style can be used. + +Setting `ui:sofie-enum:filter` to an array of strings will filter the dropdown by the specified DeviceType. + +#### `source-layers` + +Valid for only show-style blueprint configuration. + +This will provide a dropdown of all source-layers in the show-style. + +Setting `ui:sofie-enum:filter` to an array of numbers will filter the dropdown by the specified SourceLayerType. + +### `ui:import-export` + +Valid only for tables, this allows for importing and exporting the contents of the table. + +## Supported types + +Any JSON Schema property or type is allowed, but will be ignored if it is not supported. + +In general, if a `default` is provided, we will use that as a placeholder in the input field. + +### `object` + +This should be used as the root of your schema, and can be used anywhere inside it. The properties inside any object will be shown if they are supported. + +You may want to set the `title` property to generate a typescript interface for it. + +See the examples to see how to create a table for an object. + +`ui:displayType` can be set to `json` to allow for manual editing of an arbitrary json object. + +### `integer` + +`enum` can be set with an array of values to turn it into a dropdown. + +### `number` + +### `boolean` + +### `string` + +`enum` can be set with an array of values to turn it into a dropdown. + +`ui:sofie-enum` can be used to make a special dropdown. + +### `array` + +The behaviour of this depends on the type of the `items`. + +#### `string` + +`enum` can be set with an array of values to turn it into a dropdown + +`ui:sofie-enum` can be used to make a special dropdown. + +Otherwise is treated as a multi-line string, stored as an array of strings. + +#### `object` + +This is not available in all places we use this schema. For example, Mappings are unable to use this, but device configuration is. Additionally, using it inside of another object-array is not allowed. + +## Examples + +Below is an example of a simple schema for a gateway configuration. The subdevices are handled separately, with their own schema. + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/product.schema.json", + "title": "Mos Gateway Config", + "type": "object", + "properties": { + "mosId": { + "type": "string", + "ui:title": "MOS ID of Mos-Gateway (Sofie MOS ID)", + "ui:description": "MOS ID of the Sofie MOS device (ie our ID). Example: sofie.mos", + "default": "" + }, + "debugLogging": { + "type": "boolean", + "ui:title": "Activate Debug Logging", + "default": false + } + }, + "required": ["mosId"], + "additionalProperties": false +} +``` + +### Defining a table as an object + +In the generated typescript interface, this will produce a property `"TestTable": { [id: string]: TestConfig }`. + +The key part here, is that it is an object with no `properties` defined, and a single `patternProperties` value performing a catchall. + +An `object` table is better than an `array` in blueprint-configuration, as it allows the UI to override individual values, instead of the table as a whole. + +```json +"TestTable": { + "type": "object", + "ui:category": "Test", + "ui:title": "Test table", + "ui:description": "", + "patternProperties": { + "": { + "type": "object", + "title": "TestConfig", + "properties": { + "number": { + "type": "integer", + "ui:title": "Number", + "ui:description": "Camera number", + "ui:summaryTitle": "Number", + "default": 1, + "min": 0 + }, + "port": { + "type": "integer", + "ui:title": "Port", + "ui:description": "ATEM Port", + "default": 1, + "min": 0 + } + }, + "required": ["number", "port"], + "additionalProperties": false + } + }, + "additionalProperties": false +}, + +``` + +### Select multiple ATEM device mappings + +```json +"mappingId": { + "type": "array", + "ui:title": "Mapping", + "ui:description": "", + "ui:summaryTitle": "Mapping", + "items": { + "type": "string", + "ui:sofie-enum": "mappings", + "ui:sofie-enum:filter": ["ATEM"], + }, + "uniqueItems": true +}, +``` diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/libraries.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/libraries.md new file mode 100644 index 00000000000..943938848c3 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/libraries.md @@ -0,0 +1,56 @@ +--- +description: List of all repositories related to Sofie +sidebar_position: 5 +--- + +# Applications & Libraries + +## Main Application + +[**Sofie Core**](https://github.com/Sofie-Automation/sofie-core) is the main application that serves the web GUI and handles the core logic. + +## Gateways and Services + +Together with the _Sofie Core_ there are several _gateways_ which are separate applications, but which connect to _Sofie Core_ and are managed from within the Core's web UI. + +- [**Playout Gateway**](https://github.com/Sofie-Automation/sofie-core/tree/main/packages/playout-gateway) Handles the playout from _Sofie_. Connects to and controls a multitude of devices, such as vision mixers, graphics, light controllers, audio mixers etc.. +- [**MOS Gateway**](https://github.com/Sofie-Automation/sofie-core/tree/main/packages/mos-gateway) Connects _Sofie_ to a newsroom system \(NRCS\) and ingests rundowns via the [MOS protocol](http://mosprotocol.com/). +- [**Live Status Gateway**](https://github.com/Sofie-Automation/sofie-core/tree/main/packages/live-status-gateway) Allows external systems to subscribe to state changes in Sofie. +- [**iNEWS Gateway**](https://github.com/tv2/inews-ftp-gateway) Connects _Sofie_ to an Avid iNEWS newsroom system. +- [**Spreadsheet Gateway**](https://github.com/SuperFlyTV/spreadsheet-gateway) Connects _Sofie_ to a _Google Drive_ folder and ingests rundowns from _Google Sheets_. +- [**Input Gateway**](https://github.com/Sofie-Automation/sofie-input-gateway) Connects _Sofie_ to various input devices, allowing triggering _User-Actions_ using these devices. +- [**Package Manager**](https://github.com/Sofie-Automation/sofie-package-manager) Handles media asset transfer and media file management for pulling new files, deleting expired files on playout devices and generating additional metadata (previews, thumbnails, automated QA checks) in a more performant, and possibly distributed, way. Can smartly figure out how to get a file on storage A to playout server B. + +## Libraries + +There are a number of libraries used in the Sofie ecosystem: + +- [**ATEM Connection**](https://github.com/Sofie-Automation/sofie-atem-connection) Library for communicating with Blackmagic Design's ATEM mixers +- [**ATEM State**](https://github.com/Sofie-Automation/sofie-atem-state) Used in TSR to tracks the state of ATEMs and generate commands to control them. +- [**CasparCG Server Connection**](https://github.com/SuperFlyTV/casparcg-connection) developed by **[_SuperFly.tv_](https://github.com/SuperFlyTV)** Library to connect and interact with CasparCG Servers. +- [**CasparCG State**](https://github.com/superflytv/casparcg-state) developed by **[_SuperFly.tv_](https://github.com/SuperFlyTV)** Used in TSR to tracks the state of CasparCG Servers and generate commands to control them. +- [**Ember+ Connection**](https://github.com/Sofie-Automation/sofie-emberplus-connection) Library to communicate with _Ember+_ control protocol +- [**HyperDeck Connection**](https://github.com/Sofie-Automation/sofie-hyperdeck-connection) Library for connecting to Blackmagic Design's HyperDeck recorders. +- [**MOS Connection**](https://github.com/Sofie-Automation/sofie-mos-connection/) A [_MOS protocol_](http://mosprotocol.com/) library for acting as a MOS device and connecting to an newsroom control system. +- [**Quantel Gateway Client**](https://github.com/Sofie-Automation/sofie-quantel-gateway-client) An interface that talks to the Quantel-Gateway application. +- [**Sofie Core Integration**](https://github.com/Sofie-Automation/sofie-core-integration) Used to connect to the [Sofie Core](https://github.com/Sofie-Automation/sofie-core) by the Gateways. +- [**Sofie Blueprints Integration**](https://www.npmjs.com/package/@sofie-automation/blueprints-integration) Common types and interfaces used by both Sofie Core and the user-defined blueprints. +- [**SuperFly-Timeline**](https://github.com/SuperFlyTV/supertimeline) developed by **[_SuperFly.tv_](https://github.com/SuperFlyTV)** Resolver and rules for placing objects on a virtual timeline. +- [**ThreadedClass**](https://github.com/nytamin/threadedClass) developed by **[_Nytamin_](https://github.com/nytamin)** Used in TSR to spawn device controllers in separate processes. +- [**Timeline State Resolver**](https://github.com/Sofie-Automation/sofie-timeline-state-resolver) \(TSR\) The main driver in **Playout Gateway,** handles connections to playout-devices and sends commands based on a **Timeline** received from **Core**. + +There are also a few typings-only libraries that define interfaces between applications: + +- [**Blueprints Integration**](https://www.npmjs.com/package/@sofie-automation/blueprints-integration) Defines the interface between [**Blueprints**](../user-guide/concepts-and-architecture.md#blueprints) and **Sofie Core**. +- [**Timeline State Resolver types**](https://www.npmjs.com/package/timeline-state-resolver-types) Defines the interface between [**Blueprints**](../user-guide/concepts-and-architecture.md#blueprints) and the timeline that will be fed into **TSR** for playout. + +## Other Sofie-related Repositories + +- [**CasparCG Server** \(NRK fork\)](https://github.com/nrkno/sofie-casparcg-server) Sofie-specific fork of CasparCG Server. +- [**CasparCG Launcher**](https://github.com/Sofie-Automation/sofie-casparcg-launcher) Launcher, controller, and logger for CasparCG Server. +- [**CasparCG Media Scanner** \(NRK fork\)](https://github.com/nrkno/sofie-casparcg-server) Sofie-specific fork of CasparCG Server 2.2 Media Scanner. +- [**Sofie Chef**](https://github.com/Sofie-Automation/sofie-chef) A simple Chromium based renderer, used for kiosk mode rendering of web pages. +- [**Media Manager**](https://github.com/nrkno/sofie-media-management) _(deprecated)_ Handles media transfer and media file management for pulling new files and deleting expired files on playout devices. +- [**Quantel Browser Plugin**](https://github.com/Sofie-Automation/sofie-quantel-browser-plugin) MOS-compatible Quantel video clip browser for use with Sofie. +- [**Sisyfos Audio Controller**](https://github.com/nrkno/sofie-sisyfos-audio-controller) _developed by [*olzzon*](https://github.com/olzzon/)_ +- [**Quantel Gateway**](https://github.com/Sofie-Automation/sofie-quantel-gateway) CORBA to REST gateway for _Quantel/ISA_ playback. diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/mos-plugins.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/mos-plugins.md new file mode 100644 index 00000000000..1c414442719 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/mos-plugins.md @@ -0,0 +1,185 @@ +--- +title: MOS-plugins +sidebar_position: 20 +--- + +# iFrames MOS-plugins + +**The usage of MOS-plugins allow micro frontends to be injected into Sofie for the purpose of adding content to the production without turning away from the Sofie UI.** + +Example use cases can be browsing and playing clips straight from a video server, or the creation of lower third graphics without storing it in the NRCS. + +:::note MOS reference +[5.3 MOS Plug-in Communication messages](https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOSProtocolVersion40/index.html#calibre_link-61) + +The link points at MOS documentations for MOS 4 (for the benefit of having the best documentation), but will be compatible with most older versions too. +::: + +## Bucket items workflow + +MOS-plugins are managed through the Shelf-system. They are added as `external_frame` either as a Tab to a Rundown layout or as a Panel to a Dashboard layout. + +![Video browser MOS Plugin in Shelf tab](/img/docs/for-developers/shelf-bucket-items.jpg) +A video server browser plugin shown as a tab in the rundown layout shelf. + +The user can create one or more Buckets. From the plugin they can drag-and-drop content into the buckets. The user can manage the buckets and their content by creating, renaming, re-arranging and deleting. More details available at the [Bucket concept description.](/docs/user-guide/concepts-and-architecture#buckets) + +## Cross-origin drag-and-drop + +:::note Bucket workflow without drag-and-drop +The plugin iFrame can send a `postMessage` call with an `ncsItem` payload to programmatically create an ncsItem without the drag-and-drop interaction. This is a viable solution which avoids cross-origin drag-and-drop problems. +::: + +### The problem + +**Web browsers prevent drops into a webpage if the drag started from a page hosted on another origin.** + +This means that drag-and-drop must happen between pages from the same origin. This is relevant for MOS-plugins, as they are supposed to be displayed in iFrames. Specifically, this means that the plugin in the iFrame must be served from the same origin as the parent page (where the drop will happen). + +There are no properties or options to bypass this from within HTML/Javascript. Bypassing is theoretically possible by overriding the browser's security settings, but this is not recommended. + +:::note Background +The background for the policy is discussed in this Chromium Issue from 2010: [Security: do not allow on-page drag-and-drop from non-same-origin frames (or require an extra gesture)](https://issues.chromium.org/issues/40083787) +::: + +:::note What counts as different origins? +| Sofie Server Domain | Plugin Domain | Cross-origin or Same-origin? | +| ------------------- | ------------- | ---------------------------- | +| `https://mySofie.com:443` | `https://myPlugin.com:443` | cross-origin: different domains | +| | `https://www.mySofie.com:443` | cross-origin: different subdomains | +| | `https://myPlugin.mySofie.com:443` | cross-origin: different subdomains | +| | `http://mySofie.com:443` | cross-origin: different schemes | +| | `https://mySofie.com:80` | cross-origin: different ports | +| | `https://mySofie.com:443/myPlugin` | same-origin: domain, scheme and port match | +| | `https://mySofie.com/myPlugin` | same-origin: domain, scheme and port match (https implies port 443) | + +::: + +#### The "proxy idea" + +As you can tell from the table, you need to exactly match both the protocol, domain and port number. More importantly, different subdomains trigger the cross-origin policy. + +_The proxy idea_ is to use rewrite-rules in a proxy server (e.g. NGINX) to serve the plugin from a path on the Sofie server's domain. As this can't be done as subdomains, that leaves the option of having a folder underneath the top level of the Sofie server's domain. + +An example of this would be to serve Sofie at `https://mysofie.com` and then host the plugin (directly or via a proxy) at `https://mysofie.com/myplugin`. Technically this will work, but this solution is fragile. All links within the plugin will have to be either absolute or truly relative links that take the URL structure into account. This is doable if the plugin is being developed with this in mind. But it leads to a fragile tight coupling between the plugin and the host application (Sofie) which can break with any inconsiderate update in the future. + +:::note Example of linking from a (potentially proxied) subfolder +**Case:** `https://mysofie.com/myplugin/index.html` wants to access `https://mysofie.com/myplugin/static/images/logo.png`. + +Normally the plugin would be developed and bundled to work standalone, resulting in a link relative to its own base path, giving `/static/images/logo.png` which here wrongly resolves to `https://mysofie.com/static/images/logo.png`. + +The plugin would need to use either use the absolute `https://mysofie.com/myplugin/static/images/logo.png` or the relative `images/static/logo.png` or `./images/static/logo.png` or even `/myplugin/static/images/logo.png` to point to the right resource. +::: + +### The solution + +**Sofie proposes a drag-and-drop/postMessage hybrid interface.** +In this model the user interactions of drag-and-drop are targeting a dedicated Drop page served by the plugin-server (same-origin to the plugin). This can be transparently overlaid the real drop region and intercept drop events. The Bucket system has built-in support for this, configured as an additional property to the External frame panel setup in Shelf config. + +![Configuration of External frame with dedicated drop-page](/img/docs/for-developers/shelf-external_frame-config.png) + +The true communication channel between the plugin and Sofie becomes a postMessage protocol where the plugin is managing all drag-and-drop events and converts them into the postMessage protocol. Sofie also handles edge cases such as timeouts, drag leaving the browser etc. + +### Sequence diagram + +#### Post-messages from the Plugin (drag-side) + +| Message | Payload | Description | +| --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| dragStart | - | Re-sends the DOM event dragStart as a postMessage of the same kind.
This is the signal to Sofie to toggle on the Drop-zone and indicate in the UI that a drag is happening. | +| dragEnd | - | Re-sends the DOM event dragEnd as a postMessage of the same kind.
This is the signal to Sofie to toggle off the Drop-zone and reset the UI. | + +#### Post-messages from the Plugin Drop-page + +| Message | Payload | Description | +| --------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| dragEnter | `{event: 'dragEnter', label: string}` | To set the UI to reflect an object is being dragged into a specific bucket.
The label property can be used for showing a simple placeholder in the bucket. | +| dragLeave | `{event: 'dragLeave'}` | To reset any UI. | +| drop | `{event: 'drop'}` | To synchronously react to the drop in the UI. | +| data | `{event: 'data', data: ncsItem}` | To (a)synchronously receive the payload.
The expected format is an `ncsItem` MOS message (XML string) | +| error | `{event: 'error', message}` | To cancel the drag-operation and handle any errors. | + +:::note Please note +Please note how all interactions are happening over the postMessage interface. +No DOM-driven drag-n-drop events are relevant for Sofie, as they are solely handled between the plugin and its drop-page. +::: + +```mermaid +sequenceDiagram +autonumber + +actor user as User + +participant plugin as Plugin
Frontend +participant shelf as Sofie Shelf Component +participant bucket as Sofie Bucket Component +participant drop as Plugin
Drop-page + +user->>plugin: Starts dragging from Plugin +plugin->>shelf: postMessage dragStartEvent +shelf--)shelf: 10 000ms timeout to trigger a dragEndEvent
if the drag doesn't cancel or successfully drop before that. +shelf->>shelf: Filter for valid Drop Zones
based on the optional properties of the dragStartEvent +shelf->>bucket: Sofie React event dragStartEvent +bucket->>drop: Shows iFrame Drop Zone + + + +user->>drop: Drags into the area of a Drop Zone (DOM dragEnter event) +note right of drop: Read payload to provide a title
in the dragEnterEvent +drop->>drop: e.dataTransfer.getData('text/plain'); +drop->>bucket: postmessage object dragEnterEvent + +loop dragOver events + user-)drop: Drag moves over drop target (DOM dragover event) + drop->>drop: (re)set timeout 100ms
to trigger faux dragLeave +end + +drop--)drop: dragLeave timeout expires +drop->>bucket: postmessage object dragEnterEvent (faux) + + +user->>drop: Drags out of a Drop Zone, or dragOver timeout (DOM dragLeave event) +drop->>drop: cancel dragOver timeout +drop->>bucket: postmessage object dragLeaveEvent + + + +Note over user,drop: Unknown order of events. Handle both outcomes of the race. +par Successful drop or Cancelled drag + user->>plugin: Successful drop
or Cancel drag on ESC
or drop outside of Drop region
(DOM dragEnd event) + plugin->>shelf: postMessage dragEndEvent + shelf->>shelf: Clear the drop-/cancel-timeout. + shelf->>bucket: Sofie React event dragEndEvent + bucket->>drop: Hides iFrame Drop Zone +and Drops in bucket + user->>drop: Drop (DOM drop event) + drop->>bucket: dropEvent + bucket--)bucket: Set timeout to trigger an user-facing error
if the data doesn't return in time. + bucket->>bucket: Set loader UI + + drop->>drop: e.dataTransfer.getData('text/plain'); + + + alt Success + drop--)bucket: postmessage object dataEvent + bucket->>bucket: Clear loader UI/Set success UI + else Error + drop--)bucket: postmessage object errorEvent + bucket->>bucket: Clear loader UI + bucket--)user: Error message + else Timeout + bucket->>bucket: Clear loader UI + bucket--)user: Error message + end +end + +``` + +#### Minimal example sequence - happy path + +Don't worry, the sequence diagram shows a lot more detail than you need to think about. Consider this simple happy-path sequence as a representative interaction between the 3 actors (Plugin, Drop-page and Sofie): + +1. Plugin `dragStart` +2. Drop-page `dragEnter` +3. Plugin `dragEnd` and Drop-page `drop` +4. Drop-page `data` diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/npm-package-publishing.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/npm-package-publishing.md new file mode 100644 index 00000000000..079ca9c8fa9 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/npm-package-publishing.md @@ -0,0 +1,23 @@ +--- +title: NPM Package Publishing +sidebar_position: 999 +--- + +While many parts of Sofie reside in the main `sofie-core` mono-repo, there are a few NPM libraries in that repo which want to be published to NPM to allow being consumed elsewhere. + +Many features and PRs will need to make changes to these libraries, which means that you will often need to publish testing versions that you can use before your PR is merged, or when you need to publish your own Sofie releases to backport that feature onto an older release. + +To make this easy, the Github actions workflows have been structured so that you can utilise them with minimal effort for publishing to your own npm organization. +The `Publish libraries` workflow is the single workflow used to perform this publishing, for both stable and prerelease versions. You can manually trigger this workflow at any time in the Github UI or via CLI tools to trigger a prerelease build of the libraries. + +When running in your fork, this workflow will only run if the `NPM_PACKAGE_PREFIX` variable has been defined (Note: this is a variable not a secret). + +Recommended repository variables/secrets + +- `NPM_PACKAGE_PREFIX` — repository variable; your npm organisation (required for forks to publish). +- `NPM_PACKAGE_SCOPE` — repository variable; optional, adds `sofie-` prefix to package names. +- `NPM_TOKEN` — repository secret; optional if using trusted publishing, otherwise required for the workflow to publish. + +For the publishing, we recommend enabling [trusted publishing](https://docs.npmjs.com/trusted-publishers), but in case you are unable to do this (or to allow for the first publish), if you provide a `NPM_TOKEN` secret, that will be used for the publishing instead. + +The [`timeline-state-resolver`](https://github.com/Sofie-Automation/sofie-timeline-state-resolver) repository has been setup in the same way, as this is another library that you will often need to publish your own versions for. diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/publications.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/publications.md new file mode 100644 index 00000000000..c9def838a26 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/publications.md @@ -0,0 +1,43 @@ +--- +title: Publications +sidebar_position: 12 +--- + +To ensure that the UI of Sofie is reactive, we are leveraging publications over the DDP connection that Meteor provides. +In its most basic form, this allows for streaming MongoDB document updates as they happen to the UI, and there is also a structure in place for 'Custom Publications' which appear like a MongoDB collection to the client, but are generated in-memory collections of data allowing us to do some processing of data before publishing it to the client. + +It is possible to subscribe to these publications outside of Meteor, but we have not found any maintained ddp clients, except for the one we are using in `server-core-integration`. The protocol is simple and stable and has documentation on the [Meteor GitHub](https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md), and should be easy to implement in another language if desired. + +All of the publication implementations reside in [`meteor/server/publications` folder](https://github.com/Sofie-Automation/sofie-core/tree/main/meteor/server/publications), and are typically pretty well isolated from the rest of the code we have in Meteor. + +We prefer using publications in Sofie over polling because: + +- there are not enough DDP clients to a single Sofie installation for the number of connected clients to be problematic +- polling can be costly for many of these publications without some form of caching or tracking changes (which starts to get to a similar level of complexity) +- we can be more confident that all the clients have the same data as the database is our point of truth +- the system can be more reactive as changes are pushed to interested parties with minimal intervention + +## MongoDB Publications + +A majority of data is sent to the client utilising Meteor's ability to publish a MongoDB cursor. This allows us to run a MongoDB query on the backend, and let it handle the publishing of individual changes. + +In some (typically older) publications, we let the client specify the MongoDB query to use for the subscription, where we perform some basic validation and authentication before executing the query. + +In typically newer publications, we are formalising the publications a bit better by requiring some simpler parameters to the publication, with the query then generated on the backend. This will help us ensure that the queries are made with suitable indices, and to ensure that subscriptions are deduplicated where possible. + +## Custom Publications + +There has been a recent push towards using more 'custom' publications for streaming data to the UI. While we are unsure if this will be beneficial for every publication, it is really beneficial for others as it allows us to do some pre-computation of data before sending it to the client. + +To achieve this, we have an `optimisedObserver` flow which is designed to help maange to a custom publication, with a few methods to fill in to setup the reactivity and the data transformation. + +One such publication is the `PieceContentStatus`, prior to version 1.50, this was computed inside the UI. +A brief overview of this publication, is that it looks at each Piece in a Rundown, and reports whether the Piece is 'OK'. This check is primarily focussed on Pieces containing clips, where it will check the metadata generated by Package Manager to ensure that the clip is marked as being ready for playout, and that it has the correct format and some other quality checks. + +To do this on the client meant needing to subscribe to the whole contents of a couple of MongoDB collections, as it is not easy to determine which documents will be needed until the check is being run. This caused some issues as these collections could get rather large. We also did not always have every Piece loaded in the UI, so had to defer some of the computation to the backend via polling. + +This makes it more suitable for a custom publication, where we can more easily and cheaply do this computation without being concerned about causing UI lockups and with less concern about memory pressure. Performing very granular MongoDB queries is also cheaper. The result is that we build a graph of what other documents are used for the status of each Piece, so we can cheaply react to changes to any of those documents, while also watching for changes to the pieces. + +## Live Status Gateway + +The Live Status Gateway was introduced to Sofie in version 1.50. This gateway serves as a way for an external system to subscribe to publications which are designed to be simpler than the ones we publish over DDP. These publications are intended to be used by external systems which need a 'stable' API and to not have too much knowledge about the inner workings of Sofie. See [Api Stability](./api-stability.md) for more details. diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/url-query-parameters.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/url-query-parameters.md new file mode 100644 index 00000000000..e2b0fbcb755 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/url-query-parameters.md @@ -0,0 +1,25 @@ +--- +sidebar_label: URL Query Parameters +sidebar_position: 10 +--- + +# URL Query Parameters +Appending query parameter(s) to the URL will allow you to modify the behaviour of the GUI, as well as control the [Access Levels](../user-guide/features/access-levels.md). + +| Query Parameter | Description | +| :---------------------------------- | :------------------------------------------------------------------------ | +| `admin=1` | Gives the GUI the same access as the combination of [Configuration Mode](../user-guide/features/access-levels.md#Configuration-Mode) and [Studio Mode](../user-guide/features/access-levels.md#Studio-Mode) as well as having access to a set of [Testing Mode](../user-guide/features/access-levels.md#Testing-Mode) tools and a Manual Control section on the Rundown page. _Default value is `0`._ | +| `studio=1` | [Studio Mode](../user-guide/features/access-levels.md#Studio-Mode) gives the GUI full control of the studio and all information associated to it. This includes allowing actions like activating and deactivating rundowns, taking parts, adlibbing, etcetera. _Default value is `0`._ | +| `buckets=0,1,...` | The buckets can be specified as base-0 indices of the buckets as seen by the user. This means that `?buckets=1` will display the second bucket as seen by the user when not filtering the buckets. This allows the user to decide which bucket is displayed on a secondary attached screen simply by reordering the buckets on their main view. | +| `develop=1` | Enables the browser's default right-click menu to appear. It will also reveal the _Manual Control_ section on the Rundown page. _Default value is `0`._ | +| `display=layout,buckets,inspector` | A comma-separated list of features to be displayed in the shelf. Available values are: `layout` \(for displaying the Rundown Layout\), `buckets` \(for displaying the Buckets\) and `inspector` \(for displaying the Inspector\). | +| `help=1` | Enables some tooltips that might be useful to new users. _Default value is `0`._ | +| `ignore_piece_content_status=1` | Removes the "zebra" marking on VT pieces that have a "missing" status. _Default value is `0`._ | +| `reportNotificationsId=anyId,...` | Sets an ID for an individual client GUI system, to be used for reporting Notifications shown to the user. The Notifications' contents, tagged with this ID, will be sent back to the Sofie Core's log. _Default value is `0`, which disables the feature._ | +| `shelffollowsonair=1` | _Default value is `0`._ | +| `show_hidden_source_layers=1` | _Default value is `0`._ | +| `speak=1` | Experimental feature that starts playing an audible countdown 10 seconds before each planned _Take_. _Default value is `0`._ | +| `vibrate=1` | Experimental feature that triggers the vibration API in the web browser 3 seconds before each planned _Take_. _Default value is `0`._ | +| `zoom=1,...` | Sets the scaling of the entire GUI. _The unit is a percentage where `100` is the default scaling._ | + + diff --git a/packages/documentation/versioned_docs/version-1.52.0/for-developers/worker-threads-and-locks.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/worker-threads-and-locks.md new file mode 100644 index 00000000000..8018a060822 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/for-developers/worker-threads-and-locks.md @@ -0,0 +1,61 @@ +--- +title: Worker Threads & Locks +sidebar_position: 9 +--- + +Starting with v1.40.0 (Release 40), the core logic of Sofie is split across +multiple threads. This has been done to minimise performance bottlenecks such as ingest changes delaying takes. In its +current state, it should not impact deployment of Sofie. + +In the initial implementation, these threads are run through [threadedClass](https://github.com/nytamin/threadedclass) +inside of Meteor. As Meteor does not support the use of `worker_threads`, and to allow for future separation, the +`worker_threads` are treated and implemented as if they are outside of the Meteor ecosystem. The code is isolated from +Meteor inside of `packages/job-worker`, with some shared code placed in `packages/corelib`. + +Prior to v1.40.0, there was already a work-queue of sorts in Meteor. As such the functions were defined pretty well to +translate across to being on a true work queue. For now this work queue is still in-memory in the Meteor process, but we +intend to investigate relocating this in a future release. This will be necessary as part of a larger task of allowing +us to scale Meteor for better resiliency. Many parts of the worker system have been designed with this in mind, and so +have sufficient abstraction in place already. + +### The Worker + +The worker process is designed to run the work for one or more studios. The initial implementation will run for all +studios in the database, and is monitoring for studios to be added or removed. + +For each studio, the worker runs 3 threads: + +1. The Studio/Playout thread. This is where all the playout operations are executed, as well as other operations that + require 'ownership' of the Studio +2. The Ingest thread. This is where all the MOS/Ingest updates are handled and fed through the bluerpints. +3. The events thread. Some low-priority tasks are pushed to here. Such as notifying ENPS about _the yellow line_, or the + Blueprints methods used to generate External-Messages for As-Run Log. + +In future it is expected that there will be multiple ingest threads. How the work will be split across them is yet to be +determined + +### Locks + +At times, the playout and ingest threads both need to take ownership of `RundownPlaylists` and `Rundowns`. + +To facilitate this, there are a couple of lock types in Sofie. These are coordinated by the parent thread in the worker +process. + +#### PlaylistLock + +This lock gives ownership of a specific `RundownPlaylist`. It is required to be able to load a `PlayoutModel`, and +must be held during other times where the `RundownPlaylist` is modified or is expected to not change. + +This lock must be held while writing any changes to either a `RundownPlaylist` or any `Rundown` that belong to the +`RundownPlaylist`. This ensures that any writes to MongoDB are atomic, and that Sofie doesn't start performing a +playout operation halfway through an ingest operation saving. + +#### RundownLock + +This lock gives ownership of a specific `Rundown`. It is required to be able to load a `IngestModel`, and must held +during other times where the `Rundown` is modified or is expected to not change. + +:::caution +It is not allowed to acquire a `RundownLock` while inside of a `PlaylistLock`. This is to avoid deadlocks, as it is very +common to acquire a `PlaylistLock` inside of a `RundownLock` +::: diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/concepts-and-architecture.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/concepts-and-architecture.md new file mode 100644 index 00000000000..ef9008f40ca --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/concepts-and-architecture.md @@ -0,0 +1,192 @@ +--- +sidebar_position: 1 +--- + +# Concepts & Architecture + +## System Architecture + +![Example of a Sofie setup with a Playout Gateway and a Spreadsheet Gateway](/img/docs/main/features/playout-and-spreadsheet-example.png) + +### Sofie Core + +**Sofie Core** is a web server which handle business logic and serves the web GUI. +It is a [NodeJS](https://nodejs.org/) process backed up by a [MongoDB](https://www.mongodb.com/) database and based on the framework [Meteor](http://meteor.com/). + +### Gateways + +Gateways are applications that connect to Sofie Core and and exchanges data; such as rundown data from an NRCS or the [Timeline](#timeline) for playout. + +An examples of a gateways is the [Spreadsheet Gateway](https://github.com/SuperFlyTV/spreadsheet-gateway). +All gateways use the [Core Integration Library](https://github.com/Sofie-Automation/sofie-core/tree/main/packages/server-core-integration) to communicate with Core. + +## System, \(Organization\), Studio & Show Style + +To be able to facilitate various workflows and to Here's a short explanation about the differences between the "System", "Organization", "Studio" and "Show Style". + +- The **System** defines the whole of the Sofie Core +- The **Organization** \(only available if user accounts are enabled\) defines things that are common for an organization. An organization consists of: **Users, Studios** and **ShowStyles**. +- The **Studio** contains things that are related to the "hardware" or "rig". Technically, a Studio is defined as an entity that can have one \(or none\) rundown active at any given time. In most cases, this will be a representation of your gallery, with cameras, video playback and graphics systems, external inputs, sound mixers, lighting controls and so on. A single System can easily control multiple Studios. +- The **Show Style** contains settings for the "show", for example if there's a "Morning Show" and an "Afternoon Show" - produced in the same gallery - they might be two different Show Styles \(played in the same Studio\). Most importantly, the Show Style decides the "look and feel" of the Show towards the producer/director, dictating how data ingested from the NRCS will be interpreted and how the user will interact with the system during playback (see: [Show Style](./configuration/settings-view#show-style) in Settings). + - A **Show Style Variant** is a set of Show Style _Blueprint_ configuration values, that allows to use the same interaction model across multiple Shows with potentially different assets, changing the outward look of the Show: for example news programs with different hosts produced from the same Studio, but with different light setups, backscreen and overlay graphics. + +![Sofie Architecture Venn Diagram](/img/docs/main/features/sofie-venn-diagram.png) + +## Playlists, Rundowns, Segments, Parts, Pieces + +![Playlists, Rundowns, Segments, Parts, Pieces](/img/docs/main/features/playlist-rundown-segment-part-piece.png) + +### Playlist + +A Playlist \(or "Rundown Playlist"\) is the entity that "goes on air" and controls the playhead/Take Point. + +It contains one or several Rundowns inside, which are playout out in order. + +:::info +In some many studios, there is only ever one rundown in a playlist. In those cases, we sometimes lazily refer to playlists and rundowns as "being the same thing". +::: + +A Playlist is played out in the context of it's [Studio](#studio), thereby only a single Playlist can be active at a time within each Studio. + +A playlist is normally played through and then ends but it is also possible to make looping playlists in which case the playlist will start over from the top after the last part has been played. + +### Rundown + +The Rundown contains the content for a show. It contains Segments and Parts, which can be selected by the user to be played out. +A Rundown always has a [showstyle](#showstyle) and is played out in the context of the [Studio](#studio) of its Playlist. + +### Segment + +The Segment is the horizontal line in the GUI. It is intended to be used as a "chapter" or "subject" in a rundown, where each individual playable element in the Segment is called a [Part](#part). + +### Part + +The Part is the playable element inside of a [Segment](#segment). This is the thing that starts playing when the user does a [TAKE](#take-point). A Playing part is _On Air_ or _current_, while the part "cued" to be played is _Next_. +The Part in itself doesn't determine what's going to happen, that's handled by the [Pieces](#piece) in it. + +### Piece + +The Pieces inside of a Part determines what's going to happen, the could be indicating things like VT's, cut to cameras, graphics, or what script the host is going to read. + +Inside of the pieces are the [timeline-objects](#what-is-the-timeline) which controls the playout on a technical level. + +:::tip +Tip! If you want to manually play a certain piece \(for example a graphics overlay\), you can at any time double-click it in the GUI, and it will be copied and played at your play head, just like an [AdLib](#adlib-pieces) would! +::: + +See also: [Showstyle](#system-organization-studio--show-style) + +### AdLib Piece + +The AdLib pieces are Pieces that isn't programmed to fire at a specific time, but instead intended to be manually triggered by the user. + +The AdLib pieces can either come from the currently playing Part, or it could be _global AdLibs_ that are available throughout the show. + +An AdLib isn't added to the Part in the GUI until it starts playing, instead you find it in the [Shelf](features/sofie-views.mdx#shelf). + +## Buckets + +A Bucket is a container for AdLib Pieces created by the producer/operator during production. They exist independently of the Rundowns and associated content created by ingesting data from the NRCS. Users can freely create, modify and remove Buckets. + +The primary use-case of these elements is for breaking-news formats where quick turnaround video editing may require circumvention of the regular flow of show assets and programming via the NRCS. Currently, one way of creating AdLibs inside Buckets is using a MOS Plugin integration inside the Shelf, where MOS [ncsItem](https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOSProtocolVersion40/index.html#calibre_link-72) elements can be dragged from the MOS Plugin onto a bucket and ingested. + +The ingest happens via the `getAdlibItem` method: [https://github.com/Sofie-Automation/sofie-core/blob/6c4edee7f352bb542c8a29317d59c0bf9ac340ba/packages/blueprints-integration/src/api/showStyle.ts#L122](https://github.com/Sofie-Automation/sofie-core/blob/6c4edee7f352bb542c8a29317d59c0bf9ac340ba/packages/blueprints-integration/src/api/showStyle.ts#L122) + +## Views + +Being a web-based system, Sofie has a number of customisable, user-facing web [views](features/sofie-views.mdx) used for control and monitoring. + +## Blueprints + +Blueprints are plug-ins that run in Sofie Core. They interpret the data coming in from the rundowns and transform them into a rich set of playable elements \(Segments, Parts, AdLibs etc\). + +The blueprints are webpacked javascript bundles which are uploaded into Sofie via the GUI. They are custom-made and changes depending on the show style, type of input data \(NRCS\) and the types of controlled devices. A generic [blueprint that works with spreadsheets is available here](https://github.com/SuperFlyTV/sofie-demo-blueprints). + +When [Sofie Core](#sofie-core) calls upon a Blueprint, it returns a JavaScript object containing methods callable by Sofie Core. These methods will be called by Sofie Core in different situations, depending on the method. +Documentation on these interfaces are available in the [Blueprints integration](https://www.npmjs.com/package/@sofie-automation/blueprints-integration) library. + +There are 3 types of blueprints, and all 3 must be uploaded into Sofie before the system will work correctly. + +### System Blueprints + +Handle things on the _System level_. +Documentation on the interface to be exposed by the Blueprint: +[https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api.ts#L75](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api.ts#L75) + +### Studio Blueprints + +Handle things on the _Studio level_, like "which showstyle to use for this rundown". +Documentation on the interface to be exposed by the Blueprint: +[https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api.ts#L85](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api.ts#L85) + +### Showstyle Blueprints + +Handle things on the _Showstyle level_, like generating [_Baseline_](#baseline), _Segments_, _Parts, Pieces_ and _Timelines_ in a rundown. +Documentation on the interface to be exposed by the Blueprint: +[https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api.ts#L117](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api.ts#L117) + +## `PartInstances` and `PieceInstances` + +In order to be able to facilitate ingesting changes from the NRCS while continuing to provide a stable and predictable playback of the Rundowns, Sofie internally uses a concept of ["instantiation"]() of key Rundown elements. Before playback of a Part can begin, the Part and it's Pieces are copied into an Instance of a Part: a `PartInstance`. This protects the contents of the _Next_ and _On Air_ part, preventing accidental changes that could surprise the producer/director. This also makes it possible to inspect the "as played" state of the Rundown, independently of the "as planned" state ingested from the NRCS. + +The blueprints can optionally allow some changes to the Parts and Pieces to be forwarded onto these `PartInstances`: [https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api.ts#L190](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api.ts#L190) + +## Timeline + +### What is the timeline? + +The Timeline is a collection of timeline-objects, that together form a "target state", i.e. an intent on what is to be played and at what times. + +The timeline-objects can be programmed to contain relative references to each other, so programming things like _"play this thing right after this other thing"_ is as easy as `{start: { #otherThing.end }}` + +The [Playout Gateway](../for-developers/libraries.md) picks up the timeline from Sofie Core and \(using the [TSR timeline-state-resolver](https://github.com/Sofie-Automation/sofie-timeline-state-resolver)\) controls the playout devices to make sure that they actually play what is intended. + +![Example of 2 objects in a timeline: The #video object, destined to play at a certain time, and #gfx0, destined to start 15 seconds into the video.](/img/docs/main/features/timeline.png) + +### Why a timeline? + +The Sofie system is made to work with a modern web- and IT-based approach in mind. Therefore, the Sofie Core can be run either on-site, or in an off-site cloud. + +![Sofie Core can run in the cloud](/img/docs/main/features/sofie-web-architecture.png) + +One drawback of running in a cloud over the public internet is the - sometimes unpredictable - latency. The Timeline overcomes this by moving all the immediate control of the playout devices to the Playout Gateway, which is intended to run on a local network, close to the hardware it controls. +This also gives the system a simple way of load-balancing - since the number of web-clients or load on Sofie Core won't affect the playout. + +Another benefit of basing the playout on a timeline is that when programming the show \(the blueprints\), you only have to care about "what you want to be on screen", you don't have to care about cleaning up previously played things, or what was actually played out before. Those are things that are handled by the Playout Gateway automatically. This also allows the user to jump around in a rundown freely, without the risk of things going wrong on air. + +### How does it work? + +:::tip +Fun tip! The timeline in itself is a [separate library available on github](https://github.com/SuperFlyTV/supertimeline). + +You can play around with the timeline in the browser using [JSFiddle and the timeline-visualizer](https://jsfiddle.net/nytamin/rztp517u/)! +::: + +The Timeline is stored by Sofie Core in a MongoDB collection. It is generated whenever a user does a [Take](#take-point), changes the [Next-point](#next-point-and-lookahead) or anything else that might affect the playout. + +_Sofie Core_ generates the timeline using: + +- The [Studio Baseline](#baseline) \(only if no rundown is currently active\) +- The [Showstyle Baseline](#baseline), of the currently active rundown. +- The [currently playing Part](#take-point) +- The [Next'ed Part](#next-point-and-lookahead) and Parts that come after it \(the [Lookahead](#lookahead)\) +- Any [AdLibs](#adlib-pieces) the user has manually selected to play + +The [**Playout Gateway**](../for-developers/libraries.md#gateways) then picks up the new timeline, and pipes it into the [\(TSR\) timeline-state-resolver](https://github.com/Sofie-Automation/sofie-timeline-state-resolver) library. + +The TSR then... + +- Resolves the timeline, using the [timeline-library](https://github.com/SuperFlyTV/supertimeline) +- Calculates new target-states for each relevant point in time +- Maps the target-state to each playout device +- Compares the target-states for each device with the currently-tracked-state and.. +- Generates commands to send to each device to account for the change +- The commands are then put on queue and sent to the devices at the correct time + +:::info +For more information about what playout devices _TSR_ supports, and examples of the timeline-objects, see the [README of TSR](https://github.com/Sofie-Automation/sofie-timeline-state-resolver#timeline-state-resolver) +::: + +:::info +For more information about how to program timeline-objects, see the [README of the timeline-library](https://github.com/SuperFlyTV/supertimeline#superfly-timeline) +::: diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/_category_.json b/packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/_category_.json new file mode 100644 index 00000000000..d2aee9ef5b0 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Configuration", + "position": 4 +} \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/settings-view.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/settings-view.md new file mode 100644 index 00000000000..0a570ecbcd7 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/settings-view.md @@ -0,0 +1,181 @@ +--- +sidebar_position: 2 +--- +# Settings View + +:::caution +The settings views are only visible to users with the correct [access level](../features/access-levels.md)! +::: + +Recommended read before diving into the settings: [System, \(Organization\), Studio & Show Style](../concepts-and-architecture.md#system-organization-studio-and-show-style). + +## System + +The _System_ settings are settings for this installation of Sofie. In here goes the settings that are applicable system-wide. + +:::caution +Documentation for this section is yet to be written. +::: + +### Name and logo + +Sofie contains the option to change the name of the installation. This is useful to identify different studios or regions. + +We have also provided some seasonal logos just for fun. + +### System-wide notification message + +This option will show a notification to the user containing some custom text. This can be used to inform the user about on-going problems or maintenance information. + +### Support panel + +The support panel is shown in the rundown view when the user clicks the "?" button in the right bottom corner. It can contain some custom HTML which can be used to refer your users to custom information specific to your organisation. + +### Action triggers + +The action triggers section lets you set custom keybindings for system-level actions such as doing a take or resetting a rundown. + +### Monitoring + +Sofie can be configured to send information to Elastic APM. This can provide useful information about the system's performance to developers. In general this can reduce the performance of Sofie altogether though so it is recommended to disable it in production. + +Sofie can also monitor for blocked threads, and will log a message if it discovers any. This is also recommended to disable in production. + +### CRON jobs + +Sofie contains cron jobs for restarting any casparcg servers through the casparcg launcher as well as a job to create rundown snapshots periodically. + +### Clean up + +The clean up process in Sofie will search the database for unused data and indexes and removes them. If you have had an installation running for many versions this may increase database informance and is in general safe to use at any time. + +## Studio + +A _Studio_ in Sofie-terms is a physical location, with a specific set of devices and equipment. Only one show can be on air in a studio at the same time. +The _studio_ settings are settings for that specific studio, and contains settings related to hardware and playout, such as: + +* **Attached devices** - the Gateways related to this studio +* **Blueprint configuration** - custom config option defined by the blueprints +* **Layer Mappings** - Maps the logical _timeline layers_ to physical devices and outputs + +The Studio uses a studio-blueprint, which handles things like mapping up an incoming rundown to a Showstyle. + +### Attached Devices + +This section allows you to add and remove Gateways that are related to this _Studio_. When a Gateway is attached to a Studio, it will react to the changes happening within it, as well as feed the necessary data into it. + +### Blueprint Configuration + +Sofie allows the Blueprints to expose custom configuration fields that allow the System Administrator to reconfigure how these Blueprints work through the Sofie UI. Here you can change the configuration of the [Studio Blueprint](../concepts-and-architecture.md#studio-blueprints). + +### Layer Mappings + +This section allows you to add, remove and configure how logical device-control will be translated to physical automation control. [Blueprints](../concepts-and-architecture.md#blueprints) control devices through objects placed on a [Timeline](../concepts-and-architecture.md#timeline) using logical device identifiers called _Layers_. A layer represents a single aspect of a device that can be controlled at a given time: a video switcher's M/E bus, an audio mixers's fader, an OSC control node, a video server's output channel. Layer Mappings translate these logical identifiers into physical device aspects, for example: + +![A sample configuration of a Layer Mapping for the M/E1 Bus of an ATEM switcher](/img/docs/main/features/atem-layer-mapping-example.png) + +This _Layer Mapping_ configures the `atem_me_program` Timeline-layer to control the `atem0` device of the `ATEM` type. No Lookahead will be enabled for this layer. This layer will control a `MixEffect` aspect with the Index of `0` \(so M/E 1 Bus\). + +These mappings allow the System Administrator to reconfigure what devices the Blueprints will control, without the need of changing the Blueprint code. + +#### Route Sets + +In order to allow the Producer to reconfigure the automation from the Switchboard in the [Rundown View](../concepts-and-architecture.md#rundown-view), as well as have some pre-set automation control available for the System Administrator, Sofie has a concept of Route Sets. Route Sets work on top of the Layer Mappings, by configuring sets of [Layer Mappings](settings-view.md#layer-mappings) that will re-route the control from one device to another, or to disable the automation altogether. These Route Sets are presented to the Producer in the [Switchboard](../concepts-and-architecture.md#switchboard) panel. + +A Route Set is essentially a distinct set of Layer Mappings, which can modify the settings already configured by the Layer Mappings, but can be turned On and Off. Called Routes, these can change: + +* the Layer ID to a new Layer ID +* change the Device being controlled by the Layer +* change the aspect of the Device that's being controlled. + +Route Sets can be grouped into Exclusivity Groups, in which only a single Route Set can be enabled at a time. When activating a Route Set within an Exclusivity Group, all other Route Sets in that group will be deactivated. This in turn, allows the System Administrator to create entire sections of exclusive automation control within the Studio that the Producer can then switch between. One such example could be switching between Primary and Backup playout servers, or switching between Primary and Backup talent microphone. + +![The Exclusivity Group Name will be displayed as a header in the Switchboard panel](/img/docs/main/features/route-sets-exclusivity-groups.png) + +A Route Set has a Behavior property which will dictate what happens how the Route Set operates: + +| Type | Behavior | +| :-------------- | :------------------------------------------------------------------------------------------------------------------------------ | +| `ACTIVATE_ONLY` | This RouteSet cannot be deactivated, only a different RouteSet in the same Exclusivity Group can cause it to deactivate | +| `TOGGLE` | The RouteSet can be activated and deactivated. As a result, it's possible for the Exclusivity Group to have no Route Set active | +| `HIDDEN` | The RouteSet can be activated and deactivated, but it will not be presented to the user in the Switchboard panel | + +![An active RouteSet with a single Layer Mapping being re-configured](/img/docs/main/features/route-set-remap.png) + +Route Sets can also be configured with a _Default State_. This can be used to contrast a normal, day-to-day configuration with an exceptional one \(like using a backup device\) in the [Switchboard](../concepts-and-architecture#switchboard) panel. + +| Default State | Behavior | +| :------------ | :------------------------------------------------------------ | +| Active | If the Route Set is not active, an indicator will be shown | +| Not Active | If the Route Set is active, an indicator will be shown | +| Not defined | No indicator will be shown, regardless of the Route Set state | + +## Show style + +A _Showstyle_ is related to the looks and logic of a _show_, which in contrast to the _studio_ is not directly related to the hardware. +The Showstyle contains settings like + +* **Source Layers** - Groups different types of content in the GUI +* **Output Channels** - Indicates different output targets \(such as the _Program_ or _back-screen in the studio_\) +* **Action Triggers** - Select how actions can be started on a per-show basis, outside of the on-screen controls +* **Blueprint configuration** - custom config option defined by the blueprints + +:::caution +Please note the difference between _Source Layers_ and _timeline-layers_: + +[Pieces](../concepts-and-architecture.md#piece) are put onto _Source layers_, to group different types of content \(such as a VT or Camera\), they are therefore intended only as something to indicate to the user what is going to be played, not what is actually going to happen on the technical level. + +[Timeline-objects](../concepts-and-architecture.md#timeline-object) \(inside of the [Pieces](../concepts-and-architecture.md#piece)\) are put onto timeline-layers, which are \(through the Mappings in the studio\) mapped to physical devices and outputs. +The exact timeline-layer is never exposed to the user, but instead used on the technical level to control playout. + +An example of the difference could be when playing a VT \(that's a Source Layer\), which could involve all of the timeline-layers _video\_player0_, _audio\_fader\_video_, _audio\_fader\_host_ and _mixer\_pgm._ +::: + +### Action Triggers + +This is a way to set up how - outside of the Point-and-Click Graphical User Interface - actions can be performed in the User Interface. Commonly, these are the *hotkey combinations* that can be used to either trigger AdLib content or other actions in the larger system. This is done by creating sets of Triggers and Actions to be triggered by them. These pairs can be set at the Show Style level or at the _Sofie Core_ (System) level, for common actions such as doing a Take or activating a Rundown, where you want a shared method of operation. _Sofie Core_ migrations will set up a base set of basic, system-wide Action Triggers for interacting with rundowns, but they can be changed by the System blueprint. + +![Action triggers define modes of interacting with a Rundown](/img/docs/main/features/action_triggers_3.png) + +#### Triggers + +The triggers are designed to be either client-specific or issued by a peripheral device module. + +Currently, the Action Triggers system supports setting up two types of triggeers: Hotkeys and Device Triggers. + +Hotkeys are valid in the scope of a browser window and can be either a single key, a combination of keys (*combo*) or a *chord* - a sequence of key combinations pressed in a particular order. *Chords* are popular in some text editing applications and vastly expand the amount of actions that can be triggered from a keyboard, at the expense of the time needed to execute them. Currently, the Hotkey editor in Sofie does not support creating *Chords*, but they can be specified by Blueprints during migrations. + +To edit a given trigger, click on the trigger pill on the left of the Trigger-Action set. When hovering, a **+** sign will appear, allowing you to add a new trigger to the set. + +Device Triggers are valid in the scope of a Studio and will be evaluated on the currently active Rundown in a given Studio. To use Device Triggers, you need to have at least a single [Input Gateway](../installation/installing-input-gateway) attached to a Studio and a Device configured in the Input Gateway. Once that's done, when selecting a **Device** trigger type in the pop-up, you can invoke triggers on your Input Device and you will see a preview of the input events shown at the bottom of the pop-up. You can select which of these events should be the trigger by clicking on one of the previews. Note, that some devices differentiate between _Up_ and _Down_ triggers, while others don't. Some may also have other activities that can be done _to_ a trigger. What they are and how they are identified is device-specific and is best discovered through interaction with the device. + +If you would like to set up combination Triggers, using Device Triggers on an Input Device that does not support them natively, you may want to look into [Shift Registers](#shift-registers) + +#### Actions + +The actions are built using a base *action* (such as *Activate a Rundown* or *AdLib*) and a set of *filters*, limiting the scope of the *action*. Optionally, some of these *actions* can take additional *parameters*. These filters can operate on various types of objects, depending on the action in question. All actions currently require that the chain of filters starts with scoping out the Rundown the action is supposed to affect. Currently, there is only one type of Rundown-level filter supported: "The Rundown currently in view". + +The Action Triggers user interface guides the user in a wizard-like fashion through the available *filter* options on a given *action*. + +![Actions can take additional parameters](/img/docs/main/features/action_triggers_2.png) + +If the action provides a preview of the triggered items and there is an available matching Rundown, a preview will be displayed for the matching objects in that Rundown. The system will select the current active rundown, if it is of the currently-edited ShowStyle, and if not, it will select the first available Rundown of the currently-edited ShowStyle. + +![A preview of the action, as scoped by the filters](/img/docs/main/features/action_triggers_4.png) + +Clicking on the action and filter pills allows you to edit the action parameters and filter parameters. *Limit* limits the amount of objects to only the first *N* objects matched - this can significantly improve performance on large data sets. *Pick* and *Pick last* filters end the chain of the filters by selecting a single item from the filtered set of objects (the *N-th* object from the beginning or the end, respectively). *Pick* implicitly contains a *Limit* for the performance improvement. This is not true for *Pick last*, though. + +##### Shift Registers + +Shift Register modification actions are a special type of an Action, that modifies an internal state memory of the [Input Gateway](../installation/installing-input-gateway.md) and allows combination triggers, pagination, etc. on devices that don't natively support them or combining multiple devices into a single Control Surface. Refer to _Input Gateway_ documentation for more information on Shift Registers. + +Shift Register actions have no effect in the browser, triggered from a _Hotkey_. + +## Migrations + +The migrations are automatic setup-scripts that help you during initial setup and system upgrades. + +There are system-migrations that comes directly from the version of _Sofie Core_ you're running, and there are also migrations added by the different blueprints. + +It is mandatory to run migrations when you've upgraded _Sofie Core_ to a new version, or upgraded your blueprints. + diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/sofie-core-settings.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/sofie-core-settings.md new file mode 100644 index 00000000000..a6d00aa139c --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/sofie-core-settings.md @@ -0,0 +1,110 @@ +--- +sidebar_position: 1 +--- + +# Sofie Core: System Configuration + +_Sofie Core_ is configured at it's most basic level using a settings file and environment variables. + +### Environment Variables + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SettingUseDefault valueExample
+ METEOR_SETTINGS + Contents of settings file (see below) + $(cat settings.json) +
+ TZ + The default time zone of the server (used in logging) + Europe/Amsterdam +
+ MAIL_URL + + Email server to use. See{' '} + https://docs.meteor.com/api/email.html + + smtps://USERNAME:PASSWORD@HOST:PORT +
+ LOG_TO_FILE + File path to log to file + /logs/core/ +
+ +### Settings File + +The settings file is an optional JSON file that contains some configuration settings for how the _Sofie Core_ works and behaves. + +To use a settings file: + +- During development: `meteor --settings settings.json` +- During prod: environment variable \(see above\) + +The structure of the file allows for public and private fields. At the moment, Sofie only uses public fields. Below is an example settings file: + +```text +{ + "public": { + "frameRate": 25 + } +} +``` + +There are various settings you can set for an installation. See the list below: + +| **Field name** | Use | Default value | +| :---------------------------- | :---------------------------------------------------------------------------------------------------------------------------- | :------------------------------------- | +| `autoRewindLeavingSegment` | Should segments be automatically rewound after they stop playing | `false` | +| `disableBlurBorder` | Should a border be displayed around the Rundown View when it's not in focus and studio mode is enabled | `false` | +| `defaultTimeScale` | An arbitrary number, defining the default zoom factor of the Timelines | `1` | +| `allowGrabbingTimeline` | Can Segment Timelines be grabbed to scroll them? | `true` | +| `enableHeaderAuth` | If true, enable http header based security measures. See [here](../features/access-levels) for details on using this | `false` | +| `defaultDisplayDuration` | The fallback duration of a Part, when it's expectedDuration is 0. \_\_In milliseconds | `3000` | +| `allowMultiplePlaylistsInGUI` | If true, allows creation of new playlists in the Lobby Gui (rundown list). If false; only pre-existing playlists are allowed. | `false` | +| `followOnAirSegmentsHistory` | How many segments of history to show when scrolling back in time (0 = show current segment only) | `0` | +| `maximumDataAge` | Clean up stuff that are older than this [ms]) | 100 days | +| `poisonKey` | Enable the use of poison key if present and use the key specified. | `'Escape'` | +| `enableNTPTimeChecker` | If set, enables a check to ensure that the system time doesn't differ too much from the specified NTP server time. | `null` | +| `defaultShelfDisplayOptions` | Default value used to toggle Shelf options when the 'display' URL argument is not provided. | `buckets,layout,shelfLayout,inspector` | +| `enableKeyboardPreview` | The KeyboardPreview is a feature that is not implemented in the main Fork, and is kept here for compatibility | `false` | +| `keyboardMapLayout` | Keyboard map layout (what physical layout to use for the keyboard) | STANDARD_102_TKL | +| `customizationClassName` | CSS class applied to the body of the page. Used to include custom implementations that differ from the main Fork. | `undefined` | +| `useCountdownToFreezeFrame` | If true, countdowns of videos will count down to the last freeze-frame of the video instead of to the end of the video | `true` | +| `confirmKeyCode` | Which keyboard key is used as "Confirm" in modal dialogs etc. | `'Enter'` | + +:::info +The exact definition for the settings can be found [in the code here](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/Settings.ts#L12). +::: diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/faq.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/faq.md new file mode 100644 index 00000000000..73c8373c8f8 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/faq.md @@ -0,0 +1,16 @@ +# FAQ + +## What software license does the system use? + +All main components are using the [MIT license](https://opensource.org/licenses/MIT). + +## Is there anything missing in the public repositories? + +Everything needed to install and configure a fully functioning Sofie system is publicly available, with the following exceptions: + +- A rundown data set describing the actual TV show and of media assets. +- Blueprints for your specific show. + +## When will feature _y_ become available? + +Check out the [issues page](https://github.com/Sofie-Automation/Sofie-TV-automation/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3ARelease), where there are notes on current and upcoming releases. diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/_category_.json b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/_category_.json new file mode 100644 index 00000000000..0dd70d8b0ec --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Features", + "position": 2 +} \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/access-levels.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/access-levels.md new file mode 100644 index 00000000000..b0d765c86bb --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/access-levels.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 3 +--- + +# Access Levels + +## Permissions + +There are a few different access levels that users can be assigned. They are not hierarchical, you will often need to enable multiple for each user. +Any client that can access Sofie always has at least view-only access to the rundowns, and system status pages. + +| Level | Summary | +| :------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- | +| **studio** | Grants access to operate a studio for playout of a rundown. | +| **configure** | Grants access to the settings pages of Sofie, and other abilities to configure the system. | +| **developer** | Grants access to some tools useful to developers. This also changes some ui behaviours to be less aggressive in what is shown in the rundown view | +| **testing** | Enables the page Test Tools, which contains various tools useful for testing the system during development | +| **service** | Grants access to the external message status page, and some additional rundown management options that are not commonly needed | +| **gateway** | Grants access to various APIs intended for use by the various gateways that connect Sofie to other systems. | + +## Authentication providers + +There are two ways to define the access for each user, which to use depends on your security requirements. + +### Browser based + +:::info + +This is a simple mode that relies on being able to trust every client that can connect to Sofie + +::: + +In this mode, a variety of access levels can be set via the URL. The access level is persisted in browser's Local Storage. + +By default, a user cannot edit settings, nor play out anything. Some of the access levels provide additional administrative pages or helpful tool tips for new users. These modes are persistent between sessions and will need to be manually enabled or disabled by appending a suffix to the url. +Each of the modes listed in the levels table above can be used here, such as by navigating to `https://my-sofie/?studio=1` to enable studio mode, or `https://my-sofie/?studio=0` to disable studio mode. + +There are some additional url parameters that can be used to simplify the granting of permissions: + +- `?help=1` will enable some tooltips that might be useful to new users. +- `?admin=1` will give the user the same access as the _Configuration_ and _Studio_ modes as well as having access to a set of _Test Tools_ and a _Manual Control_ section on the Rundown page. + +#### See Also + +[URL Query Parameters](../../for-developers/url-query-parameters.md) + +### Header based + +:::danger + +This mode is very new and could have some undiscovered holes. +It is known that secrets can be leaked to all clients who can connect to Sofie, which is not desirable. + +::: + +In this mode, we rely on Sofie being run behind a reverse-proxy which will inform Sofie of the permissions of each connection. This allows you to use your organisations preferred auth provider, and translate that into something that Sofie can understand. +To enable this mode, you need to enable the `enableHeaderAuth` property in the [settings file](../configuration/sofie-core-settings.md) + +Sofie expects that for each DDP connection or http request, the `dnt` header will be set containing a comma separated list of the levels from the above table. If the header is not defined or is empty, the connection will have view-only access to Sofie. +This header can also contain simply `admin` to grant the connection permission to everything. +We are using the `dnt` header due to limitations imposed by Meteor, but intend this to become a proper header name in a future release. + +When in this mode, you should make sure that Sofie can only be accessed through the reverse proxy, and that the reverse-proxy will always override any value sent by a client. +Because the value is defined in the http headers, it is not possible to revoke permissions for a user who currently has the ui open. If this is necessary to do, you can force the connection to be dropped by the reverse-proxy. diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/api.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/api.md new file mode 100644 index 00000000000..a6ee88bcddd --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/api.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 10 +--- + +# API + +## Sofie User Actions REST API + +Starting with version 1.50.0, there is a semantically-versioned HTTP REST API defined using the [OpenAPI specification](https://spec.openapis.org/oas/v3.0.3) that exposes some of the functionality available through the GUI in a machine-readable fashion. The API specification can be found in the `packages/openapi` folder. The latest version of this API is available in _Sofie Core_ using the endpoint: `/api/1.0`. There should be no assumption of backwards-compatibility for this API, but this API will be semantically-versioned, with redirects set up for minor-version changes for compatibility. + +There is a also a legacy REST API available that can be used to fetch data and trigger actions. The documentation for this API is minimal, but the API endpoints are listed by _Sofie Core_ using the endpoint: `/api/0` + +## Sofie Live Status Gateway + +Starting with version 1.50.0, there is also a separate service available, called _Sofie Live Status Gateway_, running as a separate process, which will connect to the _Sofie Core_ as a Peripheral Device, listen to the changes of it's state and provide a PubSub service offering a machine-readable view into the system. The WebSocket API is defined using the [AsyncAPI specification](https://v2.asyncapi.com/docs/reference/specification/v2.5.0) and the specification can be found in the `packages/live-status-gateway/api` folder. + +## DDP – Core Integration + +If you're planning to build NodeJS applications that talk to _Sofie Core_, we recommend using the [core-integration](https://github.com/Sofie-Automation/sofie-core/tree/main/packages/server-core-integration.md) library, which exposes a number of callable methods and allows for subscribing to data the same way the [Gateways](../concepts-and-architecture.md#gateways) do it. diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/language.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/language.md new file mode 100644 index 00000000000..9fe03d816e7 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/language.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 7 +--- +# Language + +_Sofie_ uses the [i18n internationalisation framework](https://www.i18next.com/) that allows you to present user-facing views in multiple languages. + +## Language selection + +The UI will automatically detect user browser's default matching and select the best match, falling back to English. You can also force the UI language to any language by navigating to a page with `?lng=xx` query string, for example: + +`http://localhost:3000/?lng=en` + +This choice is persisted in browser's local storage, and the same language will be used until a new forced language is chosen using this method. + +_Sofie_ currently supports three languages: +* English _(default)_ `en` +* Norwegian bokmål `nb` +* Norwegian nynorsk `nn` + +## Further Reading + +* [List of language tags](https://en.wikipedia.org/wiki/IETF_language_tag) \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/prompter.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/prompter.md new file mode 100644 index 00000000000..d3b40372db7 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/prompter.md @@ -0,0 +1,199 @@ +--- +sidebar_position: 3 +--- + +# Prompter + +See [Sofie views](sofie-views.mdx#prompter-view) for how to access the prompter page. + +![Prompter screen before the first Part is taken](/img/docs/main/features/prompter-view.png) + +The prompter will display the script for the Rundown currently active in the Studio. On Air and Next parts and segments are highlighted - in red and green, respectively - to aid in navigation. In top-right corner of the screen, a Diff clock is shown, showing the difference between planned playback and what has been actually produced. This allows the host to know how far behind/ahead they are in regards to planned execution. + +![Indicators for the On Air and Next part shown underneath the Diff clock](/img/docs/main/features/prompter-view-indicators.png) + +If the user scrolls the prompter ahead or behind the On Air part, helpful indicators will be shown in the right-hand side of the screen. If the On Air or Next part's script is above the current viewport, arrows pointing up will be shown. If the On Air part's script is below the current viewport, a single arrow pointing down will be shown. + +## Customize looks + +The prompter UI can be configured using query parameters: + +| Query parameter | Type | Description | Default | +| :-------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------ | +| `mirror` | 0 / 1 | Mirror the display horizontally | `0` | +| `mirrorv` | 0 / 1 | Mirror the display vertically | `0` | +| `fontsize` | number | Set a custom font size of the text. 20 will fit in 5 lines of text, 14 will fit 7 lines etc.. | `14` | +| `marker` | string | Set position of the read-marker. Possible values: "center", "top", "bottom", "hide" | `hide` | +| `margin` | number | Set margin of screen \(used on monitors with overscan\), in %. | `0` | +| `showmarker` | 0 / 1 | If the marker is not set to "hide", control if the marker is hidden or not | `1` | +| `showscroll` | 0 / 1 | Whether the scroll bar should be shown | `1` | +| `followtake` | 0 / 1 | Whether the prompter should automatically scroll to current segment when the operator TAKE:s it | `1` | +| `showoverunder` | 0 / 1 | The timer in the top-right of the prompter, showing the overtime/undertime of the current show. | `1` | +| `debug` | 0 / 1 | Whether to display a debug box showing controller input values and the calculated speed the prompter is currently scrolling at. Used to tweak speedMaps and ranges. | `0` | + +Example: [http://127.0.0.1/prompter/studio0/?mode=mouse&followtake=0&fontsize=20](http://127.0.0.1/prompter/studio0/?mode=mouse&followtake=0&fontsize=20) + +## Controlling the prompter + +The prompter can be controlled by different types of controllers. The control mode is set by a query parameter, like so: `?mode=mouse`. + +| Query parameter | Description | +| :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Default | Controlled by both mouse and keyboard | +| `?mode=mouse` | Controlled by mouse only. [See configuration details](prompter.md#control-using-mouse-scroll-wheel) | +| `?mode=keyboard` | Controlled by keyboard only. [See configuration details](prompter.md#control-using-keyboard) | +| `?mode=shuttlekeyboard` | Controlled by a Contour Design ShuttleXpress, X-keys Jog and Shuttle or any compatible, configured as keyboard-ish device. [See configuration details](prompter.md#control-using-contour-shuttlexpress-or-x-keys) | +| `?mode=shuttlewebhid` | Controlled by a Contour Design ShuttleXpress, using the browser's WebHID API [See configuration details](prompter.md#control-using-contour-shuttlexpress-via-webhid) | +| `?mode=pedal` | Controlled by any MIDI device outputting note values between 0 - 127 of CC notes on channel 8. Analogue Expression pedals work well with TRS-USB midi-converters. [See configuration details](prompter.md#control-using-midi-input-mode-pedal) | +| `?mode=joycon` | Controlled by Nintendo Switch Joycon, using the HTML5 GamePad API. [See configuration details](prompter.md#control-using-nintendo-joycon-gamepad) | + +#### Control using mouse \(scroll wheel\) + +The prompter can be controlled in multiple ways when using the scroll wheel: + +| Query parameter | Description | +| :-------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `?controlmode=normal` | Scrolling of the mouse works as "normal scrolling" | +| `?controlmode=speed` | Scrolling of the mouse changes the speed of scrolling. Left-click to toggle, right-click to rewind | +| `?controlmode=smoothscroll` | Scrolling the mouse wheel starts continuous scrolling. Small speed adjustments can then be made by nudging the scroll wheel. Stop the scrolling by making a "larger scroll" on the wheel. | + +has several operating modes, described further below. All modes are intended to be controlled by a computer mouse or similar, such as a presenter tool. + +#### Control using keyboard + +Keyboard control is intended to be used when having a "keyboard"-device, such as a presenter tool. + +| Scroll up | Scroll down | +| :----------- | :------------ | +| `Arrow Up` | `Arrow Down` | +| `Arrow Left` | `Arrow Right` | +| `Page Up` | `Page Down` | +| | `Space` | + +#### Control using Contour ShuttleXpress or X-keys \(_?mode=shuttlekeyboard_\) + +This mode is intended to be used when having a Contour ShuttleXpress or X-keys device, configured to work as a keyboard device. These devices have jog/shuttle wheels, and their software/firmware allow them to map scroll movement to keystrokes from any key-combination. Since we only listen for key combinations, it effectively means that any device outputting keystrokes will work in this mode. + +| Query parameter | Type | Description | Default | +| :----------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | +| `shuttle_speedMap` | Array of numbers | Speeds to scroll by \(px. pr. frame - approx 60fps\) when scrolling forwards. The beginning of the forwards-range maps to the first number in this array, and the end of the forwards-range map to the end of this array. All values in between are being interpolated using a spline curve. | `0, 1, 2, 3, 5, 7, 9, 30]` | + +| Key combination | Function | +| :--------------------------------------------------------- | :------------------------------------- | +| `Ctrl` `Alt` `F1` ... `Ctrl` `Alt` `F7` | Set speed to +1 ... +7 \(Scroll down\) | +| `Ctrl` `Shift` `Alt` `F1` ... `Ctrl` `Shift` `Alt` `F7` | Set speed to -1 ... -7 \(Scroll up\) | +| `Ctrl` `Alt` `+` | Increase speed | +| `Ctrl` `Alt` `-` | Decrease speed | +| `Ctrl` `Alt` `Shift` `F8`, `Ctrl` `Alt` `Shift` `PageDown` | Jump to next Segment and stop | +| `Ctrl` `Alt` `Shift` `F9`, `Ctrl` `Alt` `Shift` `PageUp` | Jump to previous Segment and stop | +| `Ctrl` `Alt` `Shift` `F10` | Jump to top of Script and stop | +| `Ctrl` `Alt` `Shift` `F11` | Jump to Live and stop | +| `Ctrl` `Alt` `Shift` `F12` | Jump to next Segment and stop | + +Configuration files that can be used in their respective driver software: + +- [Contour ShuttleXpress](https://github.com/Sofie-Automation/sofie-core/blob/release26/resources/prompter_layout_shuttlexpress.pref) +- [X-keys](https://github.com/Sofie-Automation/sofie-core/blob/release26/resources/prompter_layout_xkeys.mw3) + +#### Control using Contour ShuttleXpress via WebHID + +This mode uses a Contour ShuttleXpress (Multimedia Controller Xpress) through web browser's WebHID API. + +When opening the Prompter View for the first time, it is necessary to press the _Connect to Contour Shuttle_ button in the top left corner of the screen, select the device, and press _Connect_. + +![Contour ShuttleXpress input mapping](/img/docs/main/features/contour-shuttle-webhid.jpg) + +#### + +#### Control using midi input \(_?mode=pedal_\) + +This mode listens to MIDI CC-notes on channel 8, expecting a linear range like i.e. 0-127. Sutiable for use with expression pedals, but any MIDI controller can be used. The mode picks the first connected MIDI device, and supports hot-swapping \(you can remove and add the device without refreshing the browser\). + +Web-Midi requires the web page to be served over HTTPS, or that the Chrome flag `unsafely-treat-insecure-origin-as-secure` is set. + +If you want to use traditional analogue pedals with 5 volt TRS connection, a converter such as the _Beat Bars EX2M_ will work well. + +| Query parameter | Type | Description | Default | +| :---------------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------- | +| `pedal_speedMap` | Array of numbers | Speeds to scroll by \(px. pr. frame - approx 60fps\) when scrolling forwards. The beginning of the forwards-range maps to the first number in this array, and the end of the forwards-range map to the end of this array. All values in between are being interpolated using a spline curve. | `[1, 2, 3, 4, 5, 7, 9, 12, 17, 19, 30]` | +| `pedal_reverseSpeedMap` | Array of numbers | Same as `pedal_speedMap` but for the backwards range. | `[10, 30, 50]` | +| `pedal_rangeRevMin` | number | The end of the backwards-range, full speed backwards. | `0` | +| `pedal_rangeNeutralMin` | number | The beginning of the backwards-range. | `35` | +| `pedal_rangeNeutralMax` | number | The minimum input to run forward, the start of the forward-range \(min speed\). This is also the end of any "deadband" you want filter out before starting moving forwards. | `80` | +| `pedal_rangeFwdMax` | number | The maximum input, the end of the forward-range \(max speed\) | `127` | + +- `pedal_rangeNeutralMin` has to be greater than `pedal_rangeRevMin` +- `pedal_rangeNeutralMax` has to be greater than `pedal_rangeNeutralMin` +- `pedal_rangeFwdMax` has to be greater than `pedal_rangeNeutralMax` + +![Yamaha FC7 mapped for both a forward (80-127) and backwards (0-35) range.](/img/docs/main/features/yamaha-fc7.jpg) + +The default values allow for both going forwards and backwards. This matches the _Yamaha FC7_ expression pedal. The default values create a forward-range from 80-127, a neutral zone from 35-80 and a reverse-range from 0-35. + +Any movement within forward range will map to the `pedal_speedMap` with interpolation between any numbers in the `pedal_speedMap`. You can turn on `?debug=1` to see how your input maps to an output. This helps during calibration. Similarly, any movement within the backwards rage maps to the `pedal_reverseSpeedMap`. + +**Calibration guide:** + +| **Symptom** | Adjustment | +| :---------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| _"I can't rest my foot without it starting to run"_ | Increase `pedal_rangeNeutralMax` | +| _"I have to push too far before it starts moving"_ | Decrease `pedal_rangeNeutralMax` | +| _"It starts out fine, but runs too fast if I push too hard"_ | Add more weight to the lower part of the `pedal_speedMap` by adding more low values early in the map, compared to the large numbers in the end. | +| _"I have to go too far back to reverse"_ | Increase `pedal_rangeNeutralMin` | +| _"As I find a good speed, it varies a bit in speed up/down even if I hold my foot still"_ | Use `?debug=1` to see what speed is calculated in the position the presenter wants to rest the foot in. Add more of that number in a sequence in the `pedal_speedMap` to flatten out the speed curve, i.e. `[1, 2, 3, 4, 4, 4, 4, 5, ...]` | + +**Note:** The default values are set up to work with the _Yamaha FC7_ expression pedal, and will probably not be good for pedals with one continuous linear range from fully released to fully depressed. A suggested configuration for such pedals \(i.e. the _Mission Engineering EP-1_\) will be like: + +| Query parameter | Suggestion | +| :---------------------- | :-------------------------------------- | +| `pedal_speedMap` | `[1, 2, 3, 4, 5, 7, 9, 12, 17, 19, 30]` | +| `pedal_reverseSpeedMap` | `-2` | +| `pedal_rangeRevMin` | `-1` | +| `pedal_rangeNeutralMin` | `0` | +| `pedal_rangeNeutralMax` | `1` | +| `pedal_rangeFwdMax` | `127` | + +#### Control using Nintendo Joycon \(_?mode=joycon_\) + +This mode uses the browsers Gamapad API and polls connected Joycons for their states on button-presses and joystick inputs. + +The Joycons can operate in 3 modes, the L-stick, the R-stick or both L+R sticks together. Reconnections and jumping between modes works, with one known limitation: **Transition from L+R to a single stick blocks all input, and requires a reconnect of the sticks you want to use.** This seems to be a bug in either the Joycons themselves or in the Gamepad API in general. + +| Query parameter | Type | Description | Default | +| :----------------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | +| `joycon_speedMap` | Array of numbes | Speeds to scroll by \(px. pr. frame - approx 60fps\) when scrolling forwards. The beginning of the forwards-range maps to the first number in this array, and thee end of the forwards-range map to the end of this array. All values in between are being interpolated in a spline curve. | `[1, 2, 3, 4, 5, 8, 12, 30]` | +| `joycon_reverseSpeedMap` | Array of numbers | Same as `joycon_speedMap` but for the backwards range. | `[1, 2, 3, 4, 5, 8, 12, 30]` | +| `joycon_rangeRevMin` | number | The end of the backwards-range, full speed backwards. | `-1` | +| `joycon_rangeNeutralMin` | number | The beginning of the backwards-range. | `-0.25` | +| `joycon_rangeNeutralMax` | number | The minimum input to run forward, the start of the forward-range \(min speed\). This is also the end of any "deadband" you want filter out before starting moving forwards. | `0.25` | +| `joycon_rangeFwdMax` | number | The maximum input, the end of the forward-range \(max speed\) | `1` | +| `joycon_rightHandOffset` | number | A ratio to increase or decrease the R Joycon joystick sensitivity relative to the L Joycon. | `1.4` | + +- `joycon_rangeNeutralMin` has to be greater than `joycon_rangeRevMin` +- `joycon_rangeNeutralMax` has to be greater than `joycon_rangeNeutralMin` +- `joycon_rangeFwdMax` has to be greater than `joycon_rangeNeutralMax` + +![Nintendo Switch Joycons](/img/docs/main/features/nintendo-switch-joycons.jpg) + +You can turn on `?debug=1` to see how your input maps to an output. + +**Button map:** + +| **Button** | Acton | +| :--------- | :------------------------ | +| L2 / R2 | Go to the "On-air" story | +| L / R | Go to the "Next" story | +| Up / X | Go top the top | +| Left / Y | Go to the previous story | +| Right / A | Go to the following story | + +**Calibration guide:** + +| **Symptom** | Adjustment | +| :------------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| _"The prompter drifts upwards when I'm not doing anything"_ | Decrease `joycon_rangeNeutralMin` | +| _"The prompter drifts downwards when I'm not doing anything"_ | Increase `joycon_rangeNeutralMax` | +| _"It starts out fine, but runs too fast if I move too far"_ | Add more weight to the lower part of the `joycon_speedMap / joycon_reverseSpeedMap` by adding more low values early in the map, compared to the large numbers in the end. | +| _"I can't reach max speed backwards"_ | Increase `joycon_rangeRevMin` | +| _"I can't reach max speed forwards"_ | Decrease `joycon_rangeFwdMax` | +| _"As I find a good speed, it varies a bit in speed up/down even if I hold my finger still"_ | Use `?debug=1` to see what speed is calculated in the position the presenter wants to rest their finger in. Add more of that number in a sequence in the `joycon_speedMap` to flatten out the speed curve, i.e. `[1, 2, 3, 4, 4, 4, 4, 5, ...]` | diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/sofie-views.mdx b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/sofie-views.mdx new file mode 100644 index 00000000000..4ce3b9ba014 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/sofie-views.mdx @@ -0,0 +1,333 @@ +--- +sidebar_position: 2 +--- + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + +# Sofie Views + +## Lobby View + +![Rundown View](/img/docs/lobby-view.png) + +All existing rundowns are listed in the _Lobby View_. + +## Rundown View + +![Rundown View](/img/docs/main/features/active-rundown-example.png) + +The _Rundown View_ is the main view that the producer is working in. + +![The Rundown view and naming conventions of components](/img/docs/main/sofie-naming-conventions.png) + +![Take Next](/img/docs/main/take-next.png) + +#### Take Point + +The Take point is currently playing [Part](#part) in the rundown, indicated by the "On Air" line in the GUI. +What's played on air is calculated from the timeline objects in the Pieces in the currently playing part. + +The Pieces inside of a Part determines what's going to happen, the could be indicating things like VT:s, cut to cameras, graphics, or what script the host is going to read. + +:::info +You can TAKE the next part by pressing _F12_ or the _Numpad Enter_ key. +::: + +#### Next Point + +The Next point is the next queued Part in the rundown. When the user clicks _Take_, the Next Part becomes the currently playing part, and the Next point is also moved. + +:::info +Change the Next point by right-clicking in the GUI, or by pressing \(Shift +\) F9 & F10. +::: + +#### Freeze-frame Countdown + +![Part is 1 second heavy, LiveSpeak piece has 7 seconds of playback until it freezes](/img/docs/main/freeze-frame-countdown.png) + +If a Piece has more or less content than the Part's expected duration allows, an additional counter with a Snowflake icon will be displayed, attached to the On Air line, counting down to the moment when content from that Piece will freeze-frame at the last frame. The time span in which the content from the Piece will be visible on the output, but will be frozen, is displayed with an overlay of icicles. + +#### Lookahead + +Elements in the [Next point](#next-point) \(or beyond\) might be pre-loaded or "put on preview", depending on the blueprints and playout devices used. This feature is called "Lookahead". + +### Storyboard Mode + +In the top-right corner of the Segment, there's a button controlling the display style of a given Segment. The default display style of a Segment can be indicated by the [Blueprints](../concepts-and-architecture.md#blueprints), but the User can switch to a different mode at any time. You can also change the display mode of all Segments at once, using a button in the bottom-right corner of the Rundown View. + +![Storyboard Mode](/img/docs/main/storyboard.png) + +The **_Storyboard_** mode is an alternative to the default **_Timeline_** mode. In Storyboard mode, the accurate placement in time of each Piece is not visualized, so that more Parts can be visualized at once in a single row. This can be particularly useful in Shows without very strict timing planning or where timing is not driven by the User, but rather some external factor; or in Shows where very long Parts are joined with very short ones: sports, events and debates. This mode also does not visualize the history of the playback: rather, it only shows what is currently On Air or is planned to go On Air. + +Storyboard mode selects a "main" Piece of the Part, using the same logic as the [Presenter View](#presenter-view), and presents it with a larger, hover-scrub-enabled Piece for easy preview. The countdown to freeze-frame is displayed in the top-right hand corner of the Thumbnail, once less than 10 seconds remain to freeze-frame. The Transition Piece is displayed on top of the thumbnail. Other Pieces are placed below the thumbnail, stacked in order of playback. After a Piece goes off-air, it will disappear from the view. + +If no more Parts can be displayed in a given Segment, they are stacked in order on the right side of the Segment. The User can scroll through thse Parts by click-and-dragging the Storyboard area, or using the mouse wheel - `Alt`+Wheel, if only a vertical wheel is present in the mouse. + +### List View Mode + +Another mode available to display a Segment is the List View. In this mode, each _Part_ and it's contents are being displayed as a mini-timeline and it's width is normalized to fit the screen, unless it's shorter than 30 seconds, in which case it will be scaled down accordingly. + +![List View Mode](/img/docs/main/list_view.png) + +In this mode, the focus is on the "main" Piece of the Part. Additional _Lower-Third_ Pieces will be displayed on top of the main Piece. Infinite _Lower-Third_ Pieces and all other content can be displayed to the right of the mini-timeline as a set of indicators, one per every Layer. Clicking on those indicators will show a pop-up with the Pieces so that they can be investigated using _hover-scrub_. Indicators can be also shown for Ad-Libs assigned to a Part, for easier discovery by the User. Which Layers should be shown in the columns can be decided in the [Settings ● Layers](../configuration/settings-view.md#show-style) area. A special, larger indicator is reserved for the Script piece, which can be useful to display so-called _out-words_. + +If a Part has an _in-transition_ Piece, it will be displayed to the left of the Part's Take Point. + +This view is designed to be used in productions that are mixing pre-planned and timed segments with more free-flowing production or mixing short live in-camera links with longer pre-produced clips, while trying to keep as much of the show in the viewport as possible, at the expense of hiding some of the content from the User and the _duration_ of the Part on screen having no bearing on it's _width_. This mode also allows Sofie to visualize content _beyond_ the planned duration of a Part. + +:::info +The Segment header area also shows the expected (planned) durations for all the Parts and will also show which Parts are sharing timing in a timing group using a *⌊* symbol in the place of a counter. +::: + +All user interactions work in the Storyboard and List View mode the same as in Timeline mode: Takes, AdLibs, Holds and moving the [Next Point](#next-point) around the Rundown. + +### Segment Header Countdowns + +![Each Segment has two clocks - the Segment Time Budget and a Segment Countdown](/img/docs/main/segment-budget-and-countdown.png) + + + +Clock on the left is an indicator of how much time has been spent playing Parts from that Segment in relation to how much time was planned for Parts in that Segment. If more time was spent playing than was planned for, this clock will turn red, there will be a **+** sign in front of it and will begin counting upwards. + + + +Clock on the right is a countdown to the beginning of a given segment. This takes into account unplayed time in the On Air Part and all unplayed Parts between the On Air Part and a given Segment. If there are no unplayed Parts between the On Air Part and the Segment, this counter will disappear. + + + +In the illustration above, the first Segment \(_Ny Sak_\) has been playing for 4 minutes and 25 seconds longer than it was planned for. The second segment \(_Direkte Strømstad\)_ is planned to play for 4 minutes and 40 seconds. There are 5 minutes and 46 seconds worth of content between the current On Air line \(which is in the first Segment\) and the second Segment. + +If you click on the Segment header countdowns, you can switch the _Segment Countdown_ to a _Segment OnAir Clock_ where this will show the time-of-day when a given Segment is expected to air. + +![Each Segment has two clocks - the Segment Time Budget and a Segment Countdown](/img/docs/main/features/segment-header-2.png) + +### Rundown Dividers + +When using a workflow and blueprints that combine multiple NRCS Rundowns into a single Sofie Rundown \(such as when using the "Ready To Air" functionality in AP ENPS\), information about these individual NRCS Rundowns will be inserted into the Rundown View at the point where each of these incoming Rundowns start. + +![Rundown divider between two NRCS Rundowns in a "Ready To Air" Rundown](/img/docs/main/rundown-divider.png) + +For reference, these headers show the Name, Planned Start and Planned Duration of the individual NRCS Rundown. + +### Shelf + +The shelf contains lists of AdLibs that can be played out. + +![Shelf](/img/docs/main/shelf.png) + +:::info +The Shelf can be opened by clicking the handle at the bottom of the screen, or by pressing the TAB key +::: + +### Shelf Layouts + +The _Rundown View_ and the _Detached Shelf View_ UI can have multiple concurrent layouts for any given Show Style. The automatic selection mechanism works as follows: + +1. select the first layout of the `RUNDOWN_LAYOUT` type, +2. select the first layout of any type, +3. use the default layout \(no additional filters\), in the style of `RUNDOWN_LAYOUT`. + +To use a specific layout in these views, you can use the `?layout=...` query string, providing either the ID of the layout or a part of the name. This string will then be matched against all available layouts for the Show Style, and the first matching will be selected. For example, for a layout called `Stream Deck layout`, to open the currently active rundown's Detached Shelf use: + +`http://localhost:3000/activeRundown/studio0/shelf?layout=Stream` + +The Detached Shelf view with a custom `DASHBOARD_LAYOUT` allows displaying the Shelf on an auxiliary touch screen, tablet or a Stream Deck device. A specialized Stream Deck view will be used if the view is opened on a device with hardware characteristics matching a Stream Deck device. + +The shelf also contains additional elements, not controlled by the Rundown View Layout. These include Buckets and the Inspector. If needed, these components can be displayed or hidden using additional url arguments: + +| Query parameter | Description | +| :---------------------------------- | :------------------------------------------------------------------------ | +| Default | Display the rundown layout \(as selected\), all buckets and the inspector | +| `?display=layout,buckets,inspector` | A comma-separated list of features to be displayed in the shelf | +| `?buckets=0,1,...` | A comma-separated list of buckets to be displayed | + +- `display`: Available values are: `layout` \(for displaying the Rundown Layout\), `buckets` \(for displaying the Buckets\) and `inspector` \(for displaying the Inspector\). +- `buckets`: The buckets can be specified as base-0 indices of the buckets as seen by the user. This means that `?buckets=1` will display the second bucket as seen by the user when not filtering the buckets. This allows the user to decide which bucket is displayed on a secondary attached screen simply by reordering the buckets on their main view. + +_Note: the Inspector is limited in scope to a particular browser window/screen, so do not expect the contents of the inspector to sync across multiple screens._ + +For the purpose of running the system in a studio environment, there are some additional views that can be used for various purposes: + +### Sidebar Panel + +#### Switchboard + +![Switchboard](/img/docs/main/switchboard.png) + +The Switchboard allows the producer to turn automation _On_ and _Off_ for sets of devices, as well as re-route automation control between devices - both with an active rundown and when no rundown is active in a [Studio](../concepts-and-architecture.md#system-organization-studio-and-show-style). + +The Switchboard panel can be accessed from the Rundown View's right-hand Toolbar, by clicking on the Switchboard button, next to the Support panel button. + +:::info +Technically, the switchboard activates and deactivates Route Sets. The Route Sets are grouped by Exclusivity Group. If an Exclusivity Group contains exactly two elements with the `ACTIVATE_ONLY` mode, the Route Sets will be displayed on either side of the switch. Otherwise, they will be displayed separately in a list next to an _Off_ position. See also [Settings ● Route sets](../configuration/settings-view#route-sets). +::: + +#### Media Status panel + +![Media Status panel](/img/docs/main/features/media-status-rundown-view-panel.png) + +This provides an overview of the status of the various Media assets required by +this Rundown for playback. You can sort these assets according to their playout +order, status, Source Layer Name and Piece Name by clicking on the table header. + +Note that while the _Filter..._ text field is focused, you will not be able to +use hotkey triggers for playout actions. You can remove the focus from the field +by pressing the Esc key. + +## Prompter View + +`/prompter/:studioId` + +![Prompter View](/img/docs/main/features/prompter-example.png) + +A fullscreen page which displays the prompter text for the currently active rundown. The prompter can be controlled and configured in various ways, see more at the [Prompter](prompter.md) documentation. If no Rundown is active in a given studio, the [Screensaver](sofie-views.mdx#screensaver) will be displayed. + +## Presenter View + +`/countdowns/:studioId/presenter` + +![Presenter View](/img/docs/main/features/presenter-screen-example.png) + +A fullscreen page, intended to be shown to the studio presenter. It displays countdown timers for the current and next items in the rundown. If no Rundown is active in a given studio, the [Screensaver](sofie-views.mdx#screensaver) will be shown. + +### Presenter View Overlay + +`/countdowns/:studioId/overlay` + +![Presenter View Overlay](/img/docs/main/features/presenter-screen-overlay-example.png) + +A fullscreen view with transparent background, intended to be shown to the studio presenter as an overlay on top of the produced PGM signal. It displays a reduced amount of the information from the regular [Presenter screen](sofie-views.mdx#presenter-view): the countdown to the end of the current Part, a summary preview \(type and name\) of the next item in the Rundown and the current time of day. If no Rundown is active it will show the name of the Studio. + +## Camera Position View + +`/countdowns/:studioId/camera` + +![Camera Position View](/img/docs/main/features/camera-view.jpg) + +A fullscreen view designed specifically for use on mobile devices or extra screens displaying a summary of the currently active Rundown, filtered for Parts containing Pieces matching particular Source Layers and Studio Labels. + +The Pieces are displayed as a Timeline, with the Pieces moving right-to-left as time progresses, and Parts being displayed from the current one being played up till the end of the Rundown. The closest (not necessarily _Next_) Part has a countdown timer in the top-right corner showing when it's expected to be Live. Each Part also has a Duration counter on the bottom-right. + +This view can be configured using query parameters: + +| Query parameter | Type | Description | Default | +| :-------------- | :--- | :---------- | :------ | +| `sourceLayerIds` | string | A comma-separated list of Source Layer IDs to be considered for display | _(show all)_ | +| `studioLabels` | string | A comma-separated list of Studio Labels (Piece `.content.studioLabel` values) to be considered for display | _(show all)_ | +| `fullscreen` | 0 / 1 | Should the view become fullscreen on the device on first user interaction | 0 | + +Example: [http://127.0.0.1/countdowns/studio0/camera?sourceLayerIds=camera0,dve0&studioLabels=1,KAM%201,K1,KAM1&fullscreen=1](http://127.0.0.1/countdowns/studio0/camera?sourceLayerIds=camera0,dve0&studioLabels=1,KAM%201,K1,KAM1&fullscreen=1) + +## Active Rundown View + +`/activeRundown/:studioId` + +![Active Rundown View](/img/docs/main/features/active-rundown-example.png) + +A page which automatically displays the currently active rundown. Can be useful for the producer to have on a secondary screen. + +## Active Rundown – Shelf + +`/activeRundown/:studioId/shelf` + +![Active Rundown Shelf](/img/docs/main/features/active-rundown-shelf-example.png) + +A view which automatically displays the currently active rundown, and shows the Shelf in full screen. Can be useful for the producer to have on a secondary screen. + +A shelf layout can be selected by modifying the query string, see [Shelf Layouts](#shelf-layouts). + +## Specific Rundown – Shelf + +`/rundown/:rundownId/shelf` + +Displays the shelf in fullscreen for a rundown + +## Screensaver + +When big screen displays \(like Prompter and the Presenter screen\) do not have any meaningful content to show, an animated screensaver showing the current time and the next planned show will be displayed. If no Rundown is upcoming, the Studio name will be displayed. + +![A screensaver showing the next scheduled show](/img/docs/main/features/next-scheduled-show-example.png) + +## System Status + +:::caution +Documentation for this feature is yet to be written. +::: + +System and devices statuses are displayed here. + +:::info +An API endpoint for the system status is also available under the URL `/health` +::: + +## Media Status View + +This view is a summary of all the media required for playback for Rundowns +present in this System. This view allows you to see if clips are ready for +playback or if they are still waiting to become available to be transferred +onto a playout system. + +![Media Status page](/img/docs/main/features/media-status.png) + +By default, the Media items are sorted according to their position in the +rundown, and the rundowns are in the same order as in the [Lobby View] +(#lobby-view). You can change the sorting order by clicking on the buttons in +the table header. + +Rundown View also has a panel that presents this information in the [context of the current Rundown](#media-status-panel). + +## Message Queue View + +:::caution +Documentation for this feature is yet to be written. +::: + +_Sofie Core_ can send messages to external systems \(such as metadata, as-run-logs\) while on air. + +These messages are retained for a period of time, and can be reviewed in this list. + +Messages that was not successfully sent can be inspected and re-sent here. + +## User Log View + +The user activity log contains a list of the user-actions that users have previously done. This is used in troubleshooting issues on-air. + +![User Log](/img/docs/main/features/user-log.png) + +### Columns, explained + +#### Execution time + +The execution time column displays **coreDuration** + **gatewayDuration** \(**timelineResolveDuration**\)": + +- **coreDuration** : The time it took for Core to execute the command \(ie start-of-command 🠺 stored-result-into-database\) +- **gatewayDuration** : The time it took for Playout Gateway to execute the timeline \(ie stored-result-into-database 🠺 timeline-resolved 🠺 callback-to-core\) +- **timelineResolveDuration**: The duration it took in TSR \(in Playout Gateway\) to resolve the timeline + +Important to note is that **gatewayDuration** begins at the exact moment **coreDuration** ends. +So **coreDuration + gatewayDuration** is the full time it took from beginning-of-user-action to the timeline-resolved \(plus a little extra for the final callback for reporting the measurement\). + +#### Action + +Describes what action the user did; e g pressed a key, clicked a button, or selected a meny item. + +#### Method + +The internal name in _Sofie Core_ of what function was called + +#### Status + +The result of the operation. "Success" or an error message. + +## Evaluations + +When a broadcast is done, users can input feedback about how the show went in an evaluation form. + +:::info +Evaluations can be configured to be sent to Slack, by setting the "Slack Webhook URL" in the [Settings View](../configuration/settings-view.md) under _Studio_. +::: + +## Settings View + +The [Settings View](../configuration/settings-view.md) is only available to users with the [Access Level](access-levels.md) set correctly. diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/system-health.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/system-health.md new file mode 100644 index 00000000000..11ab7046b4d --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/features/system-health.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 11 +--- + +# System Health + +## Legacy healthcheck + +There is a legacy `/health` endpoint used by NRK systems. Its use is being phased out and will eventually be replaced by the new prometheus endpoint. + +## Prometheus + +From version 1.49, there is a prometheus `/metrics` endpoint exposed from Sofie. The metrics exposed from here will increase over time as we find more data to collect. + +Because Sofie is comprised of multiple worker-threads, each metric has a `threadName` label indicitating which it is from. In many cases this field will not matter, but it is useful for the default process metrics, and if your installation has multiple studios defined. + +Each thread exposes some default nodejs process metrics. These are defined by the [`prom-client`](https://github.com/siimon/prom-client#default-metrics) library we are using, and are best described there. + +The current Sofie metrics exposed are: + +| name | type | description | +| ------------------------------------------ | ------- | ------------------------------------------------------------------ | +| sofie_meteor_ddp_connections_total | Gauge | Number of open ddp connections | +| sofie_meteor_publication_subscribers_total | Gauge | Number of subscribers on a Meteor publication (ignoring arguments) | +| sofie_meteor_jobqueue_queue_total | Counter | Number of jobs put into each worker job queues | +| sofie_meteor_jobqueue_success | Counter | Number of successful jobs from each worker | +| sofie_meteor_jobqueue_queue_errors | Counter | Number of failed jobs from each worker | diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/further-reading.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/further-reading.md new file mode 100644 index 00000000000..d78295d87b5 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/further-reading.md @@ -0,0 +1,59 @@ +--- +description: This guide has a lot of links. Here they are all listed by section. +--- + +# Further Reading + +## Getting Started + +- [Sofie's Concepts & Architecture](concepts-and-architecture.md) +- [Gateways](concepts-and-architecture.md#gateways) +- [Blueprints](concepts-and-architecture.md#blueprints) + +- Ask questions in the [Sofie Slack Channel](https://sofietv.slack.com/join/shared_invite/zt-2bfz8l9lw-azLeDB55cvN2wvMgqL1alA#/shared-invite/email) + +## Installation & Setup + +### Installing Sofie Core + +- [Windows install for Docker](https://hub.docker.com/editions/community/docker-ce-desktop-windows) +- [Linux install instructions for Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/) +- [Linux install instructions for Docker Compose](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-18-04) +- [Sofie Core Docker File Download](https://firebasestorage.googleapis.com/v0/b/gitbook-28427.appspot.com/o/assets%2F-LWRCgfY_-kYo9iX6UNy%2F-Lo5eWjgoVlRRDeFzLuO%2F-Lo5fLSSyM1eO6OXScew%2Fdocker-compose.yaml?alt=media&token=fc2fbe79-365c-4817-b270-e507c6a6e3c6) + +### Installing a Gateway + +#### Ingest Gateways and NRCS + +- [MOS Protocol Overview & Documentation](http://mosprotocol.com/) +- Information about ENPS on [The Associated Press' Website](https://www.ap.org/enps/support) +- Information about iNews: [Avid's Website](https://www.avid.com/products/inews/how-to-buy) + +**Google Spreadsheet Gateway** + +- [Demo Blueprints](https://github.com/SuperFlyTV/sofie-demo-blueprints/releases) on GitHub's website. +- [Example Rundown](https://docs.google.com/spreadsheets/d/1iyegRv5MxYYtlVu8uEEMkBYXsLL-71PAMrNW0ZfWRUw/edit?usp=sharing) provided by Sofie. +- [Google Sheets API](https://console.developers.google.com/apis/library/sheets.googleapis.com?) on the Google Developer website. + +### Additional Software & Hardware + +#### Installing CasparCG Server for Sofie + +- NRK's version of [CasparCG Server](https://github.com/nrkno/sofie-casparcg-server/releases) on GitHub. +- [Media Scanner](https://github.com/Sofie-Automation/sofie-casparcg-launcher/releases) on GitHub. +- [CasparCG Launcher](https://github.com/Sofie-Automation/sofie-casparcg-launcher) on GitHub. +- [Microsoft Visual C++ 2015 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=52685) on Microsoft's website. +- [Blackmagic Design's DeckLink Cards](https://www.blackmagicdesign.com/products/decklink/models) on Blackmagic Design's website. Check the [DeckLink cards](installation/installing-connections-and-additional-hardware/casparcg-server-installation.md#decklink-cards) section for compatibility. +- [Installing a DeckLink Card](https://documents.blackmagicdesign.com/UserManuals/DesktopVideoManual.pdf) as a PDF. +- [Blackmagic Design 'Desktop Video' Driver Download](https://www.blackmagicdesign.com/support/family/capture-and-playback) on Blackmagic Design's website. +- [CasparCG Server Configuration Validator](https://casparcg.net/validator/) + +**Additional Resources** + +- Viz graphics through MSE, info on the [Vizrt](https://www.vizrt.com/) website. +- Information about the [Blackmagic Design's HyperDeck](https://www.blackmagicdesign.com/products/hyperdeckstudio) + +## FAQ, Progress, and Issues + +- [MIT Licence](https://opensource.org/licenses/MIT) +- [Releases and Issues on GitHub](https://github.com/Sofie-Automation/Sofie-TV-automation/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3ARelease) diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/_category_.json b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/_category_.json new file mode 100644 index 00000000000..2f3c7f2a9f6 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Installation", + "position": 3 +} \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/initial-sofie-core-setup.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/initial-sofie-core-setup.md new file mode 100644 index 00000000000..c0672b3e55d --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/initial-sofie-core-setup.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 3 +--- + +# Initial Sofie Core Setup + +#### Prerequisites + +* [Installed and running _Sofie Core_](installing-sofie-server-core.md) + +Once _Sofie Core_ has been installed and is running you can begin setting it up. The first step is to navigate to the _Settings page_. Please review the [Sofie Access Level](../features/access-levels.md) page for assistance getting there. + +To upgrade to a newer version or installation of new blueprints, Sofie needs to run its "Upgrade database" procedure to migrate data and pre-fill various settings. You can do this by clicking the _Upgrade Database_ button in the menu. + +![Update Database Section of the Settings Page](/img/docs/getting-started/settings-page-full-update-db-r47.png) + +Fill in the form as prompted and continue by clicking _Run Migrations Procedure_. Sometimes you will need to go through multiple steps before the upgrade is finished. + +Next, you will need to add some [Blueprints](installing-blueprints.md) and add [Gateways](installing-a-gateway/intro.md) to allow _Sofie_ to interpret rundown data and then play out things. + +![Initial Studio Settings Page](/img/docs/getting-started/settings-page-initial-studio.png) + +Next, you will need to add some [Blueprints](installing-blueprints) and add [Gateways](installing-a-gateway/intro) to allow _Sofie_ to interpret rundown data and then play out things. diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/_category_.json b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/_category_.json new file mode 100644 index 00000000000..7fa55d484d6 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Installing a Gateway", + "position": 5 +} \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/intro.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/intro.md new file mode 100644 index 00000000000..03bc8a53396 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/intro.md @@ -0,0 +1,25 @@ +--- +sidebar_label: Introduction +sidebar_position: 1 +--- +# Introduction: Installing a Gateway + +#### Prerequisites + +* [Installed and running Sofie Core](../installing-sofie-server-core.md) + +The _Sofie Core_ is the primary application for managing the broadcast, but it doesn't play anything out on it's own. A Gateway will establish the connection from _Sofie Core_ to other pieces of hardware or remote software. A basic setup may include the [Spreadsheet Gateway](rundown-or-newsroom-system-connection/installing-sofie-with-google-spreadsheet-support.md) which will ingest a rundown from Google Sheets then, use the [Playout Gateway](playout-gateway.md) send commands to a CasparCG Server graphics playout, an ATEM vision mixer, and / or the [Sisyfos audio controller](https://github.com/olzzon/sisyfos-audio-controller). + +Installing a gateway is a two part process. To begin, you will [add the required Blueprints](../installing-blueprints.md), or mini plug-in programs, to _Sofie Core_ so it can manipulate the data from the Gateway. Then you will install the Gateway itself. Each Gateway follows a similar installation pattern but, each one does differ slightly. The links below will help you navigate to the correct Gateway for the piece of hardware / software you are using. + +### Rundown & Newsroom Gateways + +* [Google Spreadsheet Gateway](rundown-or-newsroom-system-connection/installing-sofie-with-google-spreadsheet-support.md) +* [iNEWS Gateway](rundown-or-newsroom-system-connection/inews-gateway.md) +* [MOS Gateway](rundown-or-newsroom-system-connection/mos-gateway.md) + +### Playout & Media Manager Gateways + +* [Playout Gateway](playout-gateway.md) +* [Media Manager](../media-manager.md) + diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/playout-gateway.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/playout-gateway.md new file mode 100644 index 00000000000..0fd5f476267 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/playout-gateway.md @@ -0,0 +1,6 @@ +--- +sidebar_position: 3 +--- +# Playout Gateway + +The _Playout Gateway_ handles interacting external pieces of hardware or software by sending commands that will playout rundown content. This gateway used to be a separate installation but it has since been moved into the main _Sofie Core_ component. diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json new file mode 100644 index 00000000000..b4c4ffc34d5 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Rundown or Newsroom System Connection", + "position": 4 +} \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md new file mode 100644 index 00000000000..48659251a65 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md @@ -0,0 +1,12 @@ +# iNEWS Gateway + +The iNEWS Gateway communicates with an iNEWS system to ingest and remain in sync with a rundown. + +### Installing iNEWS for Sofie + +The iNEWS Gateway allows you to create rundowns from within iNEWS and sync them with the _Sofie Core_. The rundowns will update in real time and any changes made will be seen from within your Playout Timeline. + +The setup for the iNEWS Gateway is already in the Docker Compose file you downloaded earlier. Remove the _\#_ symbol from the start of the line labeled `image: tv2/inews-ftp-gateway:develop` and add a _\#_ to the other ingest gateway that was being used. + +Although the iNEWS Gateway is available free of charge, an iNEWS license is not. Visit [Avid's website](https://www.avid.com/products/inews/how-to-buy) to find an iNEWS reseller that handles your geographic area. + diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/installing-sofie-with-google-spreadsheet-support.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/installing-sofie-with-google-spreadsheet-support.md new file mode 100644 index 00000000000..8cdd2ed637c --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/installing-sofie-with-google-spreadsheet-support.md @@ -0,0 +1,46 @@ +# Google Spreadsheet Gateway + +The Spreadsheet Gateway is an application for piping data between Sofie Core and Spreadsheets on Google Drive. + +### Example Blueprints for Spreadsheet Gateway + +To begin with, you will need to install a set of Blueprints that can handle the data being sent from the _Gateway_ to _Sofie Core_. Download the `demo-blueprints-r*.zip` file containing the blueprints you need from the [Demo Blueprints GitHub Repository](https://github.com/SuperFlyTV/sofie-demo-blueprints/releases). It is recommended to choose the newest release but, an older _Sofie Core_ version may require a different Blueprint version. The _Rundown page_ will warn you about any issue and display the desired versions. + +Instructions on how to install any Blueprint can be found in the [Installing Blueprints](../../installing-blueprints.md) section from earlier. + +### Spreadsheet Gateway Configuration + +If you are using the Docker version of Sofie, then the Spreadsheet Gateway will come preinstalled. For those who are not, please follow the [instructions listed on the GitHub page](https://github.com/SuperFlyTV/spreadsheet-gateway) labeled _Installation \(for developers\)._ + +Once the Gateway has been installed, you can navigate to the _Settings page_ and check the newly added Gateway is listed as _Spreadsheet Gateway_ under the _Devices section_. + +Before you select the Device, you want to add it to the current _Studio_ you are using. Select your current Studio from the menu and navigate to the _Attached Devices_ option. Click the _+_ icon and select the Spreadsheet Gateway. + +Now you can select the _Device_ from the _Devices menu_ and click the link provided to enable your Google Drive API to send files to the _Sofie Core_. The page that opens will look similar to the image below. + +![Nodejs Quickstart page](/img/docs/installation/installing-a-gateway/rundown-or-newsroom-system-connection/nodejs-quickstart.png) +xx +Make sure to follow the steps in **Create a project and enable the API** and enable the **Google Drive API** as well as the **Google Sheets API**. Your "APIs and services" Dashboard should now look as follows: + +![APIs and Services Dashboard](/img/docs/installation/installing-a-gateway/rundown-or-newsroom-system-connection/apis-and-services-dashboard.png) + +Now follow the steps in **Create credentials** and make sure to create an **OAuth Client ID** for a **Desktop App** and download the credentials file. + +![Create Credentials page](/img/docs/installation/installing-a-gateway/rundown-or-newsroom-system-connection/create-credentials.png) + +Use the button to download the configuration to a file and navigate back to _Sofie Core's Settings page_. Select the Spreadsheet Gateway, then click the _Browse_ button and upload the configuration file you just downloaded. A new link will appear to confirm access to your google drive account. Select the link and in the new window, select the Google account you would like to use. Currently, the Sofie Core Application is not verified with Google so you will need to acknowledge this and proceed passed the unverified page. Click the _Advanced_ button and then click _Go to QuickStart \( Unsafe \)_. + +After navigating through the prompts you are presented with your verification code. Copy this code into the input field on the _Settings page_ and the field should be removed. A message confirming the access token was saved will appear. + +You can now navigate to your Google Drive account and create a new folder for your rundowns. It is important that this folder has a unique name. Next, navigate back to _Sofie Core's Settings page_ and add the folder name to the appropriate input. + +The indicator should now read _Good, Watching folder 'Folder Name Here'_. Now you just need an example rundown.[ Navigate to this Google Sheets file](https://docs.google.com/spreadsheets/d/1iyegRv5MxYYtlVu8uEEMkBYXsLL-71PAMrNW0ZfWRUw/edit?usp=sharing) and select the _File_ menu and then select _Make a copy_. In the popup window, select _My Drive_ and then navigate to and select the rundowns folder you created earlier. + +At this point, one of two things will happen. If you have the Google Sheets API enabled, this is different from the Google Drive API you enabled earlier, then the Rundown you just copied will appear in the Rundown page and is accessible. The other outcome is the Spreadsheet Gateway status reads _Unknown, Initializing..._ which most likely means you need to enable the Google Sheets API. Navigate to the[ Google Sheets API Dashboard with this link](https://console.developers.google.com/apis/library/sheets.googleapis.com?) and click the _Enable_ button. Navigate back to _Sofie's Settings page_ and restart the Spreadsheet Gateway. The status should now read, _Good, Watching folder 'Folder Name Here'_ and the rundown will appear in the _Rundown page_. + +### Further Reading + +- [Demo Blueprints](https://github.com/SuperFlyTV/sofie-demo-blueprints/) GitHub Page for Developers +- [Example Rundown](https://docs.google.com/spreadsheets/d/1iyegRv5MxYYtlVu8uEEMkBYXsLL-71PAMrNW0ZfWRUw/edit?usp=sharing) provided by Sofie. +- [Google Sheets API](https://console.developers.google.com/apis/library/sheets.googleapis.com?) on the Google Developer website. +- [Spreadsheet Gateway](https://github.com/SuperFlyTV/spreadsheet-gateway) GitHub Page for Developers diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md new file mode 100644 index 00000000000..7c9c6fd5c44 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md @@ -0,0 +1,17 @@ +--- +sidebar_position: 1 +--- +# Rundown & Newsroom Systems + +Sofie Core doesn't talk directly to the newsroom systems, but instead via one of the Gateways. + +The Google Spreadsheet Gateway, iNEWS Gateway, and the MOS \([Media Object Server Communications Protocol](http://mosprotocol.com/)\) Gateway which can handle interacting with any system that communicates via MOS. + +### Further Reading + +* [MOS Protocol Overview & Documentation](http://mosprotocol.com/) +* [iNEWS on Avid's Website](https://www.avid.com/products/inews/how-to-buy) +* [ENPS on The Associated Press' Website](https://www.ap.org/enps/support) + + + diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md new file mode 100644 index 00000000000..8a2a60145c8 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md @@ -0,0 +1,9 @@ +# MOS Gateway + +The MOS Gateway communicates with a device that supports the [MOS protocol](http://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOS-Protocol-2.8.4-Current.htm) to ingest and remain in sync with a rundown. It can connect to any editorial system \(NRCS\) that uses version 2.8.4 of the MOS protocol, such as ENPS, and sync their rundowns with the _Sofie Core_. The rundowns are kept updated in real time and any changes made will be seen in the Sofie GUI. + +The setup for the MOS Gateway is handled in the Docker Compose in the [Quick Install](../../installing-sofie-server-core.md) page. + +One thing to note if managing the mos-gateway manually: It needs a few ports open \(10540, 10541\) for MOS-messages to be pushed to it from the NCS. + + diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-blueprints.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-blueprints.md new file mode 100644 index 00000000000..34796bbb1da --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-blueprints.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 4 +--- + +# Installing Blueprints + +#### Prerequisites + +- [Installed and running Sofie Core](installing-sofie-server-core.md) +- [Initial Sofie Core Setup](initial-sofie-core-setup.md) + +Blueprints are little plug-in programs that runs inside _Sofie_. They are the logic that determines how _Sofie_ interacts with rundowns, hardware, and media. + +Blueprints are custom scripts that you create yourself \(or download an existing one\). There are a set of example Blueprints for the Spreadsheet Gateway available for use here: [https://github.com/SuperFlyTV/sofie-demo-blueprints](https://github.com/SuperFlyTV/sofie-demo-blueprints). + +To begin installing any Blueprint, navigate to the _Settings page_. Getting there is covered in the [Access Levels](../features/access-levels.md) page. + +![The Settings Page](/img/docs/getting-started/settings-page.jpg) + +To upload a new blueprint, click the _+_ icon next to Blueprints menu option. Select the newly created Blueprint and upload the local blueprint JS file. You will get a confirmation if the installation was successful. + +There are 3 types of blueprints: System, Studio and Show Style: + +### System Blueprint + +_System Blueprints handles some basic functionality on how the Sofie system will operate._ + +After you've uploaded the your system-blueprint js-file, click _Assign_ in the blueprint-page to assign it as system-blueprint. + +### Studio Blueprint + +_Studio Blueprints determine how Sofie will interact with the hardware in your studio._ + +After you've uploaded the your studio-blueprint js-file, navigate to a Studio in the settings and assign the new Blueprint to it \(under the label _Blueprint_ \). + +After having installed the Blueprint, the Studio's baseline will need to be reloaded. On the Studio page, click the button _Reload Baseline_. This will also be needed whenever you have changed any settings. + +### Show Style Blueprint + +_Show Style Blueprints determine how your show will look / feel._ + +After you've uploaded the your show-style-blueprint js-file, navigate to a Show Style in the settings and assign the new Blueprint to it \(under the label _Blueprint_ \). + +### Further Reading + +- [Blueprints Supporting the Spreadsheet Gateway](https://github.com/SuperFlyTV/sofie-demo-blueprints) diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/README.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/README.md new file mode 100644 index 00000000000..4d35fb277dc --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/README.md @@ -0,0 +1,35 @@ +# Additional Software & Hardware + +#### Prerequisites + +* [Installed and running Sofie Core](../installing-sofie-server-core.md) +* [Installed Playout Gateway](../installing-a-gateway/playout-gateway.md) +* [Installed and configured Studio Blueprints](../installing-blueprints.md#installing-a-studio-blueprint) + +The following pages are broken up by equipment type that is supported by Sofie's Gateways. + +## Playout & Recording +* [CasparCG Graphics and Video Server](casparcg-server-installation.md) - _Graphics / Playout / Recording_ +* [Blackmagic Design's HyperDeck](https://www.blackmagicdesign.com/products/hyperdeckstudio) - _Recording_ +* [Quantel](http://www.quantel.com) Solutions - _Playout_ +* [Vizrt](https://www.vizrt.com/) Graphics Solutions - _Graphics / Playout_ + +## Vision Mixers +* [Blackmagic's ATEM](https://www.blackmagicdesign.com/products/atem) hardware vision mixers +* [vMix](https://www.vmix.com/) software vision mixer \(coming soon\) + +## Audio Mixers +* [Sisyfos](https://github.com/olzzon/sisyfos-audio-controller) audio controller +* [Lawo sound mixers_,_](https://www.lawo.com/applications/broadcast-production/audio-consoles.html) _using emberplus protocol_ +* Generic OSC \(open sound control\) + +## PTZ Cameras +* [Panasonic PTZ](https://pro-av.panasonic.net/en/products/ptz_camera_systems.html) cameras + +## Lights +* [Pharos](https://www.pharoscontrols.com/) light control + +## Other +* Generic OSC \(open sound control\) +* Generic HTTP requests \(to control http-REST interfaces\) +* Generic TCP-socket diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/_category_.json b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/_category_.json new file mode 100644 index 00000000000..d3e1e8979e3 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Installing Connections and Additional Hardware", + "position": 6 +} \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md new file mode 100644 index 00000000000..f5b845d77ef --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md @@ -0,0 +1,224 @@ +--- +title: Installing CasparCG Server for Sofie +description: CasparCG Server +--- + +# Installing CasparCG Server for Sofie + +Although CasparCG Server is an open source program that is free to use for both personal and cooperate applications, the hardware needed to create and execute high quality graphics is not. You can get a preview running without any additional hardware but, it is not recommended to use CasparCG Server for production in this manner. To begin, you will install the CasparCG Server on your machine then add the additional configuration needed for your setup of choice. + +## Installing the CasparCG Server + +To begin, download the latest release of [CasparCG Server from GitHub](https://github.com/casparcg/server/releases). While some Sofie users have their own fork of CasparCG, we recommend the official builds. + +Once downloaded, extract the files into a folder and navigate inside. This folder contains your CasparCG Server Configuration file, `casparcg.config`, and your CasparCG Server executable, `casparcg.exe`. + +How you will configure the CasparCG Server will depend on the number of DeckLink cards your machine contains. The first subsection for each CasparCG Server setup, labeled _Channels_, will contain the unique portion of the configuration. The following is the majority of the configuration file that will be consistent between setups. + +```markup + + + debug + + + + media/ + log/ + data/ + template/ + + secret + + + + + + 5250 + AMCP + + + + + localhost + 8000 + + + +``` + +One additional note, the Server does require the configuration file be named `casparcg.config`. + +### Installing the CasparCG Launcher + +You can launch both of your CasparCG applications with the [CasparCG Launcher.](https://github.com/Sofie-Automation/sofie-casparcg-launcher) Download the `.exe` file in the latest release and once complete, move the file to the same folder as your `casparcg.exe` file. + +## Configuring Windows + +### Required Software + +Windows will require you install [Microsoft's Visual C++ 2015 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=52685) to run the CasparCG Server properly. Before downloading the redistributable, please ensure it is not already installed on your system. Open your programs list and in the popup window, you can search for _C++_ in the search field. If _Visual C++ 2015_ appears, you do not need install the redistributable. + +If you need to install redistributable then, navigate to [Microsoft's website](https://www.microsoft.com/en-us/download/details.aspx?id=52685) and download it from there. Once downloaded, you can run the `.exe` file and follow the prompts. + +## Hardware Recommendations + +Although CasparCG Server can be run on some lower end hardware, it is only recommended to do so for non-production uses. Below is a table of the minimum and preferred specs depending on what type of system you are using. + +| System Type | Min CPU | Pref CPU | Min GPU | Pref GPU | Min Storage | Pref Storage | +| :------------ | :--------------- | :------------------------ | :------- | :----------- | :------------- | :------------- | +| Development | i5 Gen 6i7 Gen 6 | GTX 1050 | GTX 1060 | GTX 1060 | NVMe SSD 500gb | | +| Prod, 1 Card | i7 Gen 6 | i7 Gen 7 | GTX 1060 | GTX 1070 | NVMe SSD 500gb | NVMe SSD 500gb | +| Prod, 2 Cards | i9 Gen 8 | i9 Gen 10 Extreme Edition | RTX 2070 | Quadro P4000 | Dual Drives | Dual Drives | + +For _dual drives_, it is recommended to use a smaller 250gb NVMe SSD for the operating system. Then a faster 1tb NVMe SSD for the CasparCG Server and media. It is also recommended to buy a drive with about 40% storage overhead. This is for SSD p~~e~~rformance reasons and Sofie will warn you about this if your drive usage exceeds 60%. + +### DeckLink Cards + +There are a few SDI cards made by Blackmagic Design that are supported by CasparCG. The base model, with four bi-directional input and outputs, is the [Duo 2](https://www.blackmagicdesign.com/products/decklink/techspecs/W-DLK-31). If you need additional channels, use the [Quad 2](https://www.blackmagicdesign.com/products/decklink/techspecs/W-DLK-30) which supports eight bi-directional inputs and outputs. Be aware the BNC connections are not the standard BNC type. B&H offers [Mini BNC to BNC connecters](https://www.bhphotovideo.com/c/product/1462647-REG/canare_cal33mb018_mini_rg59_12g_sdi_4k.html). Finally, for 4k support, use the [8K Pro](https://www.blackmagicdesign.com/products/decklink/techspecs/W-DLK-34) which has four bi-directional BNC connections and one reference connection. + +Here is the Blackmagic Design PDF for [installing your DeckLink card \( Desktop Video Device \).](https://documents.blackmagicdesign.com/UserManuals/DesktopVideoManual.pdf) + +Once the card in installed in your machine, you will need to download the controller from Blackmagic's website. Navigate to [this support page](https://www.blackmagicdesign.com/support/family/capture-and-playback), it will only display Desktop Video Support, and in the _Latest Downloads_ column download the most recent version of _Desktop Video_. Before installing, save your work because Blackmagic's installers will force you to restart your machine. + +Once booted back up, you should be able to launch the Desktop Video application and see your DeckLink card. + +![Blackmagic Design's Desktop Video Application](/img/docs/installation/installing-connections-and-additional-hardware/desktop-video.png) + +Click the icon in the center of the screen to open the setup window. Each production situation will very in frame rate and resolution so go through the settings and set what you know. Most things are set to standards based on your region so the default option will most likely be correct. + +![Desktop Video Settings](/img/docs/installation/installing-connections-and-additional-hardware/desktop-video-settings.png) + +If you chose a DeckLink Duo, then you will also need to set SDI connectors one and two to be your outputs. + +![DeckLink Duo SDI Output Settings](/img/docs/installation/installing-connections-and-additional-hardware/decklink_duo_card.png) + +## Hardware-specific Configurations + +### Preview Only \(Basic\) + +A preview only version of CasparCG Server does not lack any of the features of a production version. It is called a _preview only_ version because the standard outputs on a computer, without a DeckLink card, do not meet the requirements of a high quality broadcast graphics machine. It is perfectly suitable for development though. + +#### Required Hardware + +No additional hardware is required, just the computer you have been using to follow this guide. + +#### Configuration + +The default configuration will give you one preview window. No additional changes need to be made. + +### Single DeckLink Card \(Production Minimum\) + +#### Required Hardware + +To be production ready, you will need to output an SDI or HDMI signal from your production machine. CasparCG Server supports Blackmagic Design's DeckLink cards because they provide a key generator which will aid in keeping the alpha and fill channels of your graphics in sync. Please review the [DeckLink Cards](casparcg-server-installation.md#decklink-cards) section of this page to choose which card will best fit your production needs. + +#### Configuration + +You will need to add an additional consumer to your`caspar.config` file to output from your DeckLink card. After the screen consumer, add your new DeckLink consumer like so. + +```markup + + + 1080i5000 + stereo + + + 1 + true + + + + + 1 + 1 + true + stereo + normal + external_separate_device + false + 3 + + + + + +``` + +You may no longer need the screen consumer. If so, you can remove it and all of it's contents. This will dramatically improve overall performance. + +### Multiple DeckLink Cards \(Recommended Production Setup\) + +#### Required Hardware + +For a preferred production setup you want a minimum of two DeckLink Duo 2 cards. This is so you can use one card to preview your media, while your second card will support the program video and audio feeds. For CasparCG Server to recognize both cards, you need to add two additional channels to the `caspar.config` file. + +```markup + + + 1080i5000 + stereo + + + 1 + true + + + + + 1 + 1 + true + stereo + normal + external_separate_device + false + 3 + + + + + 2 + 2 + true + stereo + normal + external_separate_device + false + 3 + + + + + +``` + +### Validating the Configuration File + +Once you have setup the configuration file, you can use an online validator to check and make sure it is setup correctly. Navigate to the [CasparCG Server Config Validator](https://casparcg.net/validator/) and paste in your entire configuration file. If there are any errors, they will be displayed at the bottom of the page. + +### Launching the Server + +Launching the Server is the same for each hardware setup. This means you can run `casparcg-launcher.exe` and the server and media scanner will start. There will be two additional warning from Windows. The first is about the EXE file and can be bypassed by selecting _Advanced_ and then _Run Anyways_. The second menu will be about CasparCG Server attempting to access your firewall. You will need to allow access. + +A window will open and display the status for the server and scanner. You can start, stop, and/or restart the server from here if needed. An additional window should have opened as well. This is the main output of your CasparCG Server and will contain nothing but a black background for now. If you have a DeckLink card installed, its output will also be black. + +## Connecting Sofie to the CasparCG Server + +Now that your CasparCG Server software is running, you can connect it to the _Sofie Core_. Navigate back to the _Settings page_ and in the menu, select the _Playout Gateway_. If the _Playout Gateway's_ status does not read _Good_, then please review the [Installing and Setting up the Playout Gateway](../installing-a-gateway/playout-gateway.md) section of this guide. + +Under the Sub Devices section, you can add a new device with the _+_ button. Then select the pencil \( edit \) icon on the new device to open the sub device's settings. Select the _Device Type_ option and choose _CasparCG_ from the drop down menu. Some additional fields will be added to the form. + +The _Host_ and _Launcher Host_ fields will be _localhost_. The _Port_ will be CasparCG's TCP port responsible for handling the AMCP commands. It defaults to 5052 in the `casparcg.config` file. The _Launcher Port_ will be the CasparCG Launcher's port for handling HTTP requests. It will default to 8005 and can be changed in the _Launcher's settings page_. Once all four fields are filled out, you can click the check mark to save the device. + +In the _Attached Sub Devices_ section, you should now see the status of the CasparCG Server. You may need to restart the Playout Gateway if the status is _Bad_. + +## Further Reading + +- [CasparCG Server Releases](https://github.com/nrkno/sofie-casparcg-server/releases) on GitHub. +- [Media Scanner Releases](https://github.com/nrkno/sofie-media-scanner/releases) on GitHub. +- [CasparCG Launcher](https://github.com/Sofie-Automation/sofie-casparcg-launcher) on GitHub. +- [Microsoft Visual C++ 2015 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=52685) on Microsoft's website. +- [Blackmagic Design's DeckLink Cards](https://www.blackmagicdesign.com/products/decklink/models) on Blackmagic's website. Check the [DeckLink cards](casparcg-server-installation.md#decklink-cards) section for compatibility. +- [Installing a DeckLink Card](https://documents.blackmagicdesign.com/UserManuals/DesktopVideoManual.pdf) as a PDF. +- [Desktop Video Download Page](https://www.blackmagicdesign.com/support/family/capture-and-playback) on Blackmagic's website. +- [CasparCG Configuration Validator](https://casparcg.net/validator/) diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md new file mode 100644 index 00000000000..9833fb45a43 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md @@ -0,0 +1,35 @@ +# Adding FFmpeg and FFprobe to your PATH on Windows + +Some parts of Sofie (specifically the Package Manager) require that [`FFmpeg`](https://www.ffmpeg.org/) and [`FFprobe`](https://ffmpeg.org/ffprobe.html) be available in your `PATH` environment variable. This guide will go over how to download these executables and add them to your `PATH`. + +### Installation + +1. `FFmpeg` and `FFprobe` can be downloaded from the [FFmpeg Downloads page](https://ffmpeg.org/download.html) under the "Get packages & executable files" heading. At the time of writing, there are two sources of Windows builds: `gyan.dev` and `BtbN` -- either one will work. +2. Once downloaded, extract the archive to some place permanent such as `C:\Program Files\FFmpeg`. + - You should end up with a `bin` folder inside of `C:\Program Files\FFmpeg` and in that `bin` folder should be three executables: `ffmpeg.exe`, `ffprobe.exe`, and `ffplay.exe`. +3. Open your Start Menu and type `path`. An option named "Edit the system environment variables" should come up. Click on that option to open the System Properties menu. + + ![Start Menu screenshot](/img/docs/edit_system_environment_variables.jpg) + +4. In the System Properties menu, click the "Environment Variables..." button at the bottom of the "Advanced" tab. + + ![System Properties screenshot](/img/docs/system_properties.png) + +5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for \" heading. + + ![Environment Variables screenshot](/img/docs/environment_variables.png) + +6. In the window that pops up when you click "Edit...", click "New" and enter the path to the `bin` folder you extracted earlier. Then, click OK to add it. + + ![Edit environment variable screenshot](/img/docs/edit_path_environment_variable.png) + +7. Click "OK" to close the Environment Variables window, and then click "OK" again to close the + System Properties window. +8. Verify that it worked by opening a Command Prompt and executing the following commands: + + ```cmd + ffmpeg -version + ffprobe -version + ``` + + If you see version output from both of those commands, then you are all set! If not, double check the paths you entered and try restarting your computer. diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/vision-mixers.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/vision-mixers.md new file mode 100644 index 00000000000..1515b08840f --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/vision-mixers.md @@ -0,0 +1,14 @@ +# Configuring Vision Mixers + +## ATEM – Blackmagic Design + +The [Playout Gateway](../installing-a-gateway/playout-gateway.md) supports communicating with the entire line up of Blackmagic Design's ATEM vision mixers. + +### Connecting Sofie + +Once your ATEM is properly configured on the network, you can add it as a device to the Sofie Core. To begin, navigate to the _Settings page_ and select the _Playout Gateway_ under _Devices_. Under the _Sub Devices_ section, you can add a new device with the _+_ button. Edit it the new device with the pencil \( edit \) icon add the host IP and port for your ATEM. Once complete, you should see your ATEM in the _Attached Sub Devices_ section with a _Good_ status indicator. + +### Additional Information + +Sofie does not support connecting to a vision mixer hardware panels. All interacts with the vision mixers must be handled within a Rundown. + diff --git a/packages/documentation/docs/user-guide/installation/installing-input-gateway.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-input-gateway.md similarity index 100% rename from packages/documentation/docs/user-guide/installation/installing-input-gateway.md rename to packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-input-gateway.md diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-package-manager.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-package-manager.md new file mode 100644 index 00000000000..a38c3cc2285 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-package-manager.md @@ -0,0 +1,210 @@ +--- +sidebar_position: 7 +--- + +# Installing Package Manager + +### Prerequisites + +- [Installed and running Sofie Core](installing-sofie-server-core.md) +- [Initial Sofie Core Setup](initial-sofie-core-setup.md) +- [Installed and configured Demo Blueprints](https://github.com/SuperFlyTV/sofie-demo-blueprints) +- [Installed, configured, and running CasparCG Server](installing-connections-and-additional-hardware/casparcg-server-installation.md) (Optional) +- [`FFmpeg` and `FFprobe` available in `PATH`](installing-connections-and-additional-hardware/ffmpeg-installation.md) + +Package Manager is used by Sofie to copy, analyze, and process media files. It is what powers Sofie's ability to copy media files to playout devices, to know when a media file is ready for playout, and to display details about media files in the rundown view such as scene changes, black frames, freeze frames, and more. + +Although Package Manager can be used to copy any kind of file to/from a wide array of devices, we'll be focusing on a basic CasparCG Server Server setup for this guide. + +:::caution + +Sofie supports only one Package Manager running for a Studio. Attaching more at a time will result in weird behaviour due to them fighting over reporting the statuses of packages. +If you feel like you need multiple, then you likely want to run Package Manager in the distributed setup instead. + +::: + +:::caution + +The Package Manager worker process is primarily tested on Windows only. It does run on Linux (without support for network shares), but has not been extensively tested. + +::: + +## Installation For Development (Quick Start) + +Package Manager is a suite of standalone applications, separate from _Sofie Core_. This guide assumes that Package Manager will be running on the same computer as _CasparCG Server_ and _Sofie Core_, as that is the fastest way to set up a demo. To get all parts of _Package Manager_ up and running quickly, execute these commands: + +```bash +git clone https://github.com/Sofie-Automation/sofie-package-manager.git +cd sofie-package-manager +yarn install +yarn build +yarn start:single-app +``` + +On first startup, Package Manager will exit with the following message: + +``` +Not setup yet, exiting process! +To setup, go into Core and add this device to a Studio +``` + +This first run is necessary to get the Package Manager device registered with _Sofie Core_. We'll restart Package Manager later on in the [Configuration](#configuration) instructions. + +## Installation In Production + +Only one Package Manager can be running for a Sofie Studio. If you reached this point thinking of deploying multiple, you will want to follow the distributed setup. + +### Simple Setup + +For setups where you only need to interact with CasparCG on one machine, we provide pre-built executables for Windows (x64) systems. These can be found on the [Releases](https://github.com/Sofie-Automation/sofie-package-manager/releases) GitHub repository page for Package Manager. For a minimal installation, you'll need the `package-manager-single-app.exe` and `worker.exe`. Put them in a folder of your choice. You can also place `ffmpeg.exe` and `ffprobe.exe` alongside them, if you don't want to make them available in `PATH`. + +```bash +package-manager-single-app.exe --coreHost= --corePort= --deviceId= --deviceToken= +``` + +Package Manager can be launched from [CasparCG Launcher](./installing-connections-and-additional-hardware/casparcg-server-installation.md#installing-the-casparcg-launcher) alongside Caspar-CG. This will make management and log collection easier on a production Video Server. + +You can see a list of available options by running `package-manager-single-app.exe --help`. + +In some cases, you will need to run the HTTP proxy server component elsewhere so that it can be accessed from your Sofie UI machines. +For this, you can run the `sofietv/package-manager-http-server` docker image, which exposes its service on port 8080 and expects `/data/http-server` to be persistent storage. +When configuring the http proxy server in Sofie, you may need to follow extra configuration steps for this to work as expected. + +### Distributed Setup + +For setups where you need to interact with multiple CasparCG machines, or want a more resilient/scalable setup, Package Manager can be partially deployed in Docker, with just the workers running on each CasparCG machine. + +An example `docker-compose` of the setup is as follows: + +``` +services: + # Fix Ownership of HTTP Server + # Because docker volumes are owned by root by default + # And our images follow best-practise and don't run as root + change-vol-ownerships: + image: node:22-alpine3.22 + user: 'root' + volumes: + - http-server-data:/data/http-server + entrypoint: ['sh', '-c', 'chown -R node:node /data/http-server'] + + http-server: + image: ghcr.io/sofie-automation/sofie-package-manager-http-server:v1.52.0 + environment: + HTTP_SERVER_BASE_PATH: '/data/http-server' + ports: + - '8080:8080' + volumes: + - http-server-data:/data/http-server + depends_on: + change-vol-ownerships: + condition: service_completed_successfully + + workforce: + image: ghcr.io/sofie-automation/sofie-package-manager-workforce:v1.52.0 + ports: + - '8070:8070' # this needs to be exposed so that the workers can connect back to it + # environment: + # - WORKFORCE_ALLOW_NO_APP_CONTAINERS=1 # Uncomment this if your workers are in docker, to disable the check for no appContainers + + # You can deploy workers in docker too, which requires some additional configuration of your containers. + # This does not support FILESHARE accessors, they must be explicitly mounted as volumes + # You will likely want to deploy more than 1 worker + # worker0: + # image: ghcr.io/sofie-automation/sofie-package-manager-worker:v1.52.0 + # command: + # - --logLevel=debug + # - --workforceURL=ws://workforce:8070 + # - --costMultiplier=0.5 + # - --resourceId=docker + # - --networkIds=networkDocker + # volumes: + # - ./media-source:/data/source:ro + + package-manager: + depends_on: + - http-server + - workforce + image: ghcr.io/sofie-automation/sofie-package-manager-package-manager:v1.52.0 + environment: + CORE_HOST: '172.18.0.1' # the address for connecting back to Sofie core from this image + CORE_PORT: '3000' + DEVICE_ID: 'my-package-manager-id' + DEVICE_TOKEN: 'some-secret' + WORKFORCE_URL: 'ws://workforce:8070' # referencing the workforce component above + PACKAGE_MANAGER_PORT: '8060' + PACKAGE_MANAGER_URL: 'ws://insert-service-ip-here:8060' # the workers connect back to this address, so it needs to be accessible from the workers + # CONCURRENCY: 10 # How many expectation states can be evaluated at the same time + ports: + - '8060:8060' + +networks: + default: +volumes: + http-server-data: +``` + +In addition to this, you will need to run the appContainer and workers on each windows machine that package-manager needs access to: + +``` +./appContainer-node.exe + --appContainerId=caspar01 // This is a unique id for this instance of the appContainer + --workforceURL=ws://workforce-service-ip:8070 + --resourceId=caspar01 // This should also be set in the 'resource id' field of the `casparcgLocalFolder1` accessor. This is how Package Manager can identify which machine is which. + --networkIds=pm-net // This is not necessary, but can be useful for more complex setups +``` + +You can get the windows executables from [Releases](https://github.com/Sofie-Automation/sofie-package-manager/releases) GitHub repository page for Package Manager. You'll need the `appContainer-node.exe` and `worker.exe`. Put them in a folder of your choice. You can also place `ffmpeg.exe` and `ffprobe.exe` alongside them, if you don't want to make them available in `PATH`. + +Note that each appContainer needs to use a different resourceId and will need its own package containers set to use the same resourceIds if they need to access the local disk. This is how package-manager knows which workers have access to which machines. + +## Configuration + +1. Open the _Sofie Core_ Settings page ([http://localhost:3000/settings?admin=1](http://localhost:3000/settings?admin=1)), click on your Studio, and then Peripheral Devices. +1. Click the plus button (`+`) in the Parent Devices section and configure the created device to be for your Package Manager. +1. On the sidebar under the current Studio, select to the Package Manager section. +1. Click the plus button under the Package Containers heading, then click the edit icon (pencil) to the right of the newly-created package container. +1. Give this package container an ID of `casparcgContainer0` and a label of `CasparCG Package Container`. +1. Click on the dropdown under "Playout devices which use this package container" and select `casparcg0`. + - If you don't have a `casparcg0` device, add it to the Playout Gateway under the Devices heading, then restart the Playout Gateway. + - If you are using the distributed setup, you will likely want to repeat this step for each CasparCG machine. You will also want to set `Resource Id` to match the `resourceId` value provided in the appContainer command line. +1. Click the plus button under "Accessors", then click the edit icon to the right of the newly-created accessor. +1. Give this accessor an ID of `local`, a Label of `Local`, an Accessor Type of `LOCAL`, and a Folder path matching your CasparCG `media` folder. Then, ensure that only the "Allow Read access" boxes are checked. Finally, click the done button (checkmark icon) in the bottom right. +1. Click the plus button under the Package Containers heading, then click the edit icon (pencil) to the right of the newly-created package container. +1. Give this package container an ID of `httpProxy0` and a label of `Proxy for thumbnails & preview`. +1. Click the plus button under "Accessors", then click the edit icon to the right of the newly-created accessor. +1. Give this accessor an ID of `http0`, a Label of `HTTP`, an Accessor Type of `HTTP_PROXY`, and a Base URL of `http://localhost:8080/package`. Then, ensure that both the "Allow Read access" and "Allow Write access" boxes are checked. Finally, click the done button (checkmark icon) in the bottom right. +1. Scroll back to the top of the page and select `Proxy for thumbnails & preview` for both "Package Containers to use for previews" and "Package Containers to use for thumbnails". +1. Your settings should look like this once all the above steps have been completed: + ![Package Manager demo settings](/img/docs/Package_Manager_demo_settings.png) +1. If Package Manager `start:single-app` is running, restart it. If not, start it (see the above [Installation instructions](#installation-quick-start) for the relevant command line). + +### Separate HTTP proxy server + +In some setups, the URL of the HTTP proxy server is different when accessing the Sofie UI and Package Manager. +You can use the 'Network ID' concept in Package Manager to provide guidance on which to use when. + +By adding `--networkIds=pm-net` (a semi colon separated list) when launching the exes on the CasparCG machine, the application will know to prefer certain accessors with matching values. + +Then in the Sofie UI: + +1. Return to the Package Manager settings under the studio +1. Expand the `httpProxy0` container. +1. Edit the `http0` accessor to have a `Base URL` that is accessible from the casparcg machines. +1. Set the `Network ID` to `pm-net` (matching what was passed in the command line) +1. Click the plus button under "Accessors", then click the edit icon to the right of the newly-created accessor. +1. Give this accessor an ID of `publicHttp0`, a Label of `Public HTTP Proxy Accessor`, an Accessor Type of `HTTP_PROXY`, and a Base URL that is accessible to your Sofie client network. Then, ensure that only the "Allow read access" box is checked. Finally, click the done button (checkmark icon) in the bottom right. + +## Usage + +In this basic configuration, Package Manager won't be copying any packages into your CasparCG Server media folder. Instead, it will simply check that the files in the rundown are present in your CasparCG Server media folder, and you'll have to manually place those files in the correct directory. However, thumbnail and preview generation will still function, as will status reporting. + +If you're using the demo rundown provided by the [Rundown Editor](rundown-editor.md), you should already see work statuses on the Package Status page ([Status > Packages](http://localhost:3000/status/expected-packages)). + +![Example Package Manager status display](/img/docs/Package_Manager_status_example.jpg) + +If all is good, head to the [Rundowns page](http://localhost:3000/rundowns) and open the demo rundown. + +### Further Reading + +- [Package Manager](https://github.com/Sofie-Automation/sofie-package-manager) on GitHub. diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-sofie-server-core.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-sofie-server-core.md new file mode 100644 index 00000000000..8d930108a4e --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-sofie-server-core.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 2 +--- + +# Quick install + +## Installing for testing \(or production\) + +### **Prerequisites** + +* **Linux**: Install [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/) and [docker-compose](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-18-04). +* **Windows**: Install [Docker for Windows](https://hub.docker.com/editions/community/docker-ce-desktop-windows). + +### Installation + +This docker-compose file automates the basic setup of the [Sofie-Core application](../../for-developers/libraries.md#main-application), the backend database and different Gateway options. + +```yaml +# This is NOT recommended to be used for a production deployment. +# It aims to quickly get an evaluation version of Sofie running and serve as a basis for how to set up a production deployment. +services: + db: + hostname: mongo + image: mongo:6.0 + restart: always + entrypoint: ['/usr/bin/mongod', '--replSet', 'rs0', '--bind_ip_all'] + # the healthcheck avoids the need to initiate the replica set + healthcheck: + test: test $$(mongosh --quiet --eval "try {rs.initiate()} catch(e) {rs.status().ok}") -eq 1 + interval: 10s + start_period: 30s + ports: + - '27017:27017' + volumes: + - db-data:/data/db + networks: + - sofie + + # Fix Ownership Snapshots mount + # Because docker volumes are owned by root by default + # And our images follow best-practise and don't run as root + change-vol-ownerships: + image: node:22-alpine + user: 'root' + volumes: + - sofie-store:/mnt/sofie-store + entrypoint: ['sh', '-c', 'chown -R node:node /mnt/sofie-store'] + + core: + hostname: core + image: sofietv/tv-automation-server-core:release52 + restart: always + ports: + - '3000:3000' # Same port as meteor uses by default + environment: + PORT: '3000' + MONGO_URL: 'mongodb://db:27017/meteor' + MONGO_OPLOG_URL: 'mongodb://db:27017/local' + ROOT_URL: 'http://localhost:3000' + SOFIE_STORE_PATH: '/mnt/sofie-store' + networks: + - sofie + volumes: + - sofie-store:/mnt/sofie-store + depends_on: + change-vol-ownerships: + condition: service_completed_successfully + db: + condition: service_healthy + + playout-gateway: + image: sofietv/tv-automation-playout-gateway:release52 + restart: always + environment: + DEVICE_ID: playoutGateway0 + CORE_HOST: core + CORE_PORT: '3000' + networks: + - sofie + - lan_access + depends_on: + - core + + # Choose one of the following images, depending on which type of ingest gateway is wanted. + + # spreadsheet-gateway: + # image: superflytv/sofie-spreadsheet-gateway:latest + # restart: always + # environment: + # DEVICE_ID: spreadsheetGateway0 + # CORE_HOST: core + # CORE_PORT: '3000' + # networks: + # - sofie + # depends_on: + # - core + + # mos-gateway: + # image: sofietv/tv-automation-mos-gateway:release52 + # restart: always + # ports: + # - "10540:10540" # MOS Lower port + # - "10541:10541" # MOS Upper port + # # - "10542:10542" # MOS query port - not used + # environment: + # DEVICE_ID: mosGateway0 + # CORE_HOST: core + # CORE_PORT: '3000' + # networks: + # - sofie + # depends_on: + # - core + + # inews-gateway: + # image: tv2media/inews-ftp-gateway:1.37.0-in-testing.20 + # restart: always + # command: yarn start -host core -port 3000 -id inewsGateway0 + # networks: + # - sofie + # depends_on: + # - core + + # rundown-editor: + # image: ghcr.io/superflytv/sofie-automation-rundown-editor:v2.2.4 + # restart: always + # ports: + # - '3010:3010' + # environment: + # PORT: '3010' + # networks: + # - sofie + # depends_on: + # - core + +networks: + sofie: + lan_access: + driver: bridge + +volumes: + db-data: + sofie-store: +``` + +Create a `Sofie` folder, copy the above content, and save it as `docker-compose.yaml` within the `Sofie` folder. + +Visit [Rundowns & Newsroom Systems](installing-a-gateway/rundown-or-newsroom-system-connection/intro.md) to see which _Ingest Gateway_ can be used in your specific production environment. If you don't have an NRCS that you would like to integrate with, you can use the [Rundown Editor](rundown-editor) as a simple Rundown creation utility. Navigate to the _ingest-gateway_ section of `docker-compose.yaml` and select which type of _ingest-gateway_ you'd like installed by uncommenting it. Save your changes. + +Open a terminal, execute `cd Sofie` and `sudo docker-compose up` \(or just `docker-compose up` on Windows\). This will download MongoDB and Sofie components' container images and start them up. The installation will be done when your terminal window will be filled with messages coming from `playout-gateway_1` and `core_1`. + +Once the installation is done, Sofie should be running on [http://localhost:3000](http://localhost:3000). Next, you need to make sure that the Playout Gateway and Ingest Gateway are connected to the default Studio that has been automatically created. Open the Sofie User Interface with [Configuration Access level](../features/access-levels#browser-based) by opening [http://localhost:3000/?admin=1](http://localhost:3000/?admin=1) in your Web Browser and navigate to _Settings_ 🡒 _Studios_ 🡒 _Default Studio_ 🡒 _Peripheral Devices_. In the _Parent Devices_ section, create a new Device using the **+** button, rename the device to _Playout Gateway_ and select _Playout gateway_ from the _Peripheral Device_ drop down menu. Repeat this process for your _Ingest Gateway_ or _Sofie Rundown Editor_. + +:::note +Starting with Sofie version 1.52.0, `sofietv` container images will run as UID 1000. +::: + +### Tips for running in production + +There are some things not covered in this guide needed to run _Sofie_ in a production environment: + +- Logging: Collect, store and track error messages. [Kibana](https://www.elastic.co/kibana) and [logstash](https://www.elastic.co/logstash) is one way to do it. +- NGINX: It is customary to put a load-balancer in front of _Sofie Core_. +- Memory and CPU usage monitoring. + +## Installing for Development + +Installation instructions for installing Sofie-Core or the various gateways are available in the README file in their respective github repos. + +Common prerequisites are [Node.js](https://nodejs.org/) and [Yarn](https://yarnpkg.com/). +Links to the repos are listed at [Applications & Libraries](../../for-developers/libraries.md). + +[_Sofie Core_ GitHub Page for Developers](https://github.com/Sofie-Automation/sofie-core) diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/intro.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/intro.md new file mode 100644 index 00000000000..c3a14c218bc --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/intro.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 1 +--- +# Getting Started + +_Sofie_ can be installed in many different ways, depending on which platforms, needs, and features you desire. The _Sofie_ system consists of several applications that work together to provide complete broadcast automation system. Each of these components' installation will be covered in this guide. Additional information about the products or services mentioned alongside the Sofie Installation can be found on the [Further Reading](../further-reading.md). + +There are four minimum required components to get a Sofie system up and running. First you need the [_Sofie Core_](installing-sofie-server-core.md), which is the brains of the operation. Then a set of [_Blueprints_](installing-blueprints.md) to handle and interpret incoming and outgoing data. Next, an [_Ingest Gateway_](installing-a-gateway/rundown-or-newsroom-system-connection/intro.md) to fetch the data for the Blueprints. Then finally, a [_Playout Gateway_](installing-a-gateway/playout-gateway.md) to send the data to your playout device of choice. + + + +## Sofie Core View + +The _Rundowns_ view will display all the active rundowns that the _Sofie Core_ has access to. + +![Rundown View](/img/docs/getting-started/rundowns-in-sofie.png) + +The _Status_ views displays the current status for the attached devices and gateways. + +![Status View – Describes the state of _Sofie Core_](/img/docs/getting-started/status-page.jpg) + +The _Settings_ views contains various settings for the studio, show styles, blueprints etc.. If the link to the settings view is not visible in your application, check your [Access Levels](../features/access-levels.md). More info on specific parts of the _Settings_ view can be found in their corresponding guide sections. + +![Settings View – Describes how the _Sofie Core_ is configured](/img/docs/getting-started/settings-page.jpg) + +## Sofie Core Overview + +The _Sofie Core_ is the primary application for managing the broadcast but, it doesn't play anything out on it's own. You need to use Gateways to establish the connection from the _Sofie Core_ to other pieces of hardware or remote software. + +### Gateways + +Gateways are separate applications that bridge the gap between the _Sofie Core_ and other pieces of hardware or services. At minimum, you will need a _Playout Gateway_ so your timeline can interact with your playout system of choice. To install the _Playout Gateway_, visit the [Installing a Gateway](installing-a-gateway/intro.md) section of this guide and for a more in-depth look, please see [Gateways](../concepts-and-architecture.md#gateways). + +### Blueprints + +Blueprints can be described as the logic that determines how a studio and show should interact with one another. They interpret the data coming in from the rundowns and transform them into a rich set of playable elements \(_Segments_, _Parts_, _AdLibs,_ etcetera\). The _Sofie Core_ has three main blueprint types, _System Blueprints_, _Studio Blueprints_, and _Showstyle Blueprints_. Installing _Sofie_ does not require you understand what these blueprints do, just that they are required for the _Sofie Core_ to work. If you would like to gain a deeper understand of how _Blueprints_ work, please visit the [Blueprints](#blueprints) section. + diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/media-manager.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/media-manager.md new file mode 100644 index 00000000000..5c966aec573 --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/media-manager.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 100 +--- + +# Media Manager + +:::caution + +Media Manager is deprecated and is not recommended for new deployments. There are known issues that won't be fixed and the API's it is using to interface with Sofie will be removed. + +::: + +The Media Manager handles the media, or files, that make up the rundown content. To install it, begin by downloading the latest release of [Media Manager from GitHub](https://github.com/nrkno/sofie-media-management/releases). You can now run the `media-manager.exe` file inside the extracted folder. A warning window may popup about the app being unrecognized. You can get around this by selecting _More Info_ and clicking _Run Anyways_. A terminal window will open and begin running the application. + +You can now open the _Sofie Core_, `http://localhost:3000`, and navigate to the _Settings page_. You will see your _Media Manager_ under the _Devices_ section of the menu. The four main sections, general properties, attached storage, media flows, monitors, as well as attached subdivides, all contribute to how the media is handled within the Sofie Core. + +### Further Reading + +- [Media Manager Releases on GitHub](https://github.com/nrkno/sofie-media-management/releases) +- [Media Manager GitHub Page for Developers](https://github.com/nrkno/sofie-media-management) diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/rundown-editor.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/rundown-editor.md new file mode 100644 index 00000000000..4293431ac4e --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/rundown-editor.md @@ -0,0 +1,18 @@ +--- +sidebar_position: 8 +--- + +# Sofie Rundown Editor + +Sofie Rundown Editor is a tool for creating and editing rundowns in a _demo_ environment of Sofie, without the use of an iNews, Spreadsheet or MOS Gateway + +### Connecting Sofie Rundown Editor + +After starting the Rundown Editor via the `docker-compose.yaml` specified in [Quick Start](./installing-sofie-server-core), this app requires a special bit of configuration to connect to Sofie. You need to open the Rundown Editor web interface at [http://localhost:3010/](http://localhost:3010/), go to _Settings_ and set _Core Connection Settings_ to: + +| Property | Value | +| -------- | ------ | +| Address | `core` | +| Port | `3000` | + +The header should change to _Core Status: Connected to core:3000_. diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/intro.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/intro.md new file mode 100644 index 00000000000..4bf6b039a9f --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/intro.md @@ -0,0 +1,41 @@ +--- +sidebar_label: Introduction +sidebar_position: 0 +--- + +# Sofie User Guide + +## Key Features + +### Web-based GUI + +![Producer's / Director's View](/img/docs/Sofie_GUI_example.jpg) + +![Warnings and notifications are displayed to the user in the GUI](/img/docs/warnings-and-notifications.png) + +![The Host view, displaying time information and countdowns](/img/docs/host-view.png) + +![The prompter view](/img/docs/prompter-view.png) + +:::info +Tip: The different web views \(such as the host view and the prompter\) can easily be transmitted over an SDI signal using the HTML producer in [CasparCG](installation/installing-connections-and-additional-hardware/casparcg-server-installation.md). +::: + +### Modular Device Control + +Sofie controls playout devices \(such as vision and audio mixers, graphics and video playback\) via the Playout Gateway, using the [Timeline](concepts-and-architecture.md#timeline). +The Playout Gateway controls the devices and keeps track of their state and statuses, and lets the user know via the GUI if something's wrong that can affect the show. + +### _State-based Playout_ + +Sofie is using a state-based architecture to control playout. This means that each element in the show can be programmed independently - there's no need to take into account what has happened previously in the show; Sofie will make sure that the video is loaded and that the audio fader is tuned to the correct position, no matter what was played out previously. +This allows the producer to skip ahead or move backwards in a show, without the fear of things going wrong on air. + +### Modular Data Ingest + +Sofie features a modular ingest data-flow, allowing multiple types of input data to base rundowns on. Currently there is support for [MOS-based](http://mosprotocol.com) systems such as ENPS and iNEWS, as well as [Google Spreadsheets](installation/installing-a-gateway/rundown-or-newsroom-system-connection/installing-sofie-with-google-spreadsheet-support), and more is in development. + +### Blueprints + +The [Blueprints](concepts-and-architecture.md#blueprints) are plugins to _Sofie_, which allows for customization and tailor-made show designs. +The blueprints are made different depending on how the input data \(rundowns\) look like, how the show-design look like, and what devices to control. diff --git a/packages/documentation/versioned_docs/version-1.52.0/user-guide/supported-devices.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/supported-devices.md new file mode 100644 index 00000000000..0bee545156d --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/supported-devices.md @@ -0,0 +1,119 @@ +--- +sidebar_position: 1.5 +--- + +# Supported Playout Devices + +All playout devices are essentially driven through the _timeline_, which passes through _Sofie Core_ into the Playout Gateway where it is processed by the timeline-state-resolver. This page details which devices and what parts of the devices can be controlled through the timeline-state-resolver library. In general a blueprints developer can use the [timeline-state-resolver-types package](https://www.npmjs.com/package/timeline-state-resolver-types) to see the interfaces for the timeline objects used to control the devices. + +## Blackmagic Design's ATEM Vision Mixers + +We support almost all features of these devices except fairlight audio, camera controls and streaming capabilities. A non-inclusive list: + +- Control of camera inputs +- Transitions +- Full control of keyers +- Full control of DVE's +- Control of media pools +- Control of auxiliaries + +## CasparCG Server + +Tested and developed against [a fork of version 2.4](https://github.com/nrkno/sofie-casparcg-server) + +- Video playback +- Graphics playback +- Recording / streaming +- Mixer parameters +- Transitions + +## HTTP Protocol + +- GET/POST/PUT/DELETE methods +- Pre-shared "Bearer" token authorization +- OAuth 2.0 Client Credentials flow +- Interval based watcher for status monitoring + +## Blackmagic Design HyperDeck + +- Recording + +## Lawo Powercore & MC2 Series + +- Control over faders + - Using the ramp function on the powercore +- Control of parameters in the ember tree + +## OSC protocol + +- Sending of integers, floats, strings, blobs +- Tweening \(transitioning between\) values + +Can be configured in TCP or UDP mode. + +## Panasonic PTZ Cameras + +- Recalling presets +- Setting zoom, zoom speed and recall speed + +## Pharos Lighting Control + +- Recalling scenes +- Recalling timelines + +## Grass Valley SQ Media Servers + +- Control of playback +- Looping +- Cloning + +_Note: some features are controlled through the Package Manager_ + +## Shotoku Camera Robotics + +- Cutting to shots +- Fading to shots + +## Singular Live + +- Control nodes + +## Sisyfos + +- On-air controls +- Fader levels +- Labels +- Hide / show channels + +## TCP Protocol + +- Sending messages + +## VizRT Viz MSE + +- Pilot elements +- Continue commands +- Loading all elements +- Clearing all elements + +## vMix + +- Full M/E control +- Audio control +- Streaming / recording control +- Fade to black +- Overlays +- Transforms +- Transitions + +## OBS + +_Through OBS 28+ WebSocket API (a.k.a v5 Protocol)_ + +- Current / Preview Scene +- Current Transition +- Recording +- Streaming +- Scene Item visibility +- Source Settings (FFmpeg source) +- Source Mute diff --git a/packages/documentation/versioned_docs/version-26.03.0/about-sofie.md b/packages/documentation/versioned_docs/version-26.03.0/about-sofie.md new file mode 100644 index 00000000000..4edeccef038 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/about-sofie.md @@ -0,0 +1,20 @@ +--- +title: About Sofie +hide_table_of_contents: true +sidebar_label: About Sofie +sidebar_position: 1 +--- + +# Sofie TV Automation System + +![The producer's view in Sofie](https://raw.githubusercontent.com/Sofie-Automation/Sofie-TV-automation/main/images/Sofie_GUI_example.jpg) + +_**Sofie**_ is a web-based TV automation system for studios and live shows. It has been used in daily live TV news productions since September 2018 by broadcasters such as [**NRK**](https://www.nrk.no/about/), the [**BBC**](https://www.bbc.com/aboutthebbc), and [**TV 2 (Norway)**](https://info.tv2.no/info/s/om-tv-2). + +## Key Features + +- User-friendly, modern web-based GUI +- State-based device control and playout of video, audio, and graphics +- Modular device-control architecture with support for various hardware and software setups +- Modular data-ingest architecture that supports MOS and Google spreadsheets +- Plug-in architecture for programming shows diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/api-documentation.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/api-documentation.md new file mode 100644 index 00000000000..6af8e95f979 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/api-documentation.md @@ -0,0 +1,8 @@ +--- +sidebar_position: 6 +--- + +# API Documentation + +The Sofie Blueprints API and the Sofie Peripherals API documentation is automatically generated and available through +[sofie-automation.github.io/sofie-core/typedoc](https://sofie-automation.github.io/sofie-core/typedoc). diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/api-stability.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/api-stability.md new file mode 100644 index 00000000000..5368c979ac9 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/api-stability.md @@ -0,0 +1,26 @@ +--- +title: API Stability +sidebar_position: 11 +--- + +Sofie has various APIs for talking between components, and for external systems to interact with. + +We classify each api into one of two categories: + +## Stable + +This is a collection of APIs which we intend to avoid introducing any breaking change to unless necessary. This is so external systems can rely on this API without needing to be updated in lockstep with Sofie, and hopefully will make sense to developers who are not familiar with Sofie's inner workings. + +In version 1.50, a new REST API was introduced. This can be found at `/api/v1.0`, and is designed to allow an external system to interact with Sofie using simplified abstractions of Sofie internals. + +The _Live Status Gateway_ is also part of this stable API, intended to allow for reactively retrieving data from Sofie. Internally it is translating the internal APIs into a stable version. + +:::note +You can find the _Live Status Gateway_ in the `packages` folder of the [Sofie Core](https://github.com/Sofie-Automation/sofie-core) repository. +::: + +## Internal + +This covers everything we expose over DDP, the `/api/0` endpoint and any other http endpoints. + +These are intended for use between components of Sofie, which should be updated together. The DDP api does have breaking changes in most releases. We use the `server-core-integration` library to manage these typings, and to ensure that compatible versions are used together. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/contribution-guidelines.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/contribution-guidelines.md new file mode 100644 index 00000000000..bc636057162 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/contribution-guidelines.md @@ -0,0 +1,118 @@ +--- +description: >- + The Sofie team happily encourage contributions to the Sofie project, and + kindly ask you to observe these guidelines when doing so. +sidebar_position: 2 +--- + +# Contribution Guidelines + +_Last updated January 2026_ + +## About the Sofie TV Studio Automation Project + +The Sofie project includes a number of open source applications and libraries originally developed by the Norwegian public service broadcaster, [NRK](https://www.nrk.no/about/). Sofie has been used in daily live TV news productions since September 2018 by broadcasters such as [**NRK**](https://www.nrk.no/about/), the [**BBC**](https://www.bbc.com/aboutthebbc), and [**TV 2 (Norway)**](https://info.tv2.no/info/s/om-tv-2). + +A list of the "Sofie repositories" [can be found here](libraries.md). The Sofie Governance organisation owns the copyright of the contents of the official Sofie repositories, including the source code, related files, as well as the Sofie logo. + +The Sofie Governance organisation is responsible for development and maintenance. We also do thorough testing of each release to avoid regressions in functionality and ensure interoperability with the various hardware and software involved. + +The Sofie team welcomes open source contributions and will actively work towards enabling contributions to become mergeable into the Sofie repositories. However, we reserve the right to refuse any contributions. + +Sofie releases are targeted on a quarterly release cycle and are feature frozen six weeks before the release date, after which PRs that introduce new features are no longer accepted for that release. + +Three weeks before release, all PRs for that release should be merged to allow for testing and bug fixing before release. + +## About Contributions + +Thank you for considering contributing to the Sofie project! + +Before you start, there are a few things you should know: + +### “Discussions Before Pull Requests” + +**Minor changes** (most bug fixes and small features) can be submitted directly as pull requests to the appropriate official repo. + +However, Sofie is a big project with many differing users and use cases. **Larger changes** may be difficult to merge into an official repository if the Sofie Governance team and other contributors have not been made aware of their existence beforehand. Since figuring out what side-effects a new feature or a change may have for other Sofie users can be tricky, we advise opening an RFC issue (_Request for Comments_) early in your process. Good moments to open an RFC include: + +- When a user need is identified and described +- When you have a rough idea about how a feature may be implemented +- When you have a sketch of how a feature could look like to the user + +To facilitate timely handling of larger contributions, there’s a workflow intended to keep an open dialogue between all interested parties: + +1. Contributor opens an RFC (as a _GitHub issue_) in the appropriate repository. +2. The Sofie Technical Steering Committee (TSC) evaluates the RFC, usually within two weeks. +3. If needed, the TSC establishes contact with the RFC author, who will be invited to a workshop where the RFC is discussed. Meeting notes are published publicly on the RFC thread. +4. Discussions about the RFC continue as needed, either in workshops or in comments in the RFC thread. +5. The contributor references the RFC when a pull request is ready. + +It will be very helpful if your RFC includes specific use cases that you are facing. Providing a background on how your users are using Sofie can clear up situations in which certain phrases or processes may be ambiguous. If during your process you have already identified various solutions as favorable or unfavorable, offering this context will move the discussion further still. + +Via the RFC process, we're looking to maximize involvement from various stakeholders, so you probably don't need to come up with a very detailed design of your proposed change or feature in the RFC. An end-user oriented description will be most valuable in creating a constructive dialogue, but don't shy away from also adding a more technical description, if you find that will convey your ideas better. + +### Base contributions on the in-development branch + +In order to facilitate merging, we ask that contributions are based on the latest (at the time of the pull request) _in-development_ branch (often named `release*`). + +See **CONTRIBUTING.md** in each official repository for details on which branch to use as a base for contributions. + +## Developer Guidelines + +### Pull Requests + +We encourage you to open PRs early! If it’s still in development, open the PR as a draft. + +### Types + +All official Sofie repositories use TypeScript. When you contribute code, be sure to keep it as strictly typed as possible. + +### Code Style & Formatting + +Most of the projects use a linter (eslint) and a formatter (prettier). Before submitting a pull request, please make sure it conforms to the linting rules by running yarn lint. yarn lint --fix can fix most of the issues. + +### Tests + +See **CONTRIBUTING.md** in each official repository for details on the level of unit tests required for contribution to that repository. + +### Documentation + +We rely on two types of documentation; the [Sofie documentation](https://sofie-automation.github.io/sofie-core/) ([source code](https://github.com/Sofie-Automation/sofie-core/tree/main/packages/documentation)) and inline code documentation. + +We don't aim to have the "absolute perfect documentation possible", BUT we do try to improve and add documentation to have a good-enough-to-be-comprehensible standard. We think that: + +- _What_ something does is not as important – we can read the code for that. +- _Why_ something does something, **is** important. Implied usage, side-effects, descriptions of the context etc.... + +When you contribute, we ask you to also update any documentation where needed. + +### Updating Dependencies​ + +When updating dependencies in a library, it is preferred to do so via `yarn upgrade-interactive --latest` whenever possible. This is so that the versions in `package.json` are also updated as we have no guarantee that the library will work with versions lower than that used in the `yarn.lock` file, even if it is compatible with the semver range in `package.json`. After this, a `yarn upgrade` can be used to update any child dependencies + +Be careful when bumping across major versions. + +Also, each of the libraries has a minimum Node.js version specified in their package.json. Care must be taken when updating dependencies to ensure its compatibility is retained. + +### Resolutions​ + +We sometimes use the `yarn resolutions` property in `package.json` to fix security vulnerabilities in dependencies of libraries that haven't released a fix yet. If adding a new one, try to make it as specific as possible to ensure it doesn't have unintended side effects. + +When updating other dependencies, it is a good idea to make sure that the resolutions defined still apply and are correct. + +### Logging + +When logging, we try to adhere to the following guidelines: + +Usage of `console.log` and `console.error` directly is discouraged (except for quick debugging locally). Instead, use one of the logger libraries (to output JSON logs which are easier to index). +When logging, use one of the **log levels** described below: + +| Level | Description | Examples | +| --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `silly` | For very detailed logs (rarely used). | - | +| `debug` | Logging of info that could be useful for developers when debugging certain issues in production. | `"payload: {>JSON<} "`

`"Reloading data X from DB"` | +| `verbose` | Logging of common events. | `"File X updated"` | +| `info` | Logging of significant / uncommon events.

_Note: If an event happens often or many times, use `verbose` instead._ | `"Initializing TSR..."`

`"Starting nightly cronjob..."`

`"Snapshot X restored"`

`"Not allowing removal of current playing segment 'xyz', making segment unsynced instead"`

`"PeripheralDevice X connected"` | +| `warn` | Used when something unexpected happened, but not necessarily due to an application bug.

These logs don't have to be acted upon directly, but could be useful to provide context to a dev/sysadmin while troubleshooting an issue. | `"PeripheralDevice X disconnected"`

`"User Error: Cannot activate Rundown (Rundown not found)" `

`"mosRoItemDelete NOT SUPPORTED"` | +| `error` | Used when something went _wrong_, preventing something from functioning.

A logged `error` should always result in a sysadmin / developer looking into the issue.

_Note: Don't use `error` for things that are out of the app's control, such as user error._ | `"Cannot read property 'length' of undefined"`

`"Failed to save Part 'X' to DB"` | +| `crit` | Fatal errors (rarely used) | - | diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/data-model.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/data-model.md new file mode 100644 index 00000000000..ee7143da9af --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/data-model.md @@ -0,0 +1,130 @@ +--- +title: Data Model +sidebar_position: 9 +--- + +Sofie persists the majority of its data in a MongoDB database. This allows us to use Typescript friendly documents, +without needing to worry too much about the strictness of schemas, and allows us to watch for changes happening inside +the database as a way of ensuring that updates are reactive. + +Data is typically pushed to the UI or the gateways through [Publications](./publications) over the DDP connection that Meteor provides. + +## Collection Ownership + +Each collection in MongoDB is owned by a different area of Sofie. In some cases, changes are also made by another area, but we try to keep this to a minimum. +In every case, any layout changes and any scheduled cleanup are performed by the Meteor layer for simplicity. + +### Meteor + +This category of collections is rather loosely defined, as it ends up being everything that doesn't belong somewhere else + +This consists of anything that is configurable from the Sofie UI, anything needed solely for the UI and some other bits. Additionally, there are some collections which are populated by other portions of a Sofie system, such as by Package Manager, through an API over DDP. +Currently, there is not a very clearly defined flow for modifying these documents, with the UI often making changes directly with minimal or no validation. + +This includes: + +- [Blueprints](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Blueprint.ts) +- [Buckets](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Bucket.ts) +- [CoreSystem](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/CoreSystem.ts) +- [Evaluations](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/Evaluations.ts) +- [ExternalMessageQueue](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ExternalMessageQueue.ts) +- [ExpectedPackageWorkStatuses](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ExpectedPackageWorkStatuses.ts) +- [MediaObjects](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/MediaObjects.ts) +- [MediaWorkFlows](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/MediaWorkFlows.ts) +- [MediaWorkFlowSteps](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/MediaWorkFlowSteps.ts) +- [PackageInfos](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PackageInfos.ts) +- [PackageContainerPackageStatuses](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PackageContainerPackageStatus.ts) +- [PackageContainerStatuses](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PackageContainerStatus.ts) +- [PeripheralDeviceCommands](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PeripheralDeviceCommand.ts) +- [PeripheralDevices](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PeripheralDevice.ts) +- [RundownLayouts](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/RundownLayouts.ts) +- [ShowStyleBase](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ShowStyleBase.ts) +- [ShowStyleVariant](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ShowStyleVariant.ts) +- [Snapshots](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/Snapshots.ts) +- [Studio](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Studio.ts) +- [TriggeredActions](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/TriggeredActions.ts) +- [TranslationsBundles](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/TranslationsBundles.ts) +- [UserActionsLog](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/UserActionsLog.ts) +- [Users](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/Users.ts) +- [Workers](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/collections/Workers.ts) +- [WorkerThreads](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/WorkerThreads.ts) + +### Ingest + +This category of collections is owned by the ingest [worker threads](./worker-threads-and-locks.md), and models a Rundown based on how it is defined by the NRCS. + +These collections are not exposed as writable in Meteor, and are only allowed to be written to by the ingest worker threads. +There is an exception to both of these; Meteor is allowed to write to it as part of migrations, and cleaning up old documents. While the playout worker is allowed to modify certain Segments that are labelled as being owned by playout. + +The collections which are owned by the ingest workers are: + +- [AdLibActions](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/AdLibActions.ts) +- [AdLibPieces](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/AdLibPieces.ts) +- [BucketAdLibActions](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/BucketAdLibActions.ts) +- [BucketAdLibPieces](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/BucketAdLibPieces.ts) +- [ExpectedPackages](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ExpectedPackages.ts) +- [ExpectedPlayoutItems](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/ExpectedPlayoutItems.ts) +- [IngestDataCache](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/IngestDataCache.ts) +- [Parts](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Parts.ts) +- [Pieces](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Pieces.ts) +- [RundownBaselineAdLibActions](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/RundownBaselineAdLibActions.ts) +- [RundownBaselineAdLibPieces](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/RundownBaselineAdLibPieces.ts) +- [RundownBaselineObjects](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/RundownBaselineObjects.ts) +- [Rundowns](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Rundowns.ts) +- [Segments](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Segments.ts) + +These collections model a Rundown from the NRCS in a Sofie form. Almost all of these contain documents which are largely generated by blueprints. +Some of these collections are used by Package Manager to initiate work, while others form a view of the Rundown for the users, and are used as part of the model for playout. + +### Playout + +This category of collections is owned by the playout [worker threads](./worker-threads-and-locks.md), and is used to model the playout of a Rundown or set of Rundowns. + +During the final stage of an ingest operation, there is a period where the ingest worker acquires a `PlaylistLock`, so that it can ensure that the RundownPlaylist the Rundown is a part of is updated with any necessary changes following the ingest operation. During this lock, it will also attempt to [sync any ingest changes](./for-blueprint-developers/sync-ingest-changes) to the PartInstances and PieceInstances, if supported by the blueprints. + +As before, Meteor is allowed to write to these collections as part of migrations, and cleaning up old documents. + +The collections which can only be modified inside of a `PlaylistLock` are: + +- [PartInstances](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PartInstances.ts) +- [PieceInstances](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/PieceInstances.ts) +- [RundownPlaylists](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/RundownPlaylists.ts) +- [Timelines](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/Timelines.ts) +- [TimelineDatastore](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/corelib/src/dataModel/TimelineDatastore.ts) + +These collections are used in combination with many of the ingest collections, to drive playout. + +#### RundownPlaylist + +RundownPlaylists are a Sofie invention designed to solve one problem; in some NRCS it is beneficial to build a show across multiple Rundowns, which should then be concatenated for playout. +In particular, MOS has no concept of a Playlist, only Rundowns, and it was here where we need to be able to combine multiple Rundowns. + +This functionality can be used to either break down long shows into manageable chunks, or to indicate a different type of show between the each portion. + +Because of this, RundownPlaylists are largely missing from the ingest side of Sofie. We do not expose them in the ingest APIs, or do anything with them throughout the majority of the blueprints generating a Rundown. +Instead, we let the blueprints specify that a Rundown should be part of a RundownPlaylist by setting the `playlistExternalId` property, where multiple Rundowns in a Studio with the same id will be grouped into a RundownPlaylist. +If this property is not used, we automatically generate a RundownPlaylist containing the Rundown by itself. + +It is during the final stages of an ingest operation, where the RundownPlaylist will be generated (with the help of blueprints), if it is necessary. +Another benefit to this approach, is that it allows for very cheaply and easily moving Rundowns between RundownPlaylists, even safely affecting a RundownPlaylist that is currently on air. + +#### Part vs PartInstance and Piece vs PieceInstance + +In the early days of Sofie, we had only Parts and Pieces, no PartInstances and PieceInstances. + +This quickly became costly and complicated to handle cases where the user used Adlibs in Sofie. Some of the challenges were: + +- When a Part is deleted from the NRCS and that part is on air, we don't want to delete it in Sofie immediately +- When a Part is modified in the NRCS and that part is on air, we may not want to apply all of the changes to playout immediately +- When a Part has finished playback and is set-as-next again, we need to make sure to discard any changes made by the previous playout, and restore it to as if was refreshly ingested (including the changes we ignored while it was on air) +- When creating an adlib part, we need to be sure that an ingest operation doesn't attempt to delete it, until playout is finished with it. +- After using an adlib in a part, we need to remove the piece it created when we set-as-next again, or reset the rundown +- When an earlier part is removed, where an infinite piece has spanned into the current part, we may not want to remove that infinite piece + +Our solution to some of this early on was to not regenerate certain Parts when receiving ingest operations for them, and to defer it until after that Part was off air. While this worked, it was not optimal to re-run ingest operations like that while doing a take. This also required the blueprint api to generate a single part in each call, which we were starting to find limiting. This was also problematic when resetting a rundown, as that would often require rerunning ingest for the whole rundown, making it a notably slow operation. + +At this point in time, Adlib Actions did not exist in Sofie. They are able to change almost every property of a Part of Piece that ingest is able to define, which makes the resetting process harder. + +PartInstances and PieceInstances were added as a way for us to make a copy of each Part and Piece, as it was selected for playout, so that we could allow ingest without risking affecting playout, and to simplify the cleanup performed. The PartInstances and PieceInstances are our record of how the Rundown was played, which we can utilise to output metadata such as for chapter markers on a web player. In earlier versions of Sofie this was tracked independently with an `AsRunLog`, which resulted in odd issues such as having `AsRunLog` entries which referred to a Part which no longer existed, or whose content was very different to how it was played. + +Later on, this separation has allowed us to more cleanly define operations as ingest or playout, and allows us to run them in parallel with more confidence that they won't accidentally wipe out each others changes. Previously, both ingest and playout operations would be modifying documents in the Piece and Part collections, making concurrent operations unsafe as they could be modifying the same Part or Piece. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/_category_.json b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/_category_.json new file mode 100644 index 00000000000..c5a2693b0e7 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Device Integrations", + "position": 5 +} diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/intro.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/intro.md new file mode 100644 index 00000000000..727613264a9 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/intro.md @@ -0,0 +1,18 @@ +# Introduction + +Device integrations in Sofie are part of the Timeline State Resolver (TSR) library. A device integration has a couple of responsibilities in the Sofie eco-system. First and foremost it should establish a connection with a foreign device. It should also be able to convert Sofie's idea of what the device should be doing into commands to control the device. And lastly it should export interfaces to be used by the blueprints developer. + +In order to understand all about writing TSR integrations there are some concepts to familiarise yourself with, in this documentation we will attempt to explain these. + +- [Options and mappings](./options-and-mappings) +- [TSR Integration API](./tsr-api) +- [TSR Types package](./tsr-types) +- [TSR Actions](./tsr-actions) + +But to start off we will explain the general structure of the TSR. Any user of the TSR will interface primarily with the Conductor class. Primarily the user will input device configurations, mappings and timelines into the TSR. The timeline describes the entire state of all of the devices over time. It does this by putting objects on timeline layers. Every timeline layer maps to a specific part of the device, this is configured through the mappings. + +The timeline is converted into disctinct states at different points in time, and these states are fed to the individual integrations. As an integration developer you shouldn't have to worry about keeping track of this. It is most important that you expose \(a\) a method to convert from a Timeline State to a Device State, \(b\) a method for diffing 2 device states and \(c\) a way to send commands to the device. We'll dive deeper into this in [TSR Integration API](./tsr-api). + +:::info +The information in this section is not a conclusive guide on writing an integration, it should be use more as a guide to use while looking at a TSR integration such as the [OSC integration](https://github.com/Sofie-Automation/sofie-timeline-state-resolver/tree/master/packages/timeline-state-resolver/src/integrations/osc). +::: diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/options-and-mappings.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/options-and-mappings.md new file mode 100644 index 00000000000..ac843283460 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/options-and-mappings.md @@ -0,0 +1,11 @@ +# Options and mappings + +For an end user to configure the system from the Sofie UI we have to expose options and mappings from the TSR. This is done through [JSON config schemas](../json-config-schema) in the `$schemas` folder of your integration. + +## Options + +Options are for any configuration the user needs to make for your device integration to work well. Things like IP addresses and ports go here. + +## Mappings + +A mappings is essentially an addresses into the device you are integrating with. For example, a mapping for CasparCG contains a channel and a layer. And a mapping for an Atem can be a mix effect or a downstream keyer. It is entirely possible for the user to define 2 mappings pointing to the same bit of hardware so keep that in mind while writing your integration. The granularity of the mappings influences both how you write your device as well as the shape of the timeline objects. If, for example, we had not included the layer number in the CasparCG mapping, we would have had to define this separately on every timeline object. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/shared-hardware-control.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/shared-hardware-control.md new file mode 100644 index 00000000000..8c0a056d322 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/shared-hardware-control.md @@ -0,0 +1,68 @@ +# TSR Shared Hardware Control + +TSR (Timeline State Resolver) in Sofie Core is responsible for translating state changes into device commands. Normally, TSR assumes full control over the devices it manages — meaning the device should always be in the expected "State A" before transitioning to "State B." However, in real-world integrations, devices are sometimes externally controlled or adjusted. This documentation describes how a TSR integration can be implemented to detect and reconcile external device changes using the Shared Hardware Control mechanism. + +## Overview + +TSR’s command generation is based on timeline state diffs. To transition a device from State A to State B, TSR generates commands based on the difference between these two states. If the device is not currently in State A (e.g., due to external control), then TSR’s assumptions break — leading to incorrect command generation. + +To support external control while maintaining robustness, we introduce the concept of **tracked address states**. These allow TSR to be aware of and react to externally-triggered changes on a per-address basis. + +## Principles of Address States + +Address states represent granular, trackable substates for specific device control addresses (e.g., a channel on an audio mixer, a switcher’s ME state). Each address state is tracked in 2 ways: + +- **Internal State:** by TSR’s own understanding of what the state should be +- **External State:** via state feedback from the device + +This dual tracking allows TSR to understand when a device has been manipulated outside of its control. + +## Detecting External Changes + +To detect that a device is no longer in the timeline-driven state, you can enable external state tracking in your integration implementation. + +The process includes: + +1. **Receiving External State Updates:** + Your integration should listen for incoming updates from the device via its native protocol (e.g., TCP, UDP, HTTP API). + +2. **Tracking Updated Address States:** + Use the `setAddressState` method on the integration context to notify TSR of updated state for specific addresses. + +3. **Marking the Address as ahead:** + After a small debounce time the TSR will call the `diffAddressStates` method on your integration implementation to establish whether the updated External State is different from the Internal State. If it is, then the address will be marked as being ahead of the timeline. + +The TSR will take care of tracking the Internal state and modifying the states when necessary through the `applyAddressState` method on your integration implementation. + +## When to Reassert Control + +Reasserting control means allowing TSR to override the current state of the device to bring it back in line with the timeline. Whether and when to do this is integration-specific, and the system is designed to allow flexible control. + +Your integration should implement the `addressStateReassertsControl` method to signal when this happens. + +Common use cases include: + +- A new timeline object has begun +- The user explicitly re-enables timeline control + +## Implementation + +A few things need to be added to an existing integration to enable the Shared Hardware Control mechanism: + +1. Adjust the `convertTimelineStateToDeviceState` to output Address States +    - Part of this step is to make a design choice in the granularity of your Address States +    - The addresses you return for each Address State must be unique to that Address State and you must be able to connect them with updates you receive from the device +    - The Address State must include the values you want to use to establish when control should be reasserted +2. Process updates from the external device +    - After receiving an update from a device it has to be converted into Address States and Addresses +    - Call `this.context.setAddressState` for each updated Address State +3. Implement `addressStateReassertsControl` method +    - Your implementation will be given an old address state and a new one, it is up to you to tell the TSR whether this change in address state implies that control should be reasserted. +4. Implement `diffAddressStates` method +    - Your implementation must be able to take in 2 Address States and return a boolean value `true` if the 2 Address States are different and `false` if they are equivalent. +5. Implement `applyAddressState` method +    - In this method you should copy the contents from an Address State onto the Device State output of your `convertTimelineStateToDeviceState` implementation + +## Notes + +The Shared Hardware Control system is opt-in. If your device does not need to support external control, the standard TSR behavior will remain unaffected. In addition, there is a user setting to override the Shared Hardware Control feature. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-actions.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-actions.md new file mode 100644 index 00000000000..791c6f5a26c --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-actions.md @@ -0,0 +1,11 @@ +# TSR Actions + +Sometimes a state based model isn't enough and you just need to fire an action. In Sofie we try to be strict about any playout operations needing to be state based, i.e. doing a transition operation on a vision mixer should be a result of a state change, not an action. However, there are things that are easier done with actions. For example cleaning up a playlist on a graphics server or formatting a disk on a recorder. For these scenarios we have added TSR Actions. + +TSR Actions can be triggered through the UI by a user, through blueprints when the rundown is activated or deactivated or through adlib actions. + +When implementing the TSR Actions API you should start by defining a JSON schema outlying the action id's and payload your integration will consume. Once you've done this you're ready to implement the actions as callbacks on the `actions` property of your integration. + +:::warning +Beware that if your action changes the state of the device you should handle this appropriately by resetting the resolver +::: diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-api.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-api.md new file mode 100644 index 00000000000..f09e0f43a01 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-api.md @@ -0,0 +1,28 @@ +# TSR Integration API + +:::info +As of version 1.50, there still exists a legacy API for device integrations. In this documentation we will only consider the more modern variant informally known as the _StateHandler_ format. +::: + +## Setup and status + +There are essentially 2 parts to the TSR API, the first thing you need to do is set up a connection with the device you are integrating with. This is done in the `init` method. It takes a parameter with the Device options as specified in the config schema. Additionally a `terminate` call is to be implemented to tear down the connection and prepare any timers to be garbage collected. + +Regarding status there are 2 important methods to be implemented, one is a getter for the `connected` status of the integration and the other is `getStatus` which should inform a TSR user of the status of device. You can add messages in this status as well. + +## State and commands + +The second part is where the bulk of the work happens. First your implementation for `convertTimelineStateToDeviceState` will be called with a Timeline State and the mappings for your integration. You are ought to return a "Device State" here which is an object representing the state of your device as inferred from the Timeline State and mappings. Then the next implementation is of the `diffStates` method, which will be called with 2 Device States as you've generated them earlier. The purpose of this method is to generate commands such that a state change from Device State A to Device State B can be executed. Hence it is called a "diff". The last important method here is `sendCommand` which will be called with the commands you've generated earlier when the TSR wants to transitition from State A to State B. + +Another thing to implement is the `actions` property. You can leave it as an empty object initially or read more about it in [TSR Actions](./tsr-actions.md). + +## Logging and emitting events + +Logging is done through an event emitter as is described in the DeviceEvents interface. You should also emit an event any time the connection status should change. There is an event you can emit to rerun the resolving process in TSR as well, this will more or less create new Timeline States from the timeline, diff them and see if they should be executed. + +## Best practices + +- The `init` method is asynchronous but you should not use it to wait for timeouts in your connection to reject it. Instead the rest of your integration should gracefully deal with a (initially) disconnected device. +- The result of the `getStatus` method is displayed in the UI of Sofie so try to put helpful information in the messages and only elevate to a "bad" status if something is really wrong, like being fully disconnected from a device. +- Be aware for side effects in your implementations of `convertTimelineStateToDeviceState` and `diffStates` they are _not_ guaranteed to be chronological and the states changes may never actually be executed. +- If you need to do any time aware commands (such as seeking in a media file) use the time from the Timeline State to do your calculations for these diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-plugins.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-plugins.md new file mode 100644 index 00000000000..0258aa62208 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-plugins.md @@ -0,0 +1,124 @@ +# TSR Plugins + +As of 26.03, it is possible to load additional device integrations into TSR as 'plugins'. This is intended to be an escape hatch when you need to make an integration for an internal system or for when an NDA with a device vendor does not allow for opensourcing. We still encourage anything which can be made opensource to be contributed back. + +## Creating a plugin + +It is expected that each plugin should be its own self-contained folder, including any npm dependencies. + +You can see a complete and working (at time of writing) example of this at [sofie-tsr-plugin-example](https://github.com/SuperFlyTV/sofie-tsr-plugin-example). This example is based upon a copy of the builtin atem integration. + +There are a few npm libraries which will be useful to you + +- `timeline-state-resolver-types` - Some common types from TSR are defined in here +- `timeline-state-resolver-api` - This defines the api and other types that your device integrations should implement. +- `timeline-state-resolver-tools` - This contains various tooling for building your plugin + +Some useful npm scripts you may wish to copy are: + +```js +{ + "translations:extract": "tsr-extract-translations tsr-plugin-example ./src/main.ts", + "translations:bundle": "tsr-bundle-translations tsr-plugin-example ./translations.json", + "schema:deref": "tsr-schema-deref ./src ./src/\\$schemas/generated", + "schema:types": "tsr-schema-types ./src/\\$schemas/generated ./src/generated" +} +``` + +There are a few key properties that your plugin must conform to, the rest of the structure and how it gets generated is up to you. + +1. It must be possible to `require(...)` your plugin folder. The resulting js must contain an export of the format `export const Devices: Record = {}` + This is how the TSR process finds the entrypoint for your code, and allows you to define multiple device types. + +2. There must be a `manifest.json` file at the root of your plugin folder. This should contain json in the form `Record` + This is a composite of various json schemas, we recommend generating this file with a script and using the same source schemas to generate relevant typescript types. + +3. There must be a `translations.json` file at the root of your plugin folder. This should contain json in the form `TranslationsBundle[]`. + This should contain any translation strings that should be used when displaying various things about your device in a UI. Populating this with translations is optional, you only need to do so if this is useful to your users. + +:::info +If running some of the `timeline-state-resolver-tools` scripts fails with an error relating to `cheerio`, you should add a yarn resolution (or equivalent for your package manager) to pin the version to `"cheerio": "1.0.0-rc.12"` which is compatible with our tooling. +::: + +## Using with the TSR API + +If you are using TSR in a non-sofie project, to load plugins you should: + +- construct a `DevicesRegistry` +- using the methods on this registry, load the needed plugins +- pass this registry into the `Conductor` constructor, inside the options object. + +You can mutate the contents of the `DevicesRegistry` after passing to the `Conductor`, and it will be used when spawning or restarting devices. + +## Using with Sofie + +In Sofie playout-gateway, plugins can be loaded by setting the `TSR_PLUGIN_PATHS` environment variable to any folders containing plugins. + +It is possible to extend the docker images to add in your own plugins. +You can use a dockerfile in your plugin git repository along the lines of: + +```Dockerfile +# BUILD IMAGE +FROM node:22 +WORKDIR /opt/tsr-plugin-example + +COPY . . + +RUN corepack enable +RUN yarn install +RUN yarn build +RUN yarn install --production + +# cleanup stuff we don't want in the final image +RUN rm -rf .git src + +# DEPLOY IMAGE +FROM sofietv/tv-automation-playout-gateway:release53 + +ENV TSR_PLUGIN_PATHS=/opt/tsr-plugin-example +COPY --from=0 /opt/tsr-plugin-example /opt/tsr-plugin-example +``` + +## Using in Sofie blueprints + +To use a TSR plugin in your blueprints, make sure you have your content types available in the blueprints. + +You can create a file in your src folder such as `tsr-types.d.ts` with content being something like: + +```ts +import type { FakeDeviceType, TimelineContentFakeAny } from './test-types.js' + +declare module 'timeline-state-resolver-types' { + interface TimelineContentMap { + [FakeDeviceType]: TimelineContentFakeAny + } +} +``` + +The `FakeDeviceType` should be defined as `export const FakeDeviceType = 'fake' as const` and should be used as the deviceType property of your types. + +A minimal example of the types is: + +```ts +export const FakeDeviceType = 'fake' as const + +export declare enum TimelineContentTypeFake { + AUX = 'aux', +} + +export type TimelineContentFakeAny = TimelineContentFakeAUX + +export interface TimelineContentFakeBase { + deviceType: typeof FakeDeviceType + type: TimelineContentTypeFake +} + +export interface TimelineContentFakeAUX extends TimelineContentFakeBase { + type: TimelineContentTypeFake.AUX + aux: { + input: number + } +} +``` + +With this, all of the sofie timeline object and tsr types will accept your custom types as well as the default ones. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-types.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-types.md new file mode 100644 index 00000000000..0c9d2e5108c --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-types.md @@ -0,0 +1,7 @@ +# TSR Types + +The TSR monorepo contains a types package called `timeline-state-resolver-types`. The intent behind this package is that you may want to generate a Timeline in a place where you don't want to import the TSR library for performance reasons. Blueprints are a good example of this since the webpack setup does not deal well with importing everything. + +## What you should know about this + +When the TSR is built the types for the Mappings, Options and Actions for your integration will be auto generated under `src/generated`. In addition to this you should describe the content property of the timeline objects in a file using interfaces. If you're adding a new integration also add it to the `DeviceType` enum as described in `index.ts`. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/_category_.json b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/_category_.json new file mode 100644 index 00000000000..b4dd4fcee1f --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "For Blueprint Developers", + "position": 4 +} diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/_part-timings-demo.jsx b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/_part-timings-demo.jsx new file mode 100644 index 00000000000..98cb9f4275c --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/_part-timings-demo.jsx @@ -0,0 +1,173 @@ +import React, { useState } from 'react' + +/** + * This is a demo showing the interactions between the part and piece groups on the timeline. + * The maths should be the same as in `meteor/lib/rundown/timings.ts`, but in a simplified form + */ + +const MS_TO_PIXEL_CONSTANT = 0.1 + +const viewPortStyle = { + width: '100%', + backgroundSize: '40px 40px', + backgroundImage: + 'linear-gradient(to right, grey 1px, transparent 1px), linear-gradient(to bottom, grey 1px, transparent 1px)', + overflowX: 'hidden', + display: 'flex', + flexDirection: 'column', + position: 'relative', +} + +export function PartTimingsDemo() { + const [postrollA1, setPostrollA1] = useState(0) + const [postrollA2, setPostrollA2] = useState(0) + const [prerollB1, setPrerollB1] = useState(0) + const [prerollB2, setPrerollB2] = useState(0) + const [outTransitionDuration, setOutTransitionDuration] = useState(0) + const [inTransitionBlockDuration, setInTransitionBlockDuration] = useState(0) + const [inTransitionContentsDelay, setInTransitionContentsDelay] = useState(0) + const [inTransitionKeepaliveDuration, setInTransitionKeepaliveDuration] = useState(0) + + // Arbitrary point in time for the take to be based around + const takeTime = 2400 + + const outTransitionTime = outTransitionDuration - inTransitionKeepaliveDuration + + // The amount of time needed to preroll Part B before the 'take' point + const partBPreroll = Math.max(prerollB1, prerollB2) + const prerollTime = partBPreroll - inTransitionContentsDelay + + // The amount to delay the part 'switch' to, to ensure the outTransition has time to complete as well as any prerolls for part B + const takeOffset = Math.max(0, outTransitionTime, prerollTime) + const takeDelayed = takeTime + takeOffset + + // Calculate the part A objects + const pieceA1 = { time: 0, duration: takeDelayed + inTransitionKeepaliveDuration + postrollA1 } + const pieceA2 = { time: 0, duration: takeDelayed + inTransitionKeepaliveDuration + postrollA2 } + const partA = { time: 0, duration: Math.max(pieceA1.duration, pieceA2.duration) } // part stretches to contain the piece + + // Calculate the transition objects + const pieceOutTransition = { + time: partA.time + partA.duration - outTransitionDuration - Math.max(postrollA1, postrollA2), + duration: outTransitionDuration, + } + const pieceInTransition = { time: takeDelayed, duration: inTransitionBlockDuration } + + // Calculate the part B objects + const partBBaseDuration = 2600 + const partB = { time: takeTime, duration: partBBaseDuration + takeOffset } + const pieceB1 = { time: takeDelayed + inTransitionContentsDelay - prerollB1, duration: partBBaseDuration + prerollB1 } + const pieceB2 = { time: takeDelayed + inTransitionContentsDelay - prerollB2, duration: partBBaseDuration + prerollB2 } + const pieceB3 = { time: takeDelayed + inTransitionContentsDelay + 300, duration: 200 } + + return ( +
+
+ + + + + + + + + + + + + + + +
+ + {/* Controls */} + + + + + + + + + +
+
+ ) +} + +function TimelineGroup({ duration, time, name, color }) { + return ( +
+ {name} +
+ ) +} + +function TimelineMarker({ time, title }) { + return ( +
+   +
+ ) +} + +function InputRow({ label, max, value, setValue }) { + return ( + + {label} + + setValue(parseInt(e.currentTarget.value))} + /> + + + ) +} diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/ab-playback.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/ab-playback.md new file mode 100644 index 00000000000..1a78316f770 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/ab-playback.md @@ -0,0 +1,236 @@ +# AB Playback + +:::info +Prior to 1.50 of Sofie, this was implemented in Blueprints and not natively in Sofie-core +::: + +_AB Playback_ is a common technique for clip playback. The aim is to be able to play multiple clips back to back, alternating which player is used for each clip. +At first glance it sounds simple to handle, but it quickly becomes complicated when we consider the need to allow users to run adlibs and that the system needs to seamlessly update pre-programmed clips when this happens. + +To avoid this problem, we take an approach of labelling pieces as needing an AB assignment and leaving timeline objects to have some unresolved values during the ingest blueprint operations, and we perform the AB resolving when building the timeline for playout. + +There are other challenges to the resolving to think about too, which make this a challenging area to tackle, and not something that wants to be considered when starting out with blueprints. Some of these challenges are: + +- Users get confused if the player of a clip changes without a reason +- Reloading an already loaded clip can be costly, so should be avoided when possible +- Adlibbing a clip, or changing what Part is nexted can result in needing to move what player a clip has assigned +- Postroll or preroll is often needed +- Some studios can have less players available than ideal. (eg, going back to back between two clips, and a clip is playing on the studio monitor) + +## Defining Piece sessions + +An AB-session is a request for an AB player for the lifetime of the object or Piece. The resolver operates on these sessions, to identify when players are needed and to identify which objects and Pieces are linked and should use the same Player. + +In order for the AB resolver to know what AB sessions there are on the timeline, and how they all relate to each other, we define `abSessions` properties on various objects when defining Pieces and their content during the `getSegment` blueprint method. + +The AB resolving operates by looking at all the Pieces on the timeline, and plotting all the requested abSessions out in time. It will then iterate through each of these sessions in time order and assign them in order to the available players. +Note: The sessions of TimelineObjects are not considered at this point, except for those in lookahead. + +Both Pieces and TimelineObjects accept an array of AB sessions, and are capable of using multiple AB pools on the same object. Eg, choosing a clip player and the DVE to play it through. + +:::warning +The sessions of TimelineObjects are not considered during the resolver stage, except for lookahead objects. +If a TimelineObject has an `abSession` set, its parent Piece must declare the same session. +::: + +For example: + +```ts +const partExternalId = 'id-from-nrcs' +const piece: Piece = { + externalId: partExternalId, + name: 'My Piece', + + abSessions: [{ + sessionName: partExternalId, + poolName: 'clip' + }], + + ... +} +``` + +This declares that this Piece requires a player from the 'clip' pool, with a unique sessionName. + +:::info +The `sessionName` property is an identifier for a session within the Segment. +Any other Pieces or TimelineObjects that want to share the session should use the same sessionName. Unrelated sessions must use a different name. +::: + +## Enabling AB playback resolving + +To enable AB playback for your blueprints, the `getAbResolverConfiguration` method of a ShowStyle blueprint must be implemented. This informs Sofie that you want the AB playback logic to run, and configures the behaviour. + +A minimal implementation of this is: + +```ts +getAbResolverConfiguration: (context: IShowStyleContext): ABResolverConfiguration => { + return { + resolverOptions: { + idealGapBefore: 1000, + nowWindow: 2000, + }, + pools: { + clip: [1, 2], + }, + } +} +``` + +The `resolverOptions` property defines various configuration that will affect how sessions are assigned to players. +The `pools` property defines the AB pools in your system, along with the ids of the players in the pools. These do not have to be sequential starting from 1, and can be any numbers you wish. The order used here will define the order the resolver will assign to. + +## Updating the timeline from the assignments + +There are 3 possible strategies for applying the assignments to timeline objects. The applying and ab-resolving is done just before `onTimelineGenerate` from your blueprints is called. + +### TimelineObject Keyframes + +The simplest approach is to use timeline keyframes, which can be labelled as belong to an abSession. These keyframes must be generated during ingest. + +This strategy works best for changing inputs on a video-mixer or other scenarios where a property inside of a timeline object needs changing. + +```ts +let obj = { + id: '', + enable: { start: 0 }, + layer: 'atem_me_program', + content: { + deviceType: TSR.DeviceType.ATEM, + type: TSR.TimelineContentTypeAtem.ME, + me: { + input: 0, // placeholder + transition: TSR.AtemTransitionStyle.CUT, + }, + }, + keyframes: [ + { + id: `mp_1`, + enable: { while: '1' }, + disabled: true, + content: { + input: 10, + }, + preserveForLookahead: true, + abSession: { + pool: 'clip', + index: 1, + }, + }, + { + id: `mp_2`, + enable: { while: '1' }, + disabled: true, + content: { + input: 11, + }, + preserveForLookahead: true, + abSession: { + pool: 'clip', + index: 2, + }, + }, + ], + abSessions: [ + { + pool: 'clip', + name: 'abcdef', + }, + ], +} +``` + +This object demonstrates how keyframes can be used to perform changes based on an assigned ab player session. The object itself must be labelled with the `abSession`, in the same way as the Piece is. +Each keyframe can be labelled with an `abSession`, with only one from the pool being left active. If `disabled` is set on the keyframe, that will be unset, and the other keyframes for the pool will be removed. + +Setting `disabled: true` is not strictly necessary, but ensures that the keyframe will be inactive in case that ab-pool is not processed. +In this example we are setting `preserveForLookahead` so that the keyframes are present on lookahead objects. If not set, then the keyframes will be removed by lookahead. + +### TimelineObject layer changing + +Another apoproach is to move objects between timeline layers. For example, player 1 is on CasparCG channel 1, with player 2 on CasparCG channel 2. This requires a different mapping for each layer. + +This strategy works best for playing a clip, where the whole object needs to move to different mappings. + +To enable this, the `ABResolverConfiguration` object returned from `getAbResolverConfiguration` can have a set of rules defined with the `timelineObjectLayerChangeRules` property. + +For example: + +```ts +getAbResolverConfiguration: (context: IShowStyleContext): ABResolverConfiguration => { + return { + resolverOptions: { + idealGapBefore: 1000, + nowWindow: 2000, + }, + pools: { + clip: [1, 2], + }, + timelineObjectLayerChangeRules: { + ['casparcg_player_clip_pending']: { + acceptedPoolNames: [AbSessionPool.CLIP], + newLayerName: (playerId: number) => `casparcg_player_clip_${playerId}`, + allowsLookahead: true, + }, + }, + } +} +``` + +And a timeline object: + +```ts +const clipObject: TimelineObjectCoreExt<> = { + id: '', + enable: { start: 0 }, + layer: 'casparcg_player_clip_pending', + content: { + deviceType: TSR.DeviceType.CASPARCG, + type: TSR.TimelineContentTypeCasparCg.MEDIA, + file: 'AMB', + }, + abSessions: [ + { + pool: 'clip', + name: 'abcdef', + }, + ], +} +``` + +This will result in the timeline object being moved to `casparcg_player_clip_1` if the clip is assigned to player 1, or `casparcg_player_clip_2` if the clip is assigned to player 2. + +This is also compatible with lookahead. To do this, the `casparcg_player_clip_pending` mapping should be created with the lookahead configuration set there, this should be of type `ABSTRACT`. The AB resolver will detect this lookahead object and it will get an assignment when a player is available. Lookahead should not be enabled for the `casparcg_player_clip_1` and other final mappings, as lookahead is run before AB so it will not find any objects on those layers. + +### Custom behaviour + +Sometimes, something more complex is needed than what the other options allow for. To support this, the `ABResolverConfiguration` object has an optional property `customApplyToObject`. It is advised to use the other two approaches when possible. + +```ts +getAbResolverConfiguration: (context: IShowStyleContext): ABResolverConfiguration => { + return { + resolverOptions: { + idealGapBefore: 1000, + nowWindow: 2000, + }, + pools: { + clip: [1, 2], + }, + customApplyToObject: ( + context: ICommonContext, + poolName: string, + playerId: number, + timelineObject: OnGenerateTimelineObj + ) => { + // Your own logic here + + return false + }, + } +} +``` + +Inside this function you are able to make any changes you like to the timeline object. +Return true if the object was changed, or false if it is unchanged. This allows for logging whether Sofie failed to modify an object for an ab assignment. + +For example, we use this to remap audio channels deep inside of some Sisyfos timeline objects. It is not possible for us to do this with keyframes due to the keyframes being applied with a shallow merge for the Sisyfos TSR device. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/hold.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/hold.md new file mode 100644 index 00000000000..040e241a6e6 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/hold.md @@ -0,0 +1,52 @@ +# Hold + +_Hold_ is a feature in Sofie to allow for a special form of take between two parts. It allows for the new part to start with some portions of the old part being retained, with the next 'take' stopping the remaining portions of the old part and not performing a true take. + +For example, it could be setup to hold back the video when going between two clips, creating what is known in film editing as a [split edit](https://en.wikipedia.org/wiki/Split_edit) or [J-cut](https://en.wikipedia.org/wiki/J_cut). The first _Take_ would start the audio from an _A-Roll_ (second clip), but keep the video playing from a _B-Roll_ (first clip). The second _Take_ would stop the first clip entirely, and join the audio and video for the second clip. + +![A timeline of a J-Cut in a Non-Linear Video Editor](/img/docs/video_edit_hold_j-cut.png) + +## Flow + +While _Hold_ is active or in progress, an indicator is shown in the header of the UI. +![_Hold_ in Rundown View header](/img/docs/rundown-header-hold.png) + +It is not possible to run any adlibs while a hold is active, or to change the nexted part. Once it is in progress, it is not possible to abort or cancel the _Hold_ and it must be run to completion. If the second part has an autonext and that gets reached before the _Hold_ is completed, the _Hold_ will be treated as completed and the autonext will execute as normal. + +When the part to be held is playing, with the correct part as next, the flow for the users is: + +- Before + - Part A is playing + - Part B is nexted +- Activate _Hold_ (By hotkey or other user action) + - Part A is playing + - Part B is nexted +- Perform a take into the _Hold_ + - Part B is playing + - Portions of Part A remain playing +- Perform a take to complete the _Hold_ + - Part B is playing + +Before the take into the _Hold_, it can be cancelled in the same way it was activated. + +## Supporting Hold in blueprints + +:::note +The functionality here is a bit limited, as it was originally written for one particular use-case and has not been expanded to support more complex scenarios. +Some unanswered questions we have are: + +- Should _Hold_ be rewritten to be done with adlib-actions instead to allow for more complex scenarios? +- Should there be a way to more intelligently check if _Hold_ can be done between two Parts? (perhaps a new blueprint method?) + ::: + +The blueprints have to label parts as supporting _Hold_. +You can do this with the [`holdMode`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.IBlueprintPart.html#holdMode) property, and labelling it possible to _Hold_ from or to the part. + +Note: If the user manipulates what part is set as next, they will be able to do a _Hold_ between parts that are not sequential in the Rundown. + +You also have to label Pieces as something to extend into the _Hold_. Not every piece will be wanted, so it is opt-in. +You can do this with the [`extendOnHold`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.IBlueprintPiece.html#extendOnHold) property. The pieces will get extended in the same way as infinite pieces, but limited to only be extended into the one part. The usual piece collision and priority logic applies. + +Finally, you may find that there are some timeline objects that you don't want to use inside of the extended pieces, or there are some objects in the part that you don't want active while the _Hold_ is. +You can mark an object with the [`holdMode`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.TimelineObjectCoreExt.html#holdMode) property to specify its presence during a _Hold_. +The `HoldMode.ONLY` mode tells the object to only be used when in a _Hold_, which allows for doing some overrides in more complex scenarios. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/intro.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/intro.md new file mode 100644 index 00000000000..0dfe9486a1b --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/intro.md @@ -0,0 +1,37 @@ +--- +sidebar_position: 1 +--- + +# Introduction + +:::caution +Documentation for this page is yet to be written. +::: + +[Blueprints](../../user-guide/concepts-and-architecture.md#blueprints) are JavaScript programs that run inside Sofie Core and interpret data coming in from the Rundowns and transform that into playable elements. They use an API published in [@sofie-automation/blueprints-integration](https://sofie-automation.github.io/sofie-core/typedoc/modules/_sofie_automation_blueprints_integration.html) [TypeScript](https://www.typescriptlang.org/) library to expose their functionality and communicate with Sofie Core. + +Technically, a Blueprint is a JavaScript object, implementing one of the `BlueprintManifestBase` interfaces. + +Sofie doesn't have a built-in package manager or import, so all dependencies need to be bundled into a single `*.js` file bundle using a bundler such as [Rollup](https://rollupjs.org/) or [webpack](https://webpack.js.org/). The community has built a set of utilities called [SuperFlyTV/sofie-blueprint-tools](https://github.com/SuperFlyTV/sofie-blueprint-tools/) that acts as a nascent framework for building & bundling Blueprints written in TypeScript. + +:::info +Note that the Runtime Environment for Blueprints in Sofie is plain JavaScript at [ES2015 level](https://en.wikipedia.org/wiki/ECMAScript_version_history#6th_edition_%E2%80%93_ECMAScript_2015), so other ways of building Blueprints are also possible. +::: + +Currently, there are three types of Blueprints: + +- [Show Style Blueprints](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.ShowStyleBlueprintManifest.html) - handling converting NRCS Rundown data into Sofie Rundowns and content. +- [Studio Blueprints](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.StudioBlueprintManifest.html) - handling selecting ShowStyles for a given NRCS Rundown and assigning NRCS Rundowns to Sofie Playlists +- [System Blueprints](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie_automation_blueprints_integration.SystemBlueprintManifest.html) - handling system provisioning and global configuration + +# Show Style Blueprints + +These blueprints interpret the data coming from the [NRCS](../../user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md), meaning that they need to support the particular data structures that a given Ingest Gateway uses to store incoming data from the Rundown editor. They will need to convert Rundown Pages, Cues, Items, pieces of show script and other types of objects into [Sofie concepts](../../user-guide/concepts-and-architecture.md) such as Segments, Parts, Pieces and AdLibs. + +# Studio Blueprints + +These blueprints provide a "baseline" Timeline that is being used by your Studio whenever there isn't a Rundown active. They also handle combining Rundowns into RundownPlaylists. Via the [`applyConfig`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie-automation_blueprints-integration.StudioBlueprintManifest.html#applyconfig) method, these Blueprints enable a _Configuration-as-Code_ approach to configuring connections to various elements of your Control Room and Studio. + +# System Blueprints + +These blueprints exist to allow a _Configuration-as-Code_ approach to an entire Sofie system. This is done via the [`applyConfig`](https://sofie-automation.github.io/sofie-core/typedoc/interfaces/_sofie-automation_blueprints-integration.SystemBlueprintManifest.html#applyconfig) providing personality information such as global system configuration or system-wide HotKeys via the Blueprints. \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/lookahead.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/lookahead.md new file mode 100644 index 00000000000..f1d10c34381 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/lookahead.md @@ -0,0 +1,96 @@ +# Lookahead + +Lookahead allows Sofie to look into future Parts and Pieces, in order to preload or preview what is coming up. The aim is to fill in the gaps between your TimelineObjects with lookahead versions of these objects. +In this way, it can be used to provide functionality such as an AUX on your vision mixer showing the next cut, or to load the next clip into the media player. + +## Defining + +Lookahead can be enabled by configuring a few properties on a mapping: + +```ts +/** What method core should use to create lookahead objects for this layer */ +lookahead: LookaheadMode +/** The minimum number lookahead objects to create from future parts for this layer. Default = 1 */ +lookaheadDepth?: number +/** Maximum distance to search for lookahead. Default = undefined */ +lookaheadMaxSearchDistance?: number +``` + +With `LookaheadMode` defined as: + +```ts +export enum LookaheadMode { + /** + * Disable lookahead for this layer + */ + NONE = 0, + /** + * Preload content with a secondary layer. + * This requires support from the TSR device, to allow for preloading on a resource at the same time as it being on air. + * For example, this allows for your TimelineObjects to control the foreground of a CasparCG layer, with lookahead controlling the background of the same layer. + */ + PRELOAD = 1, + /** + * Fill the gaps between the planned objects on a layer. + * This is the primary lookahead mode, and appears to TSR devices as a single layer of simple objects. + */ + WHEN_CLEAR = 3, +} +``` + +If undefined, `lookaheadMaxSearchDistance` currently has a default distance of 10 parts. This number was chosen arbitrarily, and could change in the future. Be careful when choosing a distance to not set it too high. All the Pieces from the parts being searched have to be loaded from the database, which can come at a noticeable cost. + +If you are doing [AB Playback](./ab-playback.md), or performing some other processing of the timeline in `onTimelineGenerate`, you may benefit from increasing the value of `lookaheadDepth`. In the case of AB Playback, you will likely want to set it to the number of players available in your pool. + +Typically, TimelineObjects do not need anything special to support lookahead, other than a sensible `priority` value. Lookahead objects are given a priority between `0` and `0.1`. Generally, your baseline objects should have a priority of `0` so that they are overridden by lookahead, and any objects from your Parts and Pieces should have a priority of `1` or higher, so that they override lookahead objects. + +If there are any keyframes on TimelineObjects that should be preserved when being converted to a lookahead object, they will need the `preserveForLookahead` property set. + +## How it works + +Lookahead is calculated while the timeline is being built, and searches based on the playhead, rather than looking at the planned Parts. + +The searching operates per-layer first looking at the current PartInstance, then the next PartInstance and then any Parts after the next PartInstance in the rundown. Any Parts marked as `invalid` or `floated` are ignored. This is what allows lookahead to be dynamic based on what the User is doing and intending to play. + +It is searching Parts in that order, until it has either searched through the `lookaheadMaxSearchDistance` number of Parts, or has found at least `lookaheadDepth` future timeline objects. + +Any pieces marked as `pieceType: IBlueprintPieceType.InTransition` will be considered only if playout intends to use the transition. +If an object is found in both a normal piece with `{ start: 0 }` and in an InTransition piece, then the objects from the normal piece will be ignored. + +These objects are then processed and added to the timeline. This is done in one of two ways: + +1. As timed objects. + If the object selected for lookahead is already on the timeline (it is in the current part, or the next part and autonext is enabled), then timed lookahead objects are generated. These objects are to fill in the gaps, and get their `enable` object to reference the objects on the timeline that they are filling between. + The `lookaheadDepth` setting of the mapping is ignored for these objects. + +2. As future objects. + If the object selected for lookahead is not on the timeline, then simpler objects are generated. Instead, these get an enable of either `{ while: '1' }`, or set to start after the last timed object on that layer. This lets them fill all the time after any other known objects. + The `lookaheadDepth` setting of the mapping is respected for these objects, with this number defining the **minimum** number future objects that will be produced. These future objects are inserted with a decreasing `priority`, starting from 0.1 decreasing down to but never reaching 0. + When using the `WHEN_CLEAR` lookahead mode, all but the first will be set as `disabled`, to ensure they aren't considered for being played out. These `disabled` objects can be used by `onTimelineGenerate`, or they will be dropped from the timeline if left `disabled`. + When there are multiple future objects on a layer, only the first is useful for playout directly, but the others are often utilised for [AB Playback](./ab-playback.md) + +Some additional changes done when processing each lookahead timeline object: + +- The `id` is processed to be unique +- The `isLookahead` property is set as true +- If the object has any keyframes, any not marked with `preserveForLookahead` are removed +- The object is removed from any group it was contained within +- If the lookahead mode used is `PRELOAD`, then the layer property is changed, with the `lookaheadForLayer` property set to indicate the layer it is for. + +The resulting objects are appended to the timeline and included in the call to `onTimelineGenerate` and the [AB Playback](./ab-playback.md) resolving. + +## Advanced Scenarios + +Because the lookahead objects are included in the timeline to `onTimelineGenerate`, this gives you the ability to make changes to the lookahead output. + +[AB Playback](./ab-playback.md) started out as being implemented inside of `onTimelineGenerate` and relies on lookahead objects being produced before reassigning them to other mappings. + +If any objects found by lookahead have a class `_lookahead_start_delay`, they will be given a short delay in their start time. This is a hack introduced to workaround a timing issue. At some point this will be removed once a proper solution is found. + +Sometimes it can be useful to have keyframes which are only applied when in lookahead. That can be achieved by setting `preserveForLookahead`, making the keyframe be disabled, and then re-enabling it inside `onTimelineGenerate` at the correct time. + +It is possible to implement a 'next' AUX on your vision mixer by: + +- Setup this mapping with `lookaheadDepth: 1` and `lookahead: LookaheadMode.WHEN_CLEAR` +- Each Part creates a TimelineObject on this mapping. Crucially, these have a priority of 0. +- Lookahead will run and will insert its objects overriding your predefined ones (because of its higher priority). Resulting in the AUX always showing the lookahead object. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/manipulating-ingest-data.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/manipulating-ingest-data.md new file mode 100644 index 00000000000..3b01e885cba --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/manipulating-ingest-data.md @@ -0,0 +1,139 @@ +# Manipulating Ingest Data + +In Sofie we receive the rundown from an NRCS in the form of the `IngestRundown`, `IngestSegment` and `IngestPart` types. ([Source Code](https://github.com/Sofie-Automation/sofie-core/blob/master/packages/shared-lib/src/peripheralDevice/ingest.ts)) +These are passed into the `getRundown` or `getSegment` blueprints methods to transform them into a Rundown that Sofie can display and play. + +At times it can be useful to manipulate this data before it gets passed into these methods. This wants to be done before `getSegment` in order to limit the scope of the re-generation needed. We could have made it so that `getSegment` is able to view the whole `IngestRundown`, but that would mean that any change to the `IngestRundown` would require re-generating every segment. This would be costly and could have side effects. + +A new method `processIngestData` was added to transform the `NRCSIngestRundown` into a `SofieIngestRundown`. The types of the two are the same, so implementing the `processIngestData` method is optional, with the default being to pass through the NRCS rundown unchanged. (There is an exception here for MOS, which is explained below). + +The basic implementation of this method which simply propagates nrcs changes is: + +```ts +function processIngestData( + context: IProcessIngestDataContext, + mutableIngestRundown: MutableIngestRundown, + nrcsIngestRundown: IngestRundown, + previousNrcsIngestRundown: IngestRundown | undefined, + changes: NrcsIngestChangeDetails | UserOperationChange +) { + if (changes.source === 'ingest') { + blueprintContext.defaultApplyIngestChanges(mutableIngestRundown, nrcsIngestRundown, changes) + } +} +``` + +In this method, the key part is the `mutableIngestRundown` which is the `IngestRundown` that will get used for `getRundown` and `getSegment` later. It is a class with various mutator methods which allows Sofie to cheaply check what has changed and know what needs to be regenerated. (We did consider performing deep diffs, but were concerned about the cost of diffing these very large rundown objects). +This object internally contains an `IngestRundown`. + +The `nrcsIngestRundown` parameter is the full `IngestRundown` as seen by the NRCS. The `previousNrcsIngestRundown` parameter is the `nrcsIngestRundown` from the previous call. This is to allow you to perform any comparisons between the data that may be useful. + +The `changes` object is a structure that defines what the NRCS provided changes for. The changes have already been applied onto the `nrcsIngestRundown`, this provides a description of what/where the changes were applied to. + +Finally, the `blueprintContext.defaultApplyIngestChanges` call is what performs the 'magic'. Inside of this it is interpreting the `changes` object, and calling the appropriate methods on `mutableIngestRundown`. It is expected that this logic should be able to handle most use cases, but there may be some where they need something custom, so it is completely possible to reimplement inside blueprints. + +So far this has ignored that the `changes` object can be of type `UserOperationChange`; this is explained below. + +## Modifying NRCS Ingest Data + +MOS does not have Segments, to handle this Sofie creates a Segment and Part for each MOS Story, expecting them to be grouped later if needed. + +In the past Sofie has had a hardcoded grouping logic, based on how NRK define this as a prefix in the Part names. Obviously this doesn't work for everyone, so this needed to be made more customisable. (This is still the default behaviour when `processIngestData` is not implemented) + +To perform the NRK grouping behaviour the following implementation can be used: + +```ts +function processIngestData( + context: IProcessIngestDataContext, + mutableIngestRundown: MutableIngestRundown, + nrcsIngestRundown: IngestRundown, + previousNrcsIngestRundown: IngestRundown | undefined, + changes: NrcsIngestChangeDetails | UserOperationChange +) { + if (changes.source === 'ingest') { + // Group parts by interpreting the slug to be in the form `SEGMENTNAME;PARTNAME` + const groupedResult = context.groupMosPartsInRundownAndChangesWithSeparator( + nrcsIngestRundown, + previousNrcsIngestRundown, + ingestRundownChanges.changes, + ';' // Backwards compatibility + ) + + context.defaultApplyIngestChanges( + mutableIngestRundown, + groupedResult.nrcsIngestRundown, + groupedResult.ingestChanges + ) + } +} +``` + +There is also a helper method for doing your own logic: + +```ts +function processIngestData( + context: IProcessIngestDataContext, + mutableIngestRundown: MutableIngestRundown, + nrcsIngestRundown: IngestRundown, + previousNrcsIngestRundown: IngestRundown | undefined, + changes: NrcsIngestChangeDetails | UserOperationChange +) { + if (changes.source === 'ingest') { + // Group parts by some custom logic + const groupedResult = context.groupPartsInRundownAndChanges( + nrcsIngestRundown, + previousNrcsIngestRundown, + ingestRundownChanges.changes, + (segments) => { + // TODO - perform the grouping here + return segmentsAfterMyChanges + } + ) + + context.defaultApplyIngestChanges( + mutableIngestRundown, + groupedResult.nrcsIngestRundown, + groupedResult.ingestChanges + ) + } +} +``` + +Both of these return a modified `nrcsIngestRundown` with the changes applied, and a new `changes` object which is similarly updated to match the new layout. + +You can of course do any portions of this yourself if you desire. + +## User Edits + +In some cases, it can be beneficial to allow the user to perform some editing of the Rundown from within the Sofie UI. AdLibs and AdLib Actions can allow for some of this to be done in the current and next Part, but this is limited and doesn't persist when re-running the Part. + +The idea here is that the UI will be given some descriptors on operations it can perform, which will then make calls to `processIngestData` so that they can be applied to the IngestRundown. Doing it at this level allows things to persist and for decisions to be made by blueprints over how to merge the changes when an update for a Part is received from the NRCS. + +This page doesn't go into how to define the editor for the UI, just how to handle the operations. + +There are a few Sofie defined definitions of operations, but it is also expected that custom operations will be defined. You can check the Typescript types for the builtin operations that you might want to handle. + +For example, it could be possible for Segments to be locked, so that any NRCS changes for them are ignored. + +```ts +function processIngestData( + context: IProcessIngestDataContext, + mutableIngestRundown: MutableIngestRundown, + nrcsIngestRundown: IngestRundown, + previousNrcsIngestRundown: IngestRundown | undefined, + changes: NrcsIngestChangeDetails | UserOperationChange +) { + if (changes.source === 'ingest') { + for (const segment of mutableIngestRundown.segments) { + delete ingestRundownChanges.changes.segmentChanges[segment.externalId] + // TODO - does this need to revert nrcsIngestRundown too? + } + + blueprintContext.defaultApplyIngestChanges(mutableIngestRundown, nrcsIngestRundown, changes) + } else if (changes.source === 'user') { + if (changes.operation.id === 'lock-segment') { + mutableIngestRundown.getSegment(changes.operationTarget.segmentExternalId)?.setUserEditState('locked', true) + } + } +} +``` diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/mos-statuses.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/mos-statuses.md new file mode 100644 index 00000000000..ab57f5c1059 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/mos-statuses.md @@ -0,0 +1,53 @@ +# MOS Statuses + +Sofie is able to report statuses back to stories and objects in the NRCS. This is driven by blueprints defining properties during Ingest. + +:::tip +For any statuses to be sent, this must be enabled on the gateway. There are some additional properties too, to limit what is sent. This is described in the [MOS Gateway Installation Guide]('../../../../user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md). +::: + +# Part Properties + +All of these properties reside on the IBlueprintPart that are returned from `getSegment`. + +```ts +/** The externalId of the part as expected by the NRCS. If not set, the externalId property will be used */ +ingestNotifyPartExternalId?: string + +/** Set to true if ingest-device should be notified when this part starts playing */ +shouldNotifyCurrentPlayingPart?: boolean + +/** Whether part should be reported as ready to the ingest-device. Set to undefined/null to disable this reporting */ +ingestNotifyPartReady?: boolean | null + +/** Report items as ready to the ingest-device. Only named items will be reported, using the boolean value provided */ +ingestNotifyItemsReady?: IngestPartNotifyItemReady[] +``` + +## Examples + +### Simple Statuses + +For the most basic setup, of Sofie Reporting `PLAY` and `STOP` to the NRCS at activation and while playing a rundown you need to perform the following steps. + +1. Enable the `Write Statuses to NRCS` setting in the MOS gateway setting +1. For each part that should report `PLAY` and `STOP` statuses, set `shouldNotifyCurrentPlayingPart: true`. + If your part `externalId` properties do not match the `externalId` of the NRCS data, you will need to set `ingestNotifyPartExternalId` to the NRCS `externalId`, so that the MOS gateway can match up the statuses to the NRCS data. + +Optionally, you may also wish to report `READY` or `NOTREADY` statuses to the NRCS for any stories which have not been played or set as next. You can do this by setting `ingestNotifyPartReady`. A `true` value means `READY`, with `false` meaning `NOTREADY`. Leaving it unset or `undefined` will skip reporting these statuses. + +### MOS Item Statuses + +You can also report statuses for MOS items if needed. These can be set based on Package Manager statuses, as they can trigger the ingest of a part to be rerun. With this you can build status reporting based on whether clips are ready for playout. + +Because Sofie Pieces rarely map 1:1 with MOS items, these statuses are not done via pieces, but instead the `ingestNotifyItemsReady` is used. +This property is a simple array of: + +```ts +export interface IngestPartNotifyItemReady { + externalId: string + ready: boolean +} +``` + +Only items which are present in this array will have statuses reported. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/part-and-piece-timings.mdx b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/part-and-piece-timings.mdx new file mode 100644 index 00000000000..8c2b6e8e694 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/part-and-piece-timings.mdx @@ -0,0 +1,141 @@ +import { PartTimingsDemo } from './_part-timings-demo' + +# Part and Piece Timings + +Parts and pieces are the core groups that form the timeline, and define start and end caps for the other timeline objects. + +When referring to the timeline in this page, we mean the built timeline objects that is sent to playout-gateway. +It is made of the previous PartInstance, the current PartInstance and sometimes the next PartInstance. + +### The properties + +These are stripped down interfaces, containing only the properties that are relevant for the timeline generation: + +```ts +export interface IBlueprintPart { + /** Should this item should progress to the next automatically */ + autoNext?: boolean + /** How much to overlap on when doing autonext */ + autoNextOverlap?: number + + /** Timings for the inTransition, when supported and allowed */ + inTransition?: IBlueprintPartInTransition + + /** Should we block the inTransition when starting the next Part */ + disableNextInTransition?: boolean + + /** Timings for the outTransition, when supported and allowed */ + outTransition?: IBlueprintPartOutTransition + + /** Expected duration of the line, in milliseconds */ + expectedDuration?: number +} + +/** Timings for the inTransition, when supported and allowed */ +export interface IBlueprintPartInTransition { + /** Duration this transition block a take for. After this time, another take is allowed which may cut this transition off early */ + blockTakeDuration: number + /** Duration the previous part be kept playing once the transition is started. Typically the duration of it remaining in-vision */ + previousPartKeepaliveDuration: number + /** Duration the pieces of the part should be delayed for once the transition starts. Typically the duration until the new part is in-vision */ + partContentDelayDuration: number +} + +/** Timings for the outTransition, when supported and allowed */ +export interface IBlueprintPartOutTransition { + /** How long to keep this part alive after taken out */ + duration: number +} + +export interface IBlueprintPiece { + /** Timeline enabler. When the piece should be active on the timeline. */ + enable: { + start: number | 'now' // 'now' is only valid from adlib-actions when inserting into the current part + duration?: number + } + + /** Whether this piece is a special piece */ + pieceType: IBlueprintPieceType + + /// from IBlueprintPieceGeneric: + + /** Whether and how the piece is infinite */ + lifespan: PieceLifespan + + /** + * How long this piece needs to prepare its content before it will have an effect on the output. + * This allows for flows such as starting a clip playing, then cutting to it after some ms once the player is outputting frames. + */ + prerollDuration?: number +} + +/** Special types of pieces. Some are not always used in all circumstances */ +export enum IBlueprintPieceType { + Normal = 'normal', + InTransition = 'in-transition', + OutTransition = 'out-transition', +} +``` + +### Concepts + +#### Piece Preroll + +Often, a Piece will need some time to do some preparation steps on a device before it should be considered as active. A common example is playing a video, as it often takes the player a couple of frames before the first frame is output to SDI. +This can be done with the `prerollDuration` property on the Piece. A general rule to follow is that it should not have any visible or audible effect on the output until `prerollDuration` has elapsed into the piece. + +When the timeline is built, the Pieces get their start times adjusted to allow for every Piece in the part to have its preroll time. If you look at the auto-generated pieceGroup timeline objects, their times will rarely match the times specified by the blueprints. Additionally, the previous Part will overlap into the Part long enough for the preroll to complete. + +Try the interactive to see how the prerollDuration properties interact. + +#### In Transition + +The in transition is a special Piece that can be played when taking into a Part. It is represented as a Piece, partly to show the user the transition type and duration, and partly to allow for timeline changes to be applied when the timeline generation thinks appropriate. + +When the `inTransition` is set on a Part, it will be applied when taking into that Part. During this time, any Pieces with `pieceType: IBlueprintPieceType.InTransition` will be added to the timeline, and the `IBlueprintPieceType.Normal` Pieces in the Part will be delayed based on the numbers from `inTransition` + +Try the interactive to see how the an inTransition affects the Piece and Part layout. + +#### Out Transition + +The out transition is a special Piece that gets played when taking out of the Part. It is intended to allow for some 'visual cleanup' before the take occurs. + +In effect, when `outTransition` is set on a Part, the take out of the Part will be delayed by the duration defined. During this time, any pieces with `pieceType: IBlueprintPieceType.OutTransition` will be added to the timeline and will run until the end of the Part. + +Try the interactive to see how this affects the Parts. + +### Piece postroll + +Sometimes rather than extending all the pieces and playing an out transition piece on top we want all pieces to stop except for 1, this has the same goal of 'visual cleanup' as the out transition but works slightly different. The main concept is that an out transition delays the take slightly but with postroll the take executes normally however the pieces with postroll will keep playing for a bit after the take. + +When the `postrollDuration` is set on a piece the part group will be extended slightly allowing pieces to play a little longer, however any piece that do not have postroll will end at their regular time. + +#### Autonext + +Autonext is a way for a Part to be made a fixed length. After playing for its `expectedDuration`, core will automatically perform a take into the next part. This is commonly used for fullscreen videos, to exit back to a camera before the video freezes on the last frame. It is enabled by setting the `autoNext: true` on a Part, and requires `expectedDuration` to be set to a duration higher than `1000`. + +In other situations, it can be desirable for a Part to overlap the next one for a few seconds. This is common for Parts such as a title sequence or bumpers, where the sequence ends with an keyer effect which should reveal the next Part. +To achieve this you can set `autoNextOverlap: 1000 // ms` to make the parts overlap on the timeline. In doing so, the in transition for the next Part will be ignored. + +The `autoNextOverlap` property can be thought of an override for the intransition on the next part defined as: + +```ts +const inTransition = { + blockTakeDuration: 1000, + partContentDelayDuration: 0, + previousPartKeepaliveDuration: 1000, +} +``` + +#### Infinites + +Pieces with an infinite lifespan (ie, not `lifespan: PieceLifespan.WithinPart`) get handled differently to other pieces. + +Only one pieceGroup is created for an infinite Piece which is present in multiple of the current, next and previous Parts. +The Piece calculates and tracks its own started playback times, which is preserved and reused in future takes. On the timeline it lives outside of the partGroups, but still gets the same caps applied when appropriate. + +### Interactive timings demo + +Use the sliders below to see how various Preroll and In & Out Transition timing properties interact with each other. + + diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/sync-ingest-changes.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/sync-ingest-changes.md new file mode 100644 index 00000000000..05eceb4b0d0 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/sync-ingest-changes.md @@ -0,0 +1,23 @@ +--- +title: Sync Ingest Changes +--- + +Since PartInstances and PieceInstances were added to Sofie, the default behaviour in Sofie is to not propagate any ingest changes from a Part onto its PartInstances. + +This is a safety net as without a detailed understanding of the Part and the change, we can't know whether it is safe to make on air. Without this, it would be possible for the user to change a clip name in the NRCS, and for Sofie to happily propagate that could result in a sudden change of clip mid sentence, or black if the clip needed to be copied to the playout server. This gets even more complicated when we consider that an adlib-action could have already modified a PartInstance, with changes that should likely not be overwritten with the newly ingested Part. + +Instead, this propagation can be implemented by a ShowStyle blueprint in the `syncIngestUpdateToPartInstance` method, in this way the implementation can be tailored to understand the change and its potential impact. This method is able to update the previous, current and next PartInstances. Any PartInstances older than the previous is no longer being used on the timeline so is now simply a record of how it was played and updating it would have no benefit. Sofie never has any further than the next PartInstance generated, so for any Part after that the Part is all that exists for it, so any changes will be used when it becomes the next. + +In this blueprint method, you are able to update almost any of the properties that are available to you both during ingest, and during adlib actions. It is possible the leave the Part in a broken state after this, so care must be taken to ensure it is not. If the call to your method throws an uncaught error, the changes you have made so far will be discarded but the rest of the ingest operation will continue as normal. + +### Tips + +- You should make use of the `metaData` fields on each Part and Piece to help work out what has changed. At NRK, the parsed ingest data is stored (after converting the MOS to an intermediary json format) for the Part here, so that we can do a detailed diff to figure out whether a change is safe to accept. + +- You should track in `metaData` whether a part has been modified by an adlib-action in a way that makes this sync unsafe. + +- At NRK, Pieces are differentiated into `primary`, `secondary`, `adlib`. This allows more granular control of updates. + +- `newData.part` will be `undefined` when the PartInstance is orphaned. Generally, it's useful to differentiate the behavior of the implementation of this function based on `existingPartInstance.partInstance.orphaned` state + +- `playStatus: previous` means that the currentPartInstance is `orphaned: adlib-part` and thus possibly depends on an already past PartInstance for some of it's properties. Therefore the blueprint is allowed to modify the most recently played non-adlibbed PartInstance using ingested data. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/timeline-datastore.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/timeline-datastore.md new file mode 100644 index 00000000000..ae18c75c05f --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/timeline-datastore.md @@ -0,0 +1,85 @@ +# Timeline Datastore + +The timeline datastore is a key-value store that can be used in conjunction with the timeline. The benefit of modifying values in the datastore is that the timings in the timeline are not modified so we can skip a lot of complicated calculations which reduces the system response time. An example usecase of the datastore feature is a fastpath for cutting cameras. + +## API + +In order to use the timeline datastore feature 2 API's are to be used. The timeline object has to contain a reference to a key in the datastore and the blueprints have to add a value for that key to the datastore. These references are added on the content field. + +### Timeline API + +```ts +/** + * An object containing references to the datastore + */ +export interface TimelineDatastoreReferences { + /** + * localPath is the path to the property in the content object to override + */ + [localPath: string]: { + /** Reference to the Datastore key where to fetch the value */ + datastoreKey: string + /** + * If true, the referenced value in the Datastore is only applied after the timeline-object has started (ie a later-started timeline-object will not be affected) + */ + overwrite: boolean + } +} +``` + +### Timeline API example + +```ts +const tlObj = { + id: 'obj0', + enable: { start: 1000 }, + layer: 'layer0', + content: { + deviceType: DeviceType.Atem, + type: TimelineObjectAtem.MixEffect, + + $references: { + 'me.input': { + datastoreKey: 'camInput', + overwrite: true, + }, + }, + + me: { + input: 1, + transition: TransitionType.Cut, + }, + }, +} +``` + +### Blueprints API + +Values can be added and removed from the datastore through the adlib actions API. + +```ts +interface DatastoreActionExecutionContext { + setTimelineDatastoreValue(key: string, value: unknown, mode: DatastorePersistenceMode): Promise + removeTimelineDatastoreValue(key: string): Promise +} + +enum DatastorePersistenceMode { + Temporary = 'temporary', + indefinite = 'indefinite', +} +``` + +The data persistence mode work as follows: + +- Temporary: this key-value pair may be cleaned up if it is no longer referenced to from the timeline, in practice this will currently only happen during deactivation of a rundown +- This key-value pair may _not_ be automatically removed (it can still be removed by the blueprints) + +The above context methods may be used from the usual adlib actions context but there is also a special path where none of the usual cached data is available, as loading the caches may take some time. The `executeDataStoreAction` method is executed just before the `executeAction` method. + +## Example use case: camera cutting fast path + +Assuming a set of blueprints where we can cut camera's a on a vision mixer's mix effect by using adlib pieces, we want to add a fast path where the camera input is changed through the datastore first and then afterwards we add the piece for correctness. + +1. If you haven't yet, convert the current camera adlibs to adlib actions by exporting the `IBlueprintActionManifest` as part of your `getRundown` implementation and implementing an adlib action in your `executeAction` handler that adds your camera piece. +2. Modify any camera pieces (including the one from your adlib action) to contain a reference to the datastore (See the timeline API example) +3. Implement an `executeDataStoreAction` handler as part of your blueprints, when this handler receives the action for your camera adlib it should call the `setTimelineDatastoreValue` method with the key you used in the timeline object (In the example it's `camInput`), the new input for the vision mixer and the `DatastorePersistenceMode.Temporary` persistence mode. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/intro.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/intro.md new file mode 100644 index 00000000000..6b5caa33caa --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/intro.md @@ -0,0 +1,15 @@ +--- +sidebar_label: Introduction +sidebar_position: 1 +--- + +# For Developers + +The pages below are intended for developers of any of the Sofie-related repos and/or blueprints. + +A read-through of the [Concepts & Architectures](../user-guide/concepts-and-architecture.md) is recommended, before diving too deep into development. + +- [Libraries](libraries.md) +- [Contribution Guidelines](contribution-guidelines.md) +- [For Blueprint Developers](for-blueprint-developers/intro.md) +- [API Documentation](api-documentation.md) diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/json-config-schema.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/json-config-schema.md new file mode 100644 index 00000000000..6567cbc6761 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/json-config-schema.md @@ -0,0 +1,218 @@ +--- +sidebar_label: JSON Config Schema +sidebar_position: 7 +--- + +# JSON Config Schema + +So that Sofie does not have to be aware of every type of gateway that may connect to it, each gateway provides a manifest describing itself and the configuration fields that it has. + +Since version 1.50, this is done using [JSON Schemas](https://json-schema.org/). This allows schemas to be written, with typescript interfaces generated from the schema, and for the same schema to be used to render a flexible UI. +We recommend using [json-schema-to-typescript](https://github.com/bcherny/json-schema-to-typescript) to generate typescript interfaces. + +Only a subset of the JSON Schema specification is supported, and some additional properties are used for the UI. + +We expect this subset to grow over time as more sections are found to be useful to us, but we may proceed cautiously to avoid constantly breaking other applications that use TSR and these schemas. + +## Non-standard properties + +We use some non-standard properties to help the UI render with friendly names. + +### `ui:category` + +Note: Only valid for blueprint configuration. + +Category of the property + +### `ui:title` + +Title of the property + +### `ui:description` + +Description/hint for the property + +### `ui:summaryTitle` + +If set, when in a table this property will be used as part of the summary with this label + +### `ui:zeroBased` + +If an integer property, whether to treat it as zero-based + +### `ui:displayType` + +Override the presentation with a special mode. + +Currently only valid for: + +- object properties. Valid values are 'json'. +- string properties. Valid values are 'base64-image'. +- boolean properties. Valid values are 'switch'. + +### `tsEnumNames` + +This is primarily for `json-schema-to-typescript`. + +Names of the enum values as generated for the typescript enum, which we display in the UI instead of the raw values + +### `ui:sofie-enum` & `ui:sofie-enum:filter` + +Note: Only valid for blueprint configuration. + +Sometimes it can be useful to reference other values. This property can be used on string fields, to let Sofie generate a dropdown populated with values valid in the current context. + +#### `mappings` + +Valid for both show-style and studio blueprint configuration + +This will provide a dropdown of all mappings in the studio, or studios where the show-style can be used. + +Setting `ui:sofie-enum:filter` to an array of strings will filter the dropdown by the specified DeviceType. + +#### `source-layers` + +Valid for only show-style blueprint configuration. + +This will provide a dropdown of all source-layers in the show-style. + +Setting `ui:sofie-enum:filter` to an array of numbers will filter the dropdown by the specified SourceLayerType. + +### `ui:import-export` + +Valid only for tables, this allows for importing and exporting the contents of the table. + +## Supported types + +Any JSON Schema property or type is allowed, but will be ignored if it is not supported. + +In general, if a `default` is provided, we will use that as a placeholder in the input field. + +### `object` + +This should be used as the root of your schema, and can be used anywhere inside it. The properties inside any object will be shown if they are supported. + +You may want to set the `title` property to generate a typescript interface for it. + +See the examples to see how to create a table for an object. + +`ui:displayType` can be set to `json` to allow for manual editing of an arbitrary json object. + +### `integer` + +`enum` can be set with an array of values to turn it into a dropdown. + +### `number` + +### `boolean` + +### `string` + +`enum` can be set with an array of values to turn it into a dropdown. + +`ui:sofie-enum` can be used to make a special dropdown. + +### `array` + +The behaviour of this depends on the type of the `items`. + +#### `string` + +`enum` can be set with an array of values to turn it into a dropdown + +`ui:sofie-enum` can be used to make a special dropdown. + +Otherwise is treated as a multi-line string, stored as an array of strings. + +#### `object` + +This is not available in all places we use this schema. For example, Mappings are unable to use this, but device configuration is. Additionally, using it inside of another object-array is not allowed. + +## Examples + +Below is an example of a simple schema for a gateway configuration. The subdevices are handled separately, with their own schema. + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/product.schema.json", + "title": "Mos Gateway Config", + "type": "object", + "properties": { + "mosId": { + "type": "string", + "ui:title": "MOS ID of Mos-Gateway (Sofie MOS ID)", + "ui:description": "MOS ID of the Sofie MOS device (ie our ID). Example: sofie.mos", + "default": "" + }, + "debugLogging": { + "type": "boolean", + "ui:title": "Activate Debug Logging", + "default": false + } + }, + "required": ["mosId"], + "additionalProperties": false +} +``` + +### Defining a table as an object + +In the generated typescript interface, this will produce a property `"TestTable": { [id: string]: TestConfig }`. + +The key part here, is that it is an object with no `properties` defined, and a single `patternProperties` value performing a catchall. + +An `object` table is better than an `array` in blueprint-configuration, as it allows the UI to override individual values, instead of the table as a whole. + +```json +"TestTable": { + "type": "object", + "ui:category": "Test", + "ui:title": "Test table", + "ui:description": "", + "patternProperties": { + "": { + "type": "object", + "title": "TestConfig", + "properties": { + "number": { + "type": "integer", + "ui:title": "Number", + "ui:description": "Camera number", + "ui:summaryTitle": "Number", + "default": 1, + "min": 0 + }, + "port": { + "type": "integer", + "ui:title": "Port", + "ui:description": "ATEM Port", + "default": 1, + "min": 0 + } + }, + "required": ["number", "port"], + "additionalProperties": false + } + }, + "additionalProperties": false +}, + +``` + +### Select multiple ATEM device mappings + +```json +"mappingId": { + "type": "array", + "ui:title": "Mapping", + "ui:description": "", + "ui:summaryTitle": "Mapping", + "items": { + "type": "string", + "ui:sofie-enum": "mappings", + "ui:sofie-enum:filter": ["ATEM"], + }, + "uniqueItems": true +}, +``` diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/libraries.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/libraries.md new file mode 100644 index 00000000000..98711be84af --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/libraries.md @@ -0,0 +1,55 @@ +--- +description: List of all repositories related to Sofie +sidebar_position: 5 +--- + +# Applications & Libraries + +## Main Application + +[**Sofie Core**](https://github.com/Sofie-Automation/sofie-core) is the main application that serves the web GUI and handles the core logic. + +## Gateways and Services + +Together with the _Sofie Core_ there are several _gateways_ which are separate applications, but which connect to _Sofie Core_ and are managed from within the Core's web UI. + +- [**Playout Gateway**](https://github.com/Sofie-Automation/sofie-core/tree/master/packages/playout-gateway) Handles the playout from _Sofie_. Connects to and controls a multitude of devices, such as vision mixers, graphics, light controllers, audio mixers etc.. +- [**MOS Gateway**](https://github.com/Sofie-Automation/sofie-core/tree/master/packages/mos-gateway) Connects _Sofie_ to a newsroom system \(NRCS\) and ingests rundowns via the [MOS protocol](http://mosprotocol.com/). +- [**Live Status Gateway**](https://github.com/Sofie-Automation/sofie-core/tree/master/packages/live-status-gateway) Allows external systems to subscribe to state changes in Sofie. +- [**iNEWS Gateway**](https://github.com/tv2/inews-ftp-gateway) Connects _Sofie_ to an Avid iNEWS newsroom system. +- [**Spreadsheet Gateway**](https://github.com/SuperFlyTV/spreadsheet-gateway) Connects _Sofie_ to a _Google Drive_ folder and ingests rundowns from _Google Sheets_. +- [**Input Gateway**](https://github.com/Sofie-Automation/sofie-input-gateway) Connects _Sofie_ to various input devices, allowing triggering _User-Actions_ using these devices. +- [**Package Manager**](https://github.com/Sofie-Automation/sofie-package-manager) Handles media asset transfer and media file management for pulling new files, deleting expired files on playout devices and generating additional metadata (previews, thumbnails, automated QA checks) in a more performant, and possibly distributed, way. Can smartly figure out how to get a file on storage A to playout server B. + +## Libraries + +There are a number of libraries used in the Sofie ecosystem: + +- [**ATEM Connection**](https://github.com/Sofie-Automation/sofie-atem-connection) Library for communicating with Blackmagic Design's ATEM mixers +- [**ATEM State**](https://github.com/Sofie-Automation/sofie-atem-state) Used in TSR to tracks the state of ATEMs and generate commands to control them. +- [**CasparCG Server Connection**](https://github.com/SuperFlyTV/casparcg-connection) developed by **[_SuperFly.tv_](https://github.com/SuperFlyTV)** Library to connect and interact with CasparCG Servers. +- [**CasparCG State**](https://github.com/superflytv/casparcg-state) developed by **[_SuperFly.tv_](https://github.com/SuperFlyTV)** Used in TSR to tracks the state of CasparCG Servers and generate commands to control them. +- [**Ember+ Connection**](https://github.com/Sofie-Automation/sofie-emberplus-connection) Library to communicate with _Ember+_ control protocol +- [**HyperDeck Connection**](https://github.com/Sofie-Automation/sofie-hyperdeck-connection) Library for connecting to Blackmagic Design's HyperDeck recorders. +- [**MOS Connection**](https://github.com/Sofie-Automation/sofie-mos-connection/) A [_MOS protocol_](http://mosprotocol.com/) library for acting as a MOS device and connecting to an newsroom control system. +- [**Quantel Gateway Client**](https://github.com/Sofie-Automation/sofie-quantel-gateway-client) An interface that talks to the Quantel-Gateway application. +- [**Sofie Core Integration**](https://github.com/Sofie-Automation/sofie-core-integration) Used to connect to the [Sofie Core](https://github.com/Sofie-Automation/sofie-core) by the Gateways. +- [**Sofie Blueprints Integration**](https://www.npmjs.com/package/@sofie-automation/blueprints-integration) Common types and interfaces used by both Sofie Core and the user-defined blueprints. +- [**SuperFly-Timeline**](https://github.com/SuperFlyTV/supertimeline) developed by **[_SuperFly.tv_](https://github.com/SuperFlyTV)** Resolver and rules for placing objects on a virtual timeline. +- [**ThreadedClass**](https://github.com/nytamin/threadedClass) developed by **[_Nytamin_](https://github.com/nytamin)** Used in TSR to spawn device controllers in separate processes. +- [**Timeline State Resolver**](https://github.com/Sofie-Automation/sofie-timeline-state-resolver) \(TSR\) The main driver in **Playout Gateway,** handles connections to playout-devices and sends commands based on a **Timeline** received from **Core**. + +There are also a few typings-only libraries that define interfaces between applications: + +- [**Blueprints Integration**](https://www.npmjs.com/package/@sofie-automation/blueprints-integration) Defines the interface between [**Blueprints**](../user-guide/concepts-and-architecture.md#blueprints) and **Sofie Core**. +- [**Timeline State Resolver types**](https://www.npmjs.com/package/timeline-state-resolver-types) Defines the interface between [**Blueprints**](../user-guide/concepts-and-architecture.md#blueprints) and the timeline that will be fed into **TSR** for playout. + +## Other Sofie-related Repositories + +- [**CasparCG Server**](https://github.com/CasparCG/server) CasparCG Server. +- [**CasparCG Launcher**](https://github.com/Sofie-Automation/sofie-casparcg-launcher) Launcher, controller, and logger for CasparCG Server. +- [**CasparCG Media Scanner**](https://github.com/CasparCG/media-scanner) CasparCG Media Scanner. +- [**Sofie Chef**](https://github.com/Sofie-Automation/sofie-chef) A simple Chromium based renderer, used for kiosk mode rendering of web pages. +- [**Quantel Browser Plugin**](https://github.com/Sofie-Automation/sofie-quantel-browser-plugin) MOS-compatible Quantel video clip browser for use with Sofie. +- [**Sisyfos Audio Controller**](https://github.com/Sofie-Automation/sofie-sisyfos-audio-controller) _developed by [*olzzon*](https://github.com/olzzon/)_ +- [**Quantel Gateway**](https://github.com/Sofie-Automation/sofie-quantel-gateway) CORBA to REST gateway for _Quantel/ISA_ playback. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/mos-plugins.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/mos-plugins.md new file mode 100644 index 00000000000..1c414442719 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/mos-plugins.md @@ -0,0 +1,185 @@ +--- +title: MOS-plugins +sidebar_position: 20 +--- + +# iFrames MOS-plugins + +**The usage of MOS-plugins allow micro frontends to be injected into Sofie for the purpose of adding content to the production without turning away from the Sofie UI.** + +Example use cases can be browsing and playing clips straight from a video server, or the creation of lower third graphics without storing it in the NRCS. + +:::note MOS reference +[5.3 MOS Plug-in Communication messages](https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOSProtocolVersion40/index.html#calibre_link-61) + +The link points at MOS documentations for MOS 4 (for the benefit of having the best documentation), but will be compatible with most older versions too. +::: + +## Bucket items workflow + +MOS-plugins are managed through the Shelf-system. They are added as `external_frame` either as a Tab to a Rundown layout or as a Panel to a Dashboard layout. + +![Video browser MOS Plugin in Shelf tab](/img/docs/for-developers/shelf-bucket-items.jpg) +A video server browser plugin shown as a tab in the rundown layout shelf. + +The user can create one or more Buckets. From the plugin they can drag-and-drop content into the buckets. The user can manage the buckets and their content by creating, renaming, re-arranging and deleting. More details available at the [Bucket concept description.](/docs/user-guide/concepts-and-architecture#buckets) + +## Cross-origin drag-and-drop + +:::note Bucket workflow without drag-and-drop +The plugin iFrame can send a `postMessage` call with an `ncsItem` payload to programmatically create an ncsItem without the drag-and-drop interaction. This is a viable solution which avoids cross-origin drag-and-drop problems. +::: + +### The problem + +**Web browsers prevent drops into a webpage if the drag started from a page hosted on another origin.** + +This means that drag-and-drop must happen between pages from the same origin. This is relevant for MOS-plugins, as they are supposed to be displayed in iFrames. Specifically, this means that the plugin in the iFrame must be served from the same origin as the parent page (where the drop will happen). + +There are no properties or options to bypass this from within HTML/Javascript. Bypassing is theoretically possible by overriding the browser's security settings, but this is not recommended. + +:::note Background +The background for the policy is discussed in this Chromium Issue from 2010: [Security: do not allow on-page drag-and-drop from non-same-origin frames (or require an extra gesture)](https://issues.chromium.org/issues/40083787) +::: + +:::note What counts as different origins? +| Sofie Server Domain | Plugin Domain | Cross-origin or Same-origin? | +| ------------------- | ------------- | ---------------------------- | +| `https://mySofie.com:443` | `https://myPlugin.com:443` | cross-origin: different domains | +| | `https://www.mySofie.com:443` | cross-origin: different subdomains | +| | `https://myPlugin.mySofie.com:443` | cross-origin: different subdomains | +| | `http://mySofie.com:443` | cross-origin: different schemes | +| | `https://mySofie.com:80` | cross-origin: different ports | +| | `https://mySofie.com:443/myPlugin` | same-origin: domain, scheme and port match | +| | `https://mySofie.com/myPlugin` | same-origin: domain, scheme and port match (https implies port 443) | + +::: + +#### The "proxy idea" + +As you can tell from the table, you need to exactly match both the protocol, domain and port number. More importantly, different subdomains trigger the cross-origin policy. + +_The proxy idea_ is to use rewrite-rules in a proxy server (e.g. NGINX) to serve the plugin from a path on the Sofie server's domain. As this can't be done as subdomains, that leaves the option of having a folder underneath the top level of the Sofie server's domain. + +An example of this would be to serve Sofie at `https://mysofie.com` and then host the plugin (directly or via a proxy) at `https://mysofie.com/myplugin`. Technically this will work, but this solution is fragile. All links within the plugin will have to be either absolute or truly relative links that take the URL structure into account. This is doable if the plugin is being developed with this in mind. But it leads to a fragile tight coupling between the plugin and the host application (Sofie) which can break with any inconsiderate update in the future. + +:::note Example of linking from a (potentially proxied) subfolder +**Case:** `https://mysofie.com/myplugin/index.html` wants to access `https://mysofie.com/myplugin/static/images/logo.png`. + +Normally the plugin would be developed and bundled to work standalone, resulting in a link relative to its own base path, giving `/static/images/logo.png` which here wrongly resolves to `https://mysofie.com/static/images/logo.png`. + +The plugin would need to use either use the absolute `https://mysofie.com/myplugin/static/images/logo.png` or the relative `images/static/logo.png` or `./images/static/logo.png` or even `/myplugin/static/images/logo.png` to point to the right resource. +::: + +### The solution + +**Sofie proposes a drag-and-drop/postMessage hybrid interface.** +In this model the user interactions of drag-and-drop are targeting a dedicated Drop page served by the plugin-server (same-origin to the plugin). This can be transparently overlaid the real drop region and intercept drop events. The Bucket system has built-in support for this, configured as an additional property to the External frame panel setup in Shelf config. + +![Configuration of External frame with dedicated drop-page](/img/docs/for-developers/shelf-external_frame-config.png) + +The true communication channel between the plugin and Sofie becomes a postMessage protocol where the plugin is managing all drag-and-drop events and converts them into the postMessage protocol. Sofie also handles edge cases such as timeouts, drag leaving the browser etc. + +### Sequence diagram + +#### Post-messages from the Plugin (drag-side) + +| Message | Payload | Description | +| --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| dragStart | - | Re-sends the DOM event dragStart as a postMessage of the same kind.
This is the signal to Sofie to toggle on the Drop-zone and indicate in the UI that a drag is happening. | +| dragEnd | - | Re-sends the DOM event dragEnd as a postMessage of the same kind.
This is the signal to Sofie to toggle off the Drop-zone and reset the UI. | + +#### Post-messages from the Plugin Drop-page + +| Message | Payload | Description | +| --------- | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| dragEnter | `{event: 'dragEnter', label: string}` | To set the UI to reflect an object is being dragged into a specific bucket.
The label property can be used for showing a simple placeholder in the bucket. | +| dragLeave | `{event: 'dragLeave'}` | To reset any UI. | +| drop | `{event: 'drop'}` | To synchronously react to the drop in the UI. | +| data | `{event: 'data', data: ncsItem}` | To (a)synchronously receive the payload.
The expected format is an `ncsItem` MOS message (XML string) | +| error | `{event: 'error', message}` | To cancel the drag-operation and handle any errors. | + +:::note Please note +Please note how all interactions are happening over the postMessage interface. +No DOM-driven drag-n-drop events are relevant for Sofie, as they are solely handled between the plugin and its drop-page. +::: + +```mermaid +sequenceDiagram +autonumber + +actor user as User + +participant plugin as Plugin
Frontend +participant shelf as Sofie Shelf Component +participant bucket as Sofie Bucket Component +participant drop as Plugin
Drop-page + +user->>plugin: Starts dragging from Plugin +plugin->>shelf: postMessage dragStartEvent +shelf--)shelf: 10 000ms timeout to trigger a dragEndEvent
if the drag doesn't cancel or successfully drop before that. +shelf->>shelf: Filter for valid Drop Zones
based on the optional properties of the dragStartEvent +shelf->>bucket: Sofie React event dragStartEvent +bucket->>drop: Shows iFrame Drop Zone + + + +user->>drop: Drags into the area of a Drop Zone (DOM dragEnter event) +note right of drop: Read payload to provide a title
in the dragEnterEvent +drop->>drop: e.dataTransfer.getData('text/plain'); +drop->>bucket: postmessage object dragEnterEvent + +loop dragOver events + user-)drop: Drag moves over drop target (DOM dragover event) + drop->>drop: (re)set timeout 100ms
to trigger faux dragLeave +end + +drop--)drop: dragLeave timeout expires +drop->>bucket: postmessage object dragEnterEvent (faux) + + +user->>drop: Drags out of a Drop Zone, or dragOver timeout (DOM dragLeave event) +drop->>drop: cancel dragOver timeout +drop->>bucket: postmessage object dragLeaveEvent + + + +Note over user,drop: Unknown order of events. Handle both outcomes of the race. +par Successful drop or Cancelled drag + user->>plugin: Successful drop
or Cancel drag on ESC
or drop outside of Drop region
(DOM dragEnd event) + plugin->>shelf: postMessage dragEndEvent + shelf->>shelf: Clear the drop-/cancel-timeout. + shelf->>bucket: Sofie React event dragEndEvent + bucket->>drop: Hides iFrame Drop Zone +and Drops in bucket + user->>drop: Drop (DOM drop event) + drop->>bucket: dropEvent + bucket--)bucket: Set timeout to trigger an user-facing error
if the data doesn't return in time. + bucket->>bucket: Set loader UI + + drop->>drop: e.dataTransfer.getData('text/plain'); + + + alt Success + drop--)bucket: postmessage object dataEvent + bucket->>bucket: Clear loader UI/Set success UI + else Error + drop--)bucket: postmessage object errorEvent + bucket->>bucket: Clear loader UI + bucket--)user: Error message + else Timeout + bucket->>bucket: Clear loader UI + bucket--)user: Error message + end +end + +``` + +#### Minimal example sequence - happy path + +Don't worry, the sequence diagram shows a lot more detail than you need to think about. Consider this simple happy-path sequence as a representative interaction between the 3 actors (Plugin, Drop-page and Sofie): + +1. Plugin `dragStart` +2. Drop-page `dragEnter` +3. Plugin `dragEnd` and Drop-page `drop` +4. Drop-page `data` diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/npm-package-publishing.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/npm-package-publishing.md new file mode 100644 index 00000000000..079ca9c8fa9 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/npm-package-publishing.md @@ -0,0 +1,23 @@ +--- +title: NPM Package Publishing +sidebar_position: 999 +--- + +While many parts of Sofie reside in the main `sofie-core` mono-repo, there are a few NPM libraries in that repo which want to be published to NPM to allow being consumed elsewhere. + +Many features and PRs will need to make changes to these libraries, which means that you will often need to publish testing versions that you can use before your PR is merged, or when you need to publish your own Sofie releases to backport that feature onto an older release. + +To make this easy, the Github actions workflows have been structured so that you can utilise them with minimal effort for publishing to your own npm organization. +The `Publish libraries` workflow is the single workflow used to perform this publishing, for both stable and prerelease versions. You can manually trigger this workflow at any time in the Github UI or via CLI tools to trigger a prerelease build of the libraries. + +When running in your fork, this workflow will only run if the `NPM_PACKAGE_PREFIX` variable has been defined (Note: this is a variable not a secret). + +Recommended repository variables/secrets + +- `NPM_PACKAGE_PREFIX` — repository variable; your npm organisation (required for forks to publish). +- `NPM_PACKAGE_SCOPE` — repository variable; optional, adds `sofie-` prefix to package names. +- `NPM_TOKEN` — repository secret; optional if using trusted publishing, otherwise required for the workflow to publish. + +For the publishing, we recommend enabling [trusted publishing](https://docs.npmjs.com/trusted-publishers), but in case you are unable to do this (or to allow for the first publish), if you provide a `NPM_TOKEN` secret, that will be used for the publishing instead. + +The [`timeline-state-resolver`](https://github.com/Sofie-Automation/sofie-timeline-state-resolver) repository has been setup in the same way, as this is another library that you will often need to publish your own versions for. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/publications.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/publications.md new file mode 100644 index 00000000000..c9def838a26 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/publications.md @@ -0,0 +1,43 @@ +--- +title: Publications +sidebar_position: 12 +--- + +To ensure that the UI of Sofie is reactive, we are leveraging publications over the DDP connection that Meteor provides. +In its most basic form, this allows for streaming MongoDB document updates as they happen to the UI, and there is also a structure in place for 'Custom Publications' which appear like a MongoDB collection to the client, but are generated in-memory collections of data allowing us to do some processing of data before publishing it to the client. + +It is possible to subscribe to these publications outside of Meteor, but we have not found any maintained ddp clients, except for the one we are using in `server-core-integration`. The protocol is simple and stable and has documentation on the [Meteor GitHub](https://github.com/meteor/meteor/blob/devel/packages/ddp/DDP.md), and should be easy to implement in another language if desired. + +All of the publication implementations reside in [`meteor/server/publications` folder](https://github.com/Sofie-Automation/sofie-core/tree/main/meteor/server/publications), and are typically pretty well isolated from the rest of the code we have in Meteor. + +We prefer using publications in Sofie over polling because: + +- there are not enough DDP clients to a single Sofie installation for the number of connected clients to be problematic +- polling can be costly for many of these publications without some form of caching or tracking changes (which starts to get to a similar level of complexity) +- we can be more confident that all the clients have the same data as the database is our point of truth +- the system can be more reactive as changes are pushed to interested parties with minimal intervention + +## MongoDB Publications + +A majority of data is sent to the client utilising Meteor's ability to publish a MongoDB cursor. This allows us to run a MongoDB query on the backend, and let it handle the publishing of individual changes. + +In some (typically older) publications, we let the client specify the MongoDB query to use for the subscription, where we perform some basic validation and authentication before executing the query. + +In typically newer publications, we are formalising the publications a bit better by requiring some simpler parameters to the publication, with the query then generated on the backend. This will help us ensure that the queries are made with suitable indices, and to ensure that subscriptions are deduplicated where possible. + +## Custom Publications + +There has been a recent push towards using more 'custom' publications for streaming data to the UI. While we are unsure if this will be beneficial for every publication, it is really beneficial for others as it allows us to do some pre-computation of data before sending it to the client. + +To achieve this, we have an `optimisedObserver` flow which is designed to help maange to a custom publication, with a few methods to fill in to setup the reactivity and the data transformation. + +One such publication is the `PieceContentStatus`, prior to version 1.50, this was computed inside the UI. +A brief overview of this publication, is that it looks at each Piece in a Rundown, and reports whether the Piece is 'OK'. This check is primarily focussed on Pieces containing clips, where it will check the metadata generated by Package Manager to ensure that the clip is marked as being ready for playout, and that it has the correct format and some other quality checks. + +To do this on the client meant needing to subscribe to the whole contents of a couple of MongoDB collections, as it is not easy to determine which documents will be needed until the check is being run. This caused some issues as these collections could get rather large. We also did not always have every Piece loaded in the UI, so had to defer some of the computation to the backend via polling. + +This makes it more suitable for a custom publication, where we can more easily and cheaply do this computation without being concerned about causing UI lockups and with less concern about memory pressure. Performing very granular MongoDB queries is also cheaper. The result is that we build a graph of what other documents are used for the status of each Piece, so we can cheaply react to changes to any of those documents, while also watching for changes to the pieces. + +## Live Status Gateway + +The Live Status Gateway was introduced to Sofie in version 1.50. This gateway serves as a way for an external system to subscribe to publications which are designed to be simpler than the ones we publish over DDP. These publications are intended to be used by external systems which need a 'stable' API and to not have too much knowledge about the inner workings of Sofie. See [Api Stability](./api-stability.md) for more details. diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/url-query-parameters.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/url-query-parameters.md new file mode 100644 index 00000000000..3cc86e15a65 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/url-query-parameters.md @@ -0,0 +1,25 @@ +--- +sidebar_label: URL Query Parameters +sidebar_position: 10 +--- + +# URL Query Parameters + +Appending query parameter(s) to the URL will allow you to modify the behaviour of the GUI, as well as control the [Access Levels](../user-guide/features/access-levels.md). + +| Query Parameter | Description | +| :--------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `admin=1` | Gives the GUI the same access as the combination of [Configuration Mode](../user-guide/features/access-levels.md#Permissions) and [Studio Mode](../user-guide/features/access-levels.md#Permissions) as well as having access to a set of [Testing Mode](../user-guide/features/access-levels.md#Permissions) tools and a Manual Control section on the Rundown page. _Default value is `0`._ | +| `studio=1` | [Studio Mode](../user-guide/features/access-levels.md#Permissions) gives the GUI full control of the studio and all information associated to it. This includes allowing actions like activating and deactivating rundowns, taking parts, adlibbing, etcetera. _Default value is `0`._ | +| `buckets=0,1,...` | The buckets can be specified as base-0 indices of the buckets as seen by the user. This means that `?buckets=1` will display the second bucket as seen by the user when not filtering the buckets. This allows the user to decide which bucket is displayed on a secondary attached screen simply by reordering the buckets on their main view. | +| `develop=1` | Enables the browser's default right-click menu to appear. It will also reveal the _Manual Control_ section on the Rundown page. _Default value is `0`._ | +| `display=layout,buckets,inspector` | A comma-separated list of features to be displayed in the shelf. Available values are: `layout` \(for displaying the Rundown Layout\), `buckets` \(for displaying the Buckets\) and `inspector` \(for displaying the Inspector\). | +| `help=1` | Enables some tooltips that might be useful to new users. _Default value is `0`._ | +| `ignore_piece_content_status=1` | Removes the "zebra" marking on VT pieces that have a "missing" status. _Default value is `0`._ | +| `reportNotificationsId=anyId,...` | Sets an ID for an individual client GUI system, to be used for reporting Notifications shown to the user. The Notifications' contents, tagged with this ID, will be sent back to the Sofie Core's log. _Default value is `0`, which disables the feature._ | +| `shelffollowsonair=1` | _Default value is `0`._ | +| `show_hidden_source_layers=1` | _Default value is `0`._ | +| `speak=1` | Experimental feature that starts playing an audible countdown 10 seconds before each planned _Take_. _Default value is `0`._ | +| `vibrate=1` | Experimental feature that triggers the vibration API in the web browser 3 seconds before each planned _Take_. _Default value is `0`._ | +| `zoom=1,...` | Sets the scaling of the entire GUI. _The unit is a percentage where `100` is the default scaling._ | +| `hideRundownHeader=1` | Hides header on [Rundown view](../user-guide/features/sofie-views-and-screens#rundown-view) and [Active Rundown screen](../user-guide/features/sofie-views-and-screens#active-rundown-screen). _Default value is `0`._ | diff --git a/packages/documentation/versioned_docs/version-26.03.0/for-developers/worker-threads-and-locks.md b/packages/documentation/versioned_docs/version-26.03.0/for-developers/worker-threads-and-locks.md new file mode 100644 index 00000000000..8018a060822 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/for-developers/worker-threads-and-locks.md @@ -0,0 +1,61 @@ +--- +title: Worker Threads & Locks +sidebar_position: 9 +--- + +Starting with v1.40.0 (Release 40), the core logic of Sofie is split across +multiple threads. This has been done to minimise performance bottlenecks such as ingest changes delaying takes. In its +current state, it should not impact deployment of Sofie. + +In the initial implementation, these threads are run through [threadedClass](https://github.com/nytamin/threadedclass) +inside of Meteor. As Meteor does not support the use of `worker_threads`, and to allow for future separation, the +`worker_threads` are treated and implemented as if they are outside of the Meteor ecosystem. The code is isolated from +Meteor inside of `packages/job-worker`, with some shared code placed in `packages/corelib`. + +Prior to v1.40.0, there was already a work-queue of sorts in Meteor. As such the functions were defined pretty well to +translate across to being on a true work queue. For now this work queue is still in-memory in the Meteor process, but we +intend to investigate relocating this in a future release. This will be necessary as part of a larger task of allowing +us to scale Meteor for better resiliency. Many parts of the worker system have been designed with this in mind, and so +have sufficient abstraction in place already. + +### The Worker + +The worker process is designed to run the work for one or more studios. The initial implementation will run for all +studios in the database, and is monitoring for studios to be added or removed. + +For each studio, the worker runs 3 threads: + +1. The Studio/Playout thread. This is where all the playout operations are executed, as well as other operations that + require 'ownership' of the Studio +2. The Ingest thread. This is where all the MOS/Ingest updates are handled and fed through the bluerpints. +3. The events thread. Some low-priority tasks are pushed to here. Such as notifying ENPS about _the yellow line_, or the + Blueprints methods used to generate External-Messages for As-Run Log. + +In future it is expected that there will be multiple ingest threads. How the work will be split across them is yet to be +determined + +### Locks + +At times, the playout and ingest threads both need to take ownership of `RundownPlaylists` and `Rundowns`. + +To facilitate this, there are a couple of lock types in Sofie. These are coordinated by the parent thread in the worker +process. + +#### PlaylistLock + +This lock gives ownership of a specific `RundownPlaylist`. It is required to be able to load a `PlayoutModel`, and +must be held during other times where the `RundownPlaylist` is modified or is expected to not change. + +This lock must be held while writing any changes to either a `RundownPlaylist` or any `Rundown` that belong to the +`RundownPlaylist`. This ensures that any writes to MongoDB are atomic, and that Sofie doesn't start performing a +playout operation halfway through an ingest operation saving. + +#### RundownLock + +This lock gives ownership of a specific `Rundown`. It is required to be able to load a `IngestModel`, and must held +during other times where the `Rundown` is modified or is expected to not change. + +:::caution +It is not allowed to acquire a `RundownLock` while inside of a `PlaylistLock`. This is to avoid deadlocks, as it is very +common to acquire a `PlaylistLock` inside of a `RundownLock` +::: diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/concepts-and-architecture.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/concepts-and-architecture.md new file mode 100644 index 00000000000..76adc563187 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/concepts-and-architecture.md @@ -0,0 +1,191 @@ +--- +sidebar_position: 1 +--- + +# Concepts & Architecture + +## System Architecture + +![Example of a Sofie setup with a Playout Gateway and a Spreadsheet Gateway](/img/docs/main/features/playout-and-spreadsheet-example.png) + +### Sofie Core + +**Sofie Core** is a web server which handle business logic and serves the web GUI. +It is a [NodeJS](https://nodejs.org/) process backed up by a [MongoDB](https://www.mongodb.com/) database and based on the framework [Meteor](http://meteor.com/). + +### Gateways + +Gateways are applications that connect to Sofie Core and exchange data; such as rundown data from an NRCS (Newsroom Computer System) or the [Timeline](#timeline) for playout. + +An example of a gateway is the [Spreadsheet Gateway](https://github.com/SuperFlyTV/spreadsheet-gateway). +All gateways use the [Core Integration Library](https://github.com/Sofie-Automation/sofie-core/tree/master/packages/server-core-integration) to communicate with Core. + +## System, Studio & Show Style + +To be able to facilitate various different kinds of show, Sofie Core has the concepts of "System", "Studio" and "Show Style". + +- The **System** defines the whole of the Sofie Core +- The **Studio** contains things that are related to the "hardware" or "rig". Technically, a Studio is defined as an entity that can have one \(or none\) rundown active at any given time. In most cases, this will be a representation of your gallery, with cameras, video playback and graphics systems, external inputs, sound mixers, lighting controls and so on. A single System can easily control multiple Studios. +- The **Show Style** contains settings for the "show", for example if there's a "Morning Show" and an "Afternoon Show" - produced in the same gallery - they might be two different Show Styles \(played in the same Studio\). Most importantly, the Show Style decides the "look and feel" of the Show towards the producer/director, dictating how data ingested from the NRCS will be interpreted and how the user will interact with the system during playback (see: [Show Style](configuration/settings-view#show-style) in Settings). + - A **Show Style Variant** is a set of Show Style _Blueprint_ configuration values, that allows to use the same interaction model across multiple Shows with potentially different assets, changing the outward look of the Show: for example news programs with different hosts produced from the same Studio, but with different light setups, backscreen and overlay graphics. + +![Sofie Architecture Venn Diagram](/img/docs/main/features/sofie-venn-diagram.png) + +## Playlists, Rundowns, Segments, Parts, Pieces + +![Playlists, Rundowns, Segments, Parts, Pieces](/img/docs/main/features/playlist-rundown-segment-part-piece.png) + +### Playlist + +A Playlist \(or "Rundown Playlist"\) is the entity that "goes on air" and controls the playhead/Take Point. + +It contains one or more Rundowns, which are played out in order. + +:::info +In some many studios, there is only ever one rundown in a playlist. In those cases, we sometimes lazily refer to playlists and rundowns as "being the same thing". +::: + +A Playlist is played out in the context of it's [Studio](#studio), thereby only a single Playlist can be active at a time within each Studio. + +A playlist is normally played through and then ends but it is also possible to make looping playlists in which case the playlist will start over from the top after the last part has been played. + +### Rundown + +The Rundown contains the content for a show. It contains Segments and Parts, which can be selected by the user to be played out. +A Rundown always has a [showstyle](#showstyle) and is played out in the context of the [Studio](#studio) of its Playlist. + +### Segment + +The Segment is the horizontal line in the GUI. It is intended to be used as a "chapter" or "subject" in a rundown, where each individual playable element in the Segment is called a [Part](#part). + +### Part + +The Part is the playable element inside of a [Segment](#segment). This is the thing that starts playing when the user does a [TAKE](#take-point). A Playing part is _On Air_ or _current_, while the part "cued" to be played is _Next_. +The Part in itself doesn't determine what's going to happen, that's handled by the [Pieces](#piece) in it. + +### Piece + +The Pieces inside of a Part determines what's going to happen, the could be indicating things like VT's, cut to cameras, graphics, or what script the host is going to read. + +Inside of the pieces are the [timeline-objects](#what-is-the-timeline) which controls the playout on a technical level. + +:::tip +Tip! If you want to manually play a certain piece \(for example a graphics overlay\), you can at any time double-click it in the GUI, and it will be copied and played at your play head, just like an [AdLib](#adlib-pieces) would! +::: + +See also: [Showstyle](#system-studio--show-style) + +### AdLib Piece + +The AdLib pieces are Pieces that aren't programmed to fire at a specific time, but instead intended to be manually triggered by the user. + +The AdLib pieces can either come from the currently playing Part, or it could be _global AdLibs_ that are available throughout the show. + +An AdLib isn't added to the Part in the GUI until it starts playing, instead you find it in the [Shelf](features/sofie-views-and-screens.mdx#shelf). + +## Buckets + +A Bucket is a container for AdLib Pieces created by the producer/operator during production. They exist independently of the Rundowns and associated content created by ingesting data from the NRCS. Users can freely create, modify and remove Buckets. + +The primary use-case of these elements is for breaking-news formats where quick turnaround video editing may require circumvention of the regular flow of show assets and programming via the NRCS. Currently, one way of creating AdLibs inside Buckets is using a MOS Plugin integration inside the Shelf, where MOS [ncsItem](https://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOSProtocolVersion40/index.html#calibre_link-72) elements can be dragged from the MOS Plugin onto a bucket and ingested. + +The ingest happens via the `getAdlibItem` method: [https://github.com/Sofie-Automation/sofie-core/blob/6c4edee7f352bb542c8a29317d59c0bf9ac340ba/packages/blueprints-integration/src/api/showStyle.ts#L122](https://github.com/Sofie-Automation/sofie-core/blob/6c4edee7f352bb542c8a29317d59c0bf9ac340ba/packages/blueprints-integration/src/api/showStyle.ts#L122) + +## Views and Screens + +Being a web-based system, Sofie has a number of customisable, user-facing web [views and screens](features/sofie-views-and-screens.mdx) used for control and monitoring. + +## Blueprints + +Blueprints are plug-ins that run in Sofie Core. They interpret the data coming in from the rundowns and transform them into a rich set of playable elements \(Segments, Parts, AdLibs etc\). + +The blueprints are webpacked javascript bundles which are uploaded into Sofie via the GUI. They are custom-made and vary depending on the show style, type of input data \(NRCS\) and the types of controlled devices. A generic [blueprint that works with spreadsheets is available here](https://github.com/SuperFlyTV/sofie-demo-blueprints). + +When [Sofie Core](#sofie-core) calls upon a Blueprint, it returns a JavaScript object containing methods callable by Sofie Core. These methods will be called by Sofie Core in different situations, depending on the method. +Documentation on these interfaces are available in the [Blueprints integration](https://www.npmjs.com/package/@sofie-automation/blueprints-integration) library. + +There are 3 types of blueprints, and all 3 must be uploaded into Sofie before the system will work correctly. + +### System Blueprints + +Handle things on the _System level_. +Documentation on the interface to be exposed by the Blueprint: +[https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api/system.ts](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api/system.ts) + +### Studio Blueprints + +Handle things on the _Studio level_, like "which showstyle to use for this rundown". +Documentation on the interface to be exposed by the Blueprint: +[https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api/studio.ts](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api/studio.ts) + +### Showstyle Blueprints + +Handle things on the _Showstyle level_, like generating [_Baseline_](#baseline), _Segments_, _Parts, Pieces_ and _Timelines_ in a rundown. +Documentation on the interface to be exposed by the Blueprint: +[https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api/showStyle.ts](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api/showStyle.ts) + +## `PartInstances` and `PieceInstances` + +In order to be able to facilitate ingesting changes from the NRCS while continuing to provide a stable and predictable playback of the Rundowns, Sofie internally uses a concept of ["instantiation"]() of key Rundown elements. Before playback of a Part can begin, the Part and it's Pieces are copied into an Instance of a Part: a `PartInstance`. This protects the contents of the _Next_ and _On Air_ part, preventing accidental changes that could surprise the producer/director. This also makes it possible to inspect the "as played" state of the Rundown, independently of the "as planned" state ingested from the NRCS. + +The blueprints can optionally allow some changes to the Parts and Pieces to be forwarded onto these `PartInstances`: [https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api.ts#L190](https://github.com/Sofie-Automation/sofie-core/blob/main/packages/blueprints-integration/src/api.ts#L190) + +## Timeline + +### What is the timeline? + +The Timeline is a collection of timeline-objects, that together form a "target state", i.e. an intent on what is to be played and at what times. + +The timeline-objects can be programmed to contain relative references to each other, so programming things like _"play this thing right after this other thing"_ is as easy as `{start: { #otherThing.end }}` + +The [Playout Gateway](../for-developers/libraries.md) picks up the timeline from Sofie Core and \(using the [TSR timeline-state-resolver](https://github.com/Sofie-Automation/sofie-timeline-state-resolver)\) controls the playout devices to make sure that they actually play what is intended. + +![Example of 2 objects in a timeline: The #video object, destined to play at a certain time, and #gfx0, destined to start 15 seconds into the video.](/img/docs/main/features/timeline.png) + +### Why a timeline? + +The Sofie system is made to work with a modern web- and IT-based approach in mind. Therefore, the Sofie Core can be run either on-site, or in an off-site cloud. + +![Sofie Core can run in the cloud](/img/docs/main/features/sofie-web-architecture.png) + +One drawback of running in a cloud over the public internet is the - sometimes unpredictable - latency. The Timeline overcomes this by moving all the immediate control of the playout devices to the Playout Gateway, which is intended to run on a local network, close to the hardware it controls. +This also gives the system a simple way of load-balancing - since the number of web-clients or load on Sofie Core won't affect the playout. + +Another benefit of basing the playout on a timeline is that when programming the show \(the blueprints\), you only have to care about "what you want to be on screen", you don't have to care about cleaning up previously played things, or what was actually played out before. This is handled by the Playout Gateway automatically. This also allows the user to jump around in a rundown freely, without the risk of things going wrong on air. + +### How does it work? + +:::tip +Fun tip! The timeline in itself is a [separate library available on GitHub](https://github.com/SuperFlyTV/supertimeline). + +You can play around with the timeline in the browser using [JSFiddle and the timeline-visualizer](https://jsfiddle.net/nytamin/rztp517u/)! +::: + +The Timeline is stored by Sofie Core in a MongoDB collection. It is generated whenever a user does a [Take](#take-point), changes the [Next-point](#next-point-and-lookahead) or anything else that might affect the playout. + +_Sofie Core_ generates the timeline using: + +- The [Studio Baseline](#baseline) \(only if no rundown is currently active\) +- The [Showstyle Baseline](#baseline), of the currently active rundown. +- The [currently playing Part](#take-point) +- The [Next'ed Part](#next-point-and-lookahead) and Parts that come after it \(the [Lookahead](#lookahead)\) +- Any [AdLibs](#adlib-pieces) the user has manually selected to play + +The [**Playout Gateway**](../for-developers/libraries.md#gateways) then picks up the new timeline, and pipes it into the [\(TSR\) timeline-state-resolver](https://github.com/Sofie-Automation/sofie-timeline-state-resolver) library. + +The TSR then... + +- Resolves the timeline, using the [timeline-library](https://github.com/SuperFlyTV/supertimeline) +- Calculates new target-states for each relevant point in time +- Maps the target-state to each playout device +- Compares the target-states for each device with the currently-tracked-state and.. +- Generates commands to send to each device to account for the change +- Puts the commands on the queue and sends them to the devices at the correct time. + +:::info +For more information about what playout devices _TSR_ supports, and examples of the timeline-objects, see the [README of TSR](https://github.com/Sofie-Automation/sofie-timeline-state-resolver#timeline-state-resolver) +::: + +:::info +For more information about how to program timeline-objects, see the [README of the timeline-library](https://github.com/SuperFlyTV/supertimeline#superfly-timeline) +::: diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/_category_.json b/packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/_category_.json new file mode 100644 index 00000000000..c4e45c2347d --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Configuration", + "position": 4 +} diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/settings-view.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/settings-view.md new file mode 100644 index 00000000000..9fdde7b9a36 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/settings-view.md @@ -0,0 +1,202 @@ +--- +sidebar_position: 2 +--- + +# Settings View + +:::caution +The settings views are only visible to users with the correct [access level](../features/access-levels.md)! +::: + +Recommended read before diving into the settings: [System, Studio & Show Style](../concepts-and-architecture.md#system-studio-and-show-style). + +## System + +The _System_ settings are settings for this installation of Sofie. In here goes the settings that are applicable system-wide. + +:::caution +Documentation for this section is yet to be written. +::: + +### Name and logo + +Sofie contains the option to change the name of the installation. This is useful to identify different studios or regions. + +We have also provided some seasonal logos just for fun. + +### System-wide notification message + +This option will show a notification to the user containing some custom text. This can be used to inform the user about on-going problems or maintenance information. + +### Support panel + +The support panel is shown in the rundown view when the user clicks the "?" button in the right bottom corner. It can contain some custom HTML which can be used to refer your users to custom information specific to your organisation. + +### Action triggers + +The action triggers section lets you set custom keybindings for system-level actions such as doing a take or resetting a rundown. + +### Monitoring + +Sofie can be configured to send information to Elastic APM. This can provide useful information about the system's performance to developers. In general this can reduce the performance of Sofie altogether though so it is recommended to disable it in production. + +Sofie can also monitor for blocked threads, and will log a message if it discovers any. This is also recommended to disable in production. + +### CRON jobs + +Sofie contains cron jobs for restarting any casparcg servers through the casparcg launcher as well as a job to create rundown snapshots periodically. + +### Clean up + +The clean up process in Sofie will search the database for unused data and indexes and removes them. If you have had an installation running for many versions this may increase database informance and is in general safe to use at any time. + +## Studio + +A _Studio_ in Sofie-terms is a physical location, with a specific set of devices and equipment. Only one show can be on air in a studio at the same time. +The _studio_ settings are settings for that specific studio, and contains settings related to hardware and playout, such as: + +- **Attached devices** - the Gateways related to this studio +- **Blueprint configuration** - custom config option defined by the blueprints +- **Layer Mappings** - Maps the logical _timeline layers_ to physical devices and outputs + +The Studio uses a studio-blueprint, which handles things like mapping up an incoming rundown to a Showstyle. + +### Attached Devices + +This section allows you to add and remove Gateways that are related to this _Studio_. When a Gateway is attached to a Studio, it will react to the changes happening within it, as well as feed the necessary data into it. + +### Blueprint Configuration + +Sofie allows the Blueprints to expose custom configuration fields that allow the System Administrator to reconfigure how these Blueprints work through the Sofie UI. Here you can change the configuration of the [Studio Blueprint](../concepts-and-architecture.md#studio-blueprints). + +### Layer Mappings + +This section allows you to add, remove and configure how logical device-control will be translated to physical automation control. [Blueprints](../concepts-and-architecture.md#blueprints) control devices through objects placed on a [Timeline](../concepts-and-architecture.md#timeline) using logical device identifiers called _Layers_. A layer represents a single aspect of a device that can be controlled at a given time: a video switcher's M/E bus, an audio mixers's fader, an OSC control node, a video server's output channel. Layer Mappings translate these logical identifiers into physical device aspects, for example: + +![A sample configuration of a Layer Mapping for the M/E1 Bus of an ATEM switcher](/img/docs/main/features/atem-layer-mapping-example.png) + +This _Layer Mapping_ configures the `atem_me_program` Timeline-layer to control the `atem0` device of the `ATEM` type. No Lookahead will be enabled for this layer. This layer will control a `MixEffect` aspect with the Index of `0` \(so M/E 1 Bus\). + +These mappings allow the System Administrator to reconfigure what devices the Blueprints will control, without the need of changing the Blueprint code. + +#### Route Sets + +In order to allow the Producer to reconfigure the automation from the Switchboard in the [Rundown View](../concepts-and-architecture.md#rundown-view), as well as have some pre-set automation control available for the System Administrator, Sofie has a concept of Route Sets. Route Sets work on top of the Layer Mappings, by configuring sets of [Layer Mappings](settings-view.md#layer-mappings) that will re-route the control from one device to another, or to disable the automation altogether. These Route Sets are presented to the Producer in the [Switchboard](../concepts-and-architecture.md#switchboard) panel. + +A Route Set is essentially a distinct set of Layer Mappings, which can modify the settings already configured by the Layer Mappings, but can be turned On and Off. Called Routes, these can change: + +- the Layer ID to a new Layer ID +- change the Device being controlled by the Layer +- change the aspect of the Device that's being controlled. + +Route Sets can be grouped into Exclusivity Groups, in which only a single Route Set can be enabled at a time. When activating a Route Set within an Exclusivity Group, all other Route Sets in that group will be deactivated. This in turn, allows the System Administrator to create entire sections of exclusive automation control within the Studio that the Producer can then switch between. One such example could be switching between Primary and Backup playout servers, or switching between Primary and Backup talent microphone. + +![The Exclusivity Group Name will be displayed as a header in the Switchboard panel](/img/docs/main/features/route-sets-exclusivity-groups.png) + +A Route Set has a Behavior property which will dictate what happens how the Route Set operates: + +| Type | Behavior | +| :-------------- | :------------------------------------------------------------------------------------------------------------------------------ | +| `ACTIVATE_ONLY` | This RouteSet cannot be deactivated, only a different RouteSet in the same Exclusivity Group can cause it to deactivate | +| `TOGGLE` | The RouteSet can be activated and deactivated. As a result, it's possible for the Exclusivity Group to have no Route Set active | +| `HIDDEN` | The RouteSet can be activated and deactivated, but it will not be presented to the user in the Switchboard panel | + +![An active RouteSet with a single Layer Mapping being re-configured](/img/docs/main/features/route-set-remap.png) + +Route Sets can also be configured with a _Default State_. This can be used to contrast a normal, day-to-day configuration with an exceptional one \(like using a backup device\) in the [Switchboard](../concepts-and-architecture#switchboard) panel. + +| Default State | Behavior | +| :------------ | :------------------------------------------------------------ | +| Active | If the Route Set is not active, an indicator will be shown | +| Not Active | If the Route Set is active, an indicator will be shown | +| Not defined | No indicator will be shown, regardless of the Route Set state | + +## Show style + +A _Showstyle_ is related to the looks and logic of a _show_, which in contrast to the _studio_ is not directly related to the hardware. +The Showstyle contains settings like + +- **Source Layers** - Groups different types of content in the GUI +- **Output Channels** - Indicates different output targets \(such as the _Program_ or _back-screen in the studio_\) +- **Action Triggers** - Select how actions can be started on a per-show basis, outside of the on-screen controls +- **Blueprint configuration** - custom config option defined by the blueprints + +:::caution +Please note the difference between _Source Layers_ and _timeline-layers_: + +[Pieces](../concepts-and-architecture.md#piece) are put onto _Source layers_, to group different types of content \(such as a VT or Camera\), they are therefore intended only as something to indicate to the user what is going to be played, not what is actually going to happen on the technical level. + +[Timeline-objects](../concepts-and-architecture.md#timeline-object) \(inside of the [Pieces](../concepts-and-architecture.md#piece)\) are put onto timeline-layers, which are \(through the Mappings in the studio\) mapped to physical devices and outputs. +The exact timeline-layer is never exposed to the user, but instead used on the technical level to control playout. + +An example of the difference could be when playing a VT \(that's a Source Layer\), which could involve all of the timeline-layers _video_player0_, _audio_fader_video_, _audio_fader_host_ and _mixer_pgm._ +::: + +### AB Channel Display + +The AB Channel Display settings control how AB Resolver channel assignments (A, B, C, etc.) are shown on various screens. When using the AB Resolver for video playback, clips are automatically assigned to different video server channels. This configuration determines which Pieces display their assigned channel. + +![AB Channel Display Settings](/img/docs/main/features/ab-channel-display-settings.png) + +The configuration options are: + +| Setting | Description | +| :------ | :---------- | +| **Source Layer IDs** | Specific Source Layers that should always show AB channel info | +| **Source Layer Types** | Show AB channel info for all Source Layers of these types (e.g., VT, Live Speak) | +| **Output Layer IDs** | Only show for Pieces on specific Output Layers (e.g., only PGM) | +| **Show on Director Screen** | Enable the AB channel display on the [Presenter Screen](../features/sofie-views-and-screens.mdx#presenter-screen) | + +:::info +Blueprints can provide default values for these settings. If the blueprint defines defaults, a reset button will appear allowing you to restore the blueprint's recommended configuration. +::: + +Individual Pieces can also override this configuration by setting `displayAbChannel: true` in the blueprint, which forces the AB channel to be displayed regardless of the ShowStyle settings. + +### Action Triggers + +This is a way to set up how - outside of the Point-and-Click Graphical User Interface - actions can be performed in the User Interface. Commonly, these are the _hotkey combinations_ that can be used to either trigger AdLib content or other actions in the larger system. This is done by creating sets of Triggers and Actions to be triggered by them. These pairs can be set at the Show Style level or at the _Sofie Core_ (System) level, for common actions such as doing a Take or activating a Rundown, where you want a shared method of operation. _Sofie Core_ migrations will set up a base set of basic, system-wide Action Triggers for interacting with rundowns, but they can be changed by the System blueprint. + +![Action triggers define modes of interacting with a Rundown](/img/docs/main/features/action_triggers_3.png) + +#### Triggers + +The triggers are designed to be either client-specific or issued by a peripheral device module. + +Currently, the Action Triggers system supports setting up two types of triggeers: Hotkeys and Device Triggers. + +Hotkeys are valid in the scope of a browser window and can be either a single key, a combination of keys (_combo_) or a _chord_ - a sequence of key combinations pressed in a particular order. _Chords_ are popular in some text editing applications and vastly expand the amount of actions that can be triggered from a keyboard, at the expense of the time needed to execute them. Currently, the Hotkey editor in Sofie does not support creating _Chords_, but they can be specified by Blueprints during migrations. + +To edit a given trigger, click on the trigger pill on the left of the Trigger-Action set. When hovering, a **+** sign will appear, allowing you to add a new trigger to the set. + +Device Triggers are valid in the scope of a Studio and will be evaluated on the currently active Rundown in a given Studio. To use Device Triggers, you need to have at least a single [Input Gateway](../installation/installing-a-gateway/input-gateway.md) attached to a Studio and a Device configured in the Input Gateway. Once that's done, when selecting a **Device** trigger type in the pop-up, you can invoke triggers on your Input Device and you will see a preview of the input events shown at the bottom of the pop-up. You can select which of these events should be the trigger by clicking on one of the previews. Note, that some devices differentiate between _Up_ and _Down_ triggers, while others don't. Some may also have other activities that can be done _to_ a trigger. What they are and how they are identified is device-specific and is best discovered through interaction with the device. + +If you would like to set up combination Triggers, using Device Triggers on an Input Device that does not support them natively, you may want to look into [Shift Registers](#shift-registers) + +#### Actions + +The actions are built using a base _action_ (such as _Activate a Rundown_ or _AdLib_) and a set of _filters_, limiting the scope of the _action_. Optionally, some of these _actions_ can take additional _parameters_. These filters can operate on various types of objects, depending on the action in question. All actions currently require that the chain of filters starts with scoping out the Rundown the action is supposed to affect. Currently, there is only one type of Rundown-level filter supported: "The Rundown currently in view". + +The Action Triggers user interface guides the user in a wizard-like fashion through the available _filter_ options on a given _action_. + +![Actions can take additional parameters](/img/docs/main/features/action_triggers_2.png) + +If the action provides a preview of the triggered items and there is an available matching Rundown, a preview will be displayed for the matching objects in that Rundown. The system will select the current active rundown, if it is of the currently-edited ShowStyle, and if not, it will select the first available Rundown of the currently-edited ShowStyle. + +![A preview of the action, as scoped by the filters](/img/docs/main/features/action_triggers_4.png) + +Clicking on the action and filter pills allows you to edit the action parameters and filter parameters. _Limit_ limits the amount of objects to only the first _N_ objects matched - this can significantly improve performance on large data sets. _Pick_ and _Pick last_ filters end the chain of the filters by selecting a single item from the filtered set of objects (the _N-th_ object from the beginning or the end, respectively). _Pick_ implicitly contains a _Limit_ for the performance improvement. This is not true for _Pick last_, though. + +##### Shift Registers + +Shift Register modification actions are a special type of an Action, that modifies an internal state memory of the [Input Gateway](../installation/installing-a-gateway/input-gateway.md) and allows combination triggers, pagination, etc. on devices that don't natively support them or combining multiple devices into a single Control Surface. Refer to _Input Gateway_ documentation for more information on Shift Registers. + +Shift Register actions have no effect in the browser, triggered from a _Hotkey_. + +## Migrations + +The migrations are automatic setup-scripts that help you during initial setup and system upgrades. + +There are system-migrations that comes directly from the version of _Sofie Core_ you're running, and there are also migrations added by the different blueprints. + +It is mandatory to run migrations when you've upgraded _Sofie Core_ to a new version, or upgraded your blueprints. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/sofie-core-settings.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/sofie-core-settings.md new file mode 100644 index 00000000000..a6d00aa139c --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/sofie-core-settings.md @@ -0,0 +1,110 @@ +--- +sidebar_position: 1 +--- + +# Sofie Core: System Configuration + +_Sofie Core_ is configured at it's most basic level using a settings file and environment variables. + +### Environment Variables + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SettingUseDefault valueExample
+ METEOR_SETTINGS + Contents of settings file (see below) + $(cat settings.json) +
+ TZ + The default time zone of the server (used in logging) + Europe/Amsterdam +
+ MAIL_URL + + Email server to use. See{' '} + https://docs.meteor.com/api/email.html + + smtps://USERNAME:PASSWORD@HOST:PORT +
+ LOG_TO_FILE + File path to log to file + /logs/core/ +
+ +### Settings File + +The settings file is an optional JSON file that contains some configuration settings for how the _Sofie Core_ works and behaves. + +To use a settings file: + +- During development: `meteor --settings settings.json` +- During prod: environment variable \(see above\) + +The structure of the file allows for public and private fields. At the moment, Sofie only uses public fields. Below is an example settings file: + +```text +{ + "public": { + "frameRate": 25 + } +} +``` + +There are various settings you can set for an installation. See the list below: + +| **Field name** | Use | Default value | +| :---------------------------- | :---------------------------------------------------------------------------------------------------------------------------- | :------------------------------------- | +| `autoRewindLeavingSegment` | Should segments be automatically rewound after they stop playing | `false` | +| `disableBlurBorder` | Should a border be displayed around the Rundown View when it's not in focus and studio mode is enabled | `false` | +| `defaultTimeScale` | An arbitrary number, defining the default zoom factor of the Timelines | `1` | +| `allowGrabbingTimeline` | Can Segment Timelines be grabbed to scroll them? | `true` | +| `enableHeaderAuth` | If true, enable http header based security measures. See [here](../features/access-levels) for details on using this | `false` | +| `defaultDisplayDuration` | The fallback duration of a Part, when it's expectedDuration is 0. \_\_In milliseconds | `3000` | +| `allowMultiplePlaylistsInGUI` | If true, allows creation of new playlists in the Lobby Gui (rundown list). If false; only pre-existing playlists are allowed. | `false` | +| `followOnAirSegmentsHistory` | How many segments of history to show when scrolling back in time (0 = show current segment only) | `0` | +| `maximumDataAge` | Clean up stuff that are older than this [ms]) | 100 days | +| `poisonKey` | Enable the use of poison key if present and use the key specified. | `'Escape'` | +| `enableNTPTimeChecker` | If set, enables a check to ensure that the system time doesn't differ too much from the specified NTP server time. | `null` | +| `defaultShelfDisplayOptions` | Default value used to toggle Shelf options when the 'display' URL argument is not provided. | `buckets,layout,shelfLayout,inspector` | +| `enableKeyboardPreview` | The KeyboardPreview is a feature that is not implemented in the main Fork, and is kept here for compatibility | `false` | +| `keyboardMapLayout` | Keyboard map layout (what physical layout to use for the keyboard) | STANDARD_102_TKL | +| `customizationClassName` | CSS class applied to the body of the page. Used to include custom implementations that differ from the main Fork. | `undefined` | +| `useCountdownToFreezeFrame` | If true, countdowns of videos will count down to the last freeze-frame of the video instead of to the end of the video | `true` | +| `confirmKeyCode` | Which keyboard key is used as "Confirm" in modal dialogs etc. | `'Enter'` | + +:::info +The exact definition for the settings can be found [in the code here](https://github.com/Sofie-Automation/sofie-core/blob/main/meteor/lib/Settings.ts#L12). +::: diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/faq.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/faq.md new file mode 100644 index 00000000000..73c8373c8f8 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/faq.md @@ -0,0 +1,16 @@ +# FAQ + +## What software license does the system use? + +All main components are using the [MIT license](https://opensource.org/licenses/MIT). + +## Is there anything missing in the public repositories? + +Everything needed to install and configure a fully functioning Sofie system is publicly available, with the following exceptions: + +- A rundown data set describing the actual TV show and of media assets. +- Blueprints for your specific show. + +## When will feature _y_ become available? + +Check out the [issues page](https://github.com/Sofie-Automation/Sofie-TV-automation/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3ARelease), where there are notes on current and upcoming releases. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/_category_.json b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/_category_.json new file mode 100644 index 00000000000..785c16360ba --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Features", + "position": 2 +} diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/access-levels.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/access-levels.md new file mode 100644 index 00000000000..ebf6adfa61d --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/access-levels.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 3 +--- + +# Access Levels + +## Permissions + +There are a few different access levels that users can be assigned. They are not hierarchical, you will often need to enable multiple for each user. +Any client that can access Sofie always has at least view-only access to the rundowns, and system status pages. + +| Level | Summary | +| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------ | +| **studio** | Grants access to operate a studio for playout of a rundown. | +| **configure** | Grants access to the settings pages of Sofie, and other abilities to configure the system. | +| **developer** | Grants access to some tools useful to developers. This also changes some ui behaviours to be less aggressive in what is shown in the rundown view | +| **testing** | Enables the page Test Tools, which contains various tools useful for testing the system during development | +| **service** | Grants access to the external message status page, and some additional rundown management options that are not commonly needed | +| **gateway** | Grants access to various APIs intended for use by the various gateways that connect Sofie to other systems. | + +## Authentication providers + +There are two ways to define the access for each user, which to use depends on your security requirements. + +### Browser based + +:::info + +This is a simple mode that relies on being able to trust every client that can connect to Sofie + +::: + +In this mode, a variety of access levels can be set via the URL. The access level is persisted in browser's Local Storage. + +By default, a user cannot edit settings, nor play out anything. Some of the access levels provide additional administrative pages or helpful tool tips for new users. These modes are persistent between sessions and will need to be manually enabled or disabled by appending a suffix to the url. +Each of the modes listed in the levels table above can be used here, such as by navigating to `https://my-sofie/?studio=1` to enable studio mode, or `https://my-sofie/?studio=0` to disable studio mode. + +There are some additional url parameters that can be used to simplify the granting of permissions: + +- `?help=1` will enable some tooltips that might be useful to new users. +- `?admin=1` will give the user the same access as the _Configuration_ and _Studio_ modes as well as having access to a set of _Test Tools_ and a _Manual Control_ section on the Rundown page. + +#### See Also + +[URL Query Parameters](../../for-developers/url-query-parameters.md) + +### Header based + +:::danger + +This mode is very new and could have some undiscovered holes. +It is known that secrets can be leaked to all clients who can connect to Sofie, which is not desirable. + +::: + +In this mode, we rely on Sofie being run behind a reverse-proxy which will inform Sofie of the permissions of each connection. This allows you to use your organisations preferred auth provider, and translate that into something that Sofie can understand. +To enable this mode, you need to enable the `enableHeaderAuth` property in the [settings file](../configuration/sofie-core-settings.md) + +Sofie expects that for each DDP connection or http request, the `dnt` header will be set containing a comma separated list of the levels from the above table. If the header is not defined or is empty, the connection will have view-only access to Sofie. +This header can also contain simply `admin` to grant the connection permission to everything. +We are using the `dnt` header due to limitations imposed by Meteor, but intend this to become a proper header name in a future release. + +When in this mode, you should make sure that Sofie can only be accessed through the reverse proxy, and that the reverse-proxy will always override any value sent by a client. +Because the value is defined in the http headers, it is not possible to revoke permissions for a user who currently has the ui open. If this is necessary to do, you can force the connection to be dropped by the reverse-proxy. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/api.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/api.md new file mode 100644 index 00000000000..b58e66a4cb4 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/api.md @@ -0,0 +1,19 @@ +--- +sidebar_position: 10 +--- + +# API + +## Sofie User Actions REST API + +Starting with version 1.50.0, there is a semantically-versioned HTTP REST API defined using the [OpenAPI specification](https://spec.openapis.org/oas/v3.0.3) that exposes some of the functionality available through the GUI in a machine-readable fashion. The API specification can be found in the `packages/openapi` folder. The latest version of this API is available in _Sofie Core_ using the endpoint: `/api/1.0`. There should be no assumption of backwards-compatibility for this API, but this API will be semantically-versioned, with redirects set up for minor-version changes for compatibility. + +There is a also a legacy REST API available that can be used to fetch data and trigger actions. The documentation for this API is minimal, but the API endpoints are listed by _Sofie Core_ using the endpoint: `/api/0` + +## Sofie Live Status Gateway + +Starting with version 1.50.0, there is also a separate service available, called _Sofie Live Status Gateway_, running as a separate process, which will connect to the _Sofie Core_ as a Peripheral Device, listen to the changes of it's state and provide a PubSub service offering a machine-readable view into the system. The WebSocket API is defined using the [AsyncAPI specification](https://v2.asyncapi.com/docs/reference/specification/v2.5.0) and the specification can be found in the `packages/live-status-gateway/api` folder. + +## DDP – Core Integration + +If you're planning to build NodeJS applications that talk to _Sofie Core_, we recommend using the [core-integration](https://github.com/Sofie-Automation/sofie-core/tree/master/packages/server-core-integration) library, which exposes a number of callable methods and allows for subscribing to data the same way the [Gateways](../concepts-and-architecture.md#gateways) do it. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/intro.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/intro.md new file mode 100644 index 00000000000..0e68787f702 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/intro.md @@ -0,0 +1,18 @@ +--- +sidebar_position: 1 +--- +# Introduction + +This section documents the user-facing features of Sofie, that is: what is visible in the User Interface when connected to the Sofie Web App. For more information about the playout features of Sofie, see the [For Blueprint Developers](../../for-developers/for-blueprint-developers/intro) section. + +The _Rundowns_ view will display all the active rundowns that the _Sofie Core_ has access to. + +![Rundown View](/img/docs/getting-started/rundowns-in-sofie.png) + +The _Status_ view displays the current status for the attached devices and gateways. + +![Status View – Describes the state of _Sofie Core_](/img/docs/getting-started/status-page.jpg) + +The _Settings_ view contains various settings for the Studio, Show Styles, Blueprints etc. If the link to the settings view is not visible in your application, check your [Access Levels](access-levels.md). More info on specific parts of the _Settings_ view can be found in their corresponding guide sections. + +![Settings View – Describes how the _Sofie Core_ is configured](/img/docs/getting-started/settings-page.jpg) \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/language.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/language.md new file mode 100644 index 00000000000..3c61fb16c36 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/language.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 7 +--- + +# Language + +_Sofie_ uses the [i18n internationalisation framework](https://www.i18next.com/) that allows you to present user-facing views in multiple languages. + +## Language selection + +The UI will automatically detect user browser's default matching and select the best match, falling back to English. You can also force the UI language to any language by navigating to a page with `?lng=xx` query string, for example: + +`http://localhost:3000/?lng=en` + +This choice is persisted in browser's local storage, and the same language will be used until a new forced language is chosen using this method. + +_Sofie_ currently supports three languages: + +- English _(default)_ `en` +- Norwegian bokmål `nb` +- Norwegian nynorsk `nn` + +## Further Reading + +- [List of language tags](https://en.wikipedia.org/wiki/IETF_language_tag) diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/prompter.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/prompter.md new file mode 100644 index 00000000000..aba0e34ec04 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/prompter.md @@ -0,0 +1,245 @@ +--- +sidebar_position: 3 +--- + +# Prompter Screen + +See [Sofie Views and Screens](sofie-views-and-screens.mdx#prompter-screen) to learn how to access the Prompter Screen. + +![Prompter Screen before the first Part is taken](/img/docs/main/features/prompter-view.png) + +The prompter will display the script for the Rundown currently active in the Studio. On Air and Next parts and segments are highlighted - in red and green, respectively - to aid in navigation. In top-right corner of the screen, a Diff clock is shown, showing the difference between planned playback and what has been actually produced. This allows the host to know how far behind/ahead they are in regards to planned execution. + +![Indicators for the On Air and Next part shown underneath the Diff clock](/img/docs/main/features/prompter-view-indicators.png) + +If the user scrolls the prompter ahead or behind the On Air part, helpful indicators will be shown in the right-hand side of the screen. If the On Air or Next part's script is above the current viewport, arrows pointing up will be shown. If the On Air part's script is below the current viewport, a single arrow pointing down will be shown. + +## Customize looks + +The Prompter Screen can be configured using query parameters: + +| Query parameter | Type | Description | Default | +| :-------------- | :----- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------ | +| `mirror` | 0 / 1 | Mirror the display horizontally | `0` | +| `mirrorv` | 0 / 1 | Mirror the display vertically | `0` | +| `fontsize` | number | Set a custom font size of the text. 20 will fit in 5 lines of text, 14 will fit 7 lines etc.. | `14` | +| `marker` | string | Set position of the read-marker. Possible values: "center", "top", "bottom", "hide" | `hide` | +| `margin` | number | Set margin of screen \(used on monitors with overscan\), in %. | `0` | +| `showmarker` | 0 / 1 | If the marker is not set to "hide", control if the marker is hidden or not | `1` | +| `showscroll` | 0 / 1 | Whether the scroll bar should be shown | `1` | +| `followtake` | 0 / 1 | Whether the prompter should automatically scroll to current segment when the operator TAKE:s it | `1` | +| `showoverunder` | 0 / 1 | The timer in the top-right of the prompter, showing the overtime/undertime of the current show. | `1` | +| `debug` | 0 / 1 | Whether to display a debug box showing controller input values and the calculated speed the prompter is currently scrolling at. Used to tweak speedMaps and ranges. | `0` | + +Example: [http://127.0.0.1/prompter/studio0/?mode=mouse&followtake=0&fontsize=20](http://127.0.0.1/prompter/studio0/?mode=mouse&followtake=0&fontsize=20) + +## Controlling the prompter + +The prompter can be controlled by different types of controllers. The control mode is set by a query parameter, like so: `?mode=mouse`. + +| Query parameter | Description | +| :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Default | Controlled by both mouse and keyboard | +| `?mode=mouse` | Controlled by mouse only. [See configuration details](prompter.md#control-using-mouse-scroll-wheel) | +| `?mode=keyboard` | Controlled by keyboard only. [See configuration details](prompter.md#control-using-keyboard) | +| `?mode=shuttlekeyboard` | Controlled by a Contour Design ShuttleXpress, X-keys Jog and Shuttle or any compatible, configured as keyboard-ish device. [See configuration details](prompter.md#control-using-contour-shuttlexpress-or-x-keys-modeshuttlekeyboard) | +| `?mode=shuttlewebhid` | Controlled by a Contour Design ShuttleXpress, using the browser's WebHID API [See configuration details](prompter.md#control-using-contour-shuttlexpress-via-webhid) | +| `?mode=pedal` | Controlled by any MIDI device outputting note values between 0 - 127 of CC notes on channel 8. Analogue Expression pedals work well with TRS-USB midi-converters. [See configuration details](prompter.md#control-using-midi-input-modepedal) | +| `?mode=joycon` | Controlled by Nintendo Switch Joycon, using the HTML5 GamePad API. [See configuration details](prompter.md#control-using-nintendo-joycon-gamepad) | +| `?mode=xbox` | Controlled by Xbox controller, using the HTML5 GamePad API. [See configuration details](prompter.md#control-using-xbox-controller-modexbox) | + +#### Control using mouse \(scroll wheel\) + +The prompter can be controlled in multiple ways when using the scroll wheel: + +| Query parameter | Description | +| :-------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `?controlmode=normal` | Scrolling of the mouse works as "normal scrolling" | +| `?controlmode=speed` | Scrolling of the mouse changes the speed of scrolling. Left-click to toggle, right-click to rewind | +| `?controlmode=smoothscroll` | Scrolling the mouse wheel starts continuous scrolling. Small speed adjustments can then be made by nudging the scroll wheel. Stop the scrolling by making a "larger scroll" on the wheel. | + +has several operating modes, described further below. All modes are intended to be controlled by a computer mouse or similar, such as a presenter tool. + +#### Control using keyboard + +Keyboard control is intended to be used when having a "keyboard"-device, such as a presenter tool. + +| Scroll up | Scroll down | +| :----------- | :------------ | +| `Arrow Up` | `Arrow Down` | +| `Arrow Left` | `Arrow Right` | +| `Page Up` | `Page Down` | +| | `Space` | + +#### Control using Contour ShuttleXpress or X-keys \(_?mode=shuttlekeyboard_\) + +This mode is intended to be used when having a Contour ShuttleXpress or X-keys device, configured to work as a keyboard device. These devices have jog/shuttle wheels, and their software/firmware allow them to map scroll movement to keystrokes from any key-combination. Since we only listen for key combinations, it effectively means that any device outputting keystrokes will work in this mode. + +| Query parameter | Type | Description | Default | +| :----------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | +| `shuttle_speedMap` | Array of numbers | Speeds to scroll by \(px. pr. frame - approx 60fps\) when scrolling forwards. The beginning of the forwards-range maps to the first number in this array, and the end of the forwards-range map to the end of this array. All values in between are being interpolated using a spline curve. | `0, 1, 2, 3, 5, 7, 9, 30]` | + +| Key combination | Function | +| :--------------------------------------------------------- | :------------------------------------- | +| `Ctrl` `Alt` `F1` ... `Ctrl` `Alt` `F7` | Set speed to +1 ... +7 \(Scroll down\) | +| `Ctrl` `Shift` `Alt` `F1` ... `Ctrl` `Shift` `Alt` `F7` | Set speed to -1 ... -7 \(Scroll up\) | +| `Ctrl` `Alt` `+` | Increase speed | +| `Ctrl` `Alt` `-` | Decrease speed | +| `Ctrl` `Alt` `Shift` `F8`, `Ctrl` `Alt` `Shift` `PageDown` | Jump to next Segment and stop | +| `Ctrl` `Alt` `Shift` `F9`, `Ctrl` `Alt` `Shift` `PageUp` | Jump to previous Segment and stop | +| `Ctrl` `Alt` `Shift` `F10` | Jump to top of Script and stop | +| `Ctrl` `Alt` `Shift` `F11` | Jump to Live and stop | +| `Ctrl` `Alt` `Shift` `F12` | Jump to next Segment and stop | + +Configuration files that can be used in their respective driver software: + +- [Contour ShuttleXpress](https://github.com/Sofie-Automation/sofie-core/blob/release26/resources/prompter_layout_shuttlexpress.pref) +- [X-keys](https://github.com/Sofie-Automation/sofie-core/blob/release26/resources/prompter_layout_xkeys.mw3) + +#### Control using Contour ShuttleXpress via WebHID + +This mode uses a Contour ShuttleXpress (Multimedia Controller Xpress) through web browser's WebHID API. + +When opening the Prompter View for the first time, it is necessary to press the _Connect to Contour Shuttle_ button in the top left corner of the screen, select the device, and press _Connect_. + +![Contour ShuttleXpress input mapping](/img/docs/main/features/contour-shuttle-webhid.jpg) + +#### + +#### Control using midi input \(_?mode=pedal_\) + +This mode listens to MIDI CC-notes on channel 8, expecting a linear range like i.e. 0-127. Sutiable for use with expression pedals, but any MIDI controller can be used. The mode picks the first connected MIDI device, and supports hot-swapping \(you can remove and add the device without refreshing the browser\). + +Web-Midi requires the web page to be served over HTTPS, or that the Chrome flag `unsafely-treat-insecure-origin-as-secure` is set. + +If you want to use traditional analogue pedals with 5 volt TRS connection, a converter such as the _Beat Bars EX2M_ will work well. + +| Query parameter | Type | Description | Default | +| :---------------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------- | +| `pedal_speedMap` | Array of numbers | Speeds to scroll by \(px. pr. frame - approx 60fps\) when scrolling forwards. The beginning of the forwards-range maps to the first number in this array, and the end of the forwards-range map to the end of this array. All values in between are being interpolated using a spline curve. | `[1, 2, 3, 4, 5, 7, 9, 12, 17, 19, 30]` | +| `pedal_reverseSpeedMap` | Array of numbers | Same as `pedal_speedMap` but for the backwards range. | `[10, 30, 50]` | +| `pedal_rangeRevMin` | number | The end of the backwards-range, full speed backwards. | `0` | +| `pedal_rangeNeutralMin` | number | The beginning of the backwards-range. | `35` | +| `pedal_rangeNeutralMax` | number | The minimum input to run forward, the start of the forward-range \(min speed\). This is also the end of any "deadband" you want filter out before starting moving forwards. | `80` | +| `pedal_rangeFwdMax` | number | The maximum input, the end of the forward-range \(max speed\) | `127` | + +- `pedal_rangeNeutralMin` has to be greater than `pedal_rangeRevMin` +- `pedal_rangeNeutralMax` has to be greater than `pedal_rangeNeutralMin` +- `pedal_rangeFwdMax` has to be greater than `pedal_rangeNeutralMax` + +![Yamaha FC7 mapped for both a forward (80-127) and backwards (0-35) range.](/img/docs/main/features/yamaha-fc7.jpg) + +The default values allow for both going forwards and backwards. This matches the _Yamaha FC7_ expression pedal. The default values create a forward-range from 80-127, a neutral zone from 35-80 and a reverse-range from 0-35. + +Any movement within forward range will map to the `pedal_speedMap` with interpolation between any numbers in the `pedal_speedMap`. You can turn on `?debug=1` to see how your input maps to an output. This helps during calibration. Similarly, any movement within the backwards rage maps to the `pedal_reverseSpeedMap`. + +**Calibration guide:** + +| **Symptom** | Adjustment | +| :---------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| _"I can't rest my foot without it starting to run"_ | Increase `pedal_rangeNeutralMax` | +| _"I have to push too far before it starts moving"_ | Decrease `pedal_rangeNeutralMax` | +| _"It starts out fine, but runs too fast if I push too hard"_ | Add more weight to the lower part of the `pedal_speedMap` by adding more low values early in the map, compared to the large numbers in the end. | +| _"I have to go too far back to reverse"_ | Increase `pedal_rangeNeutralMin` | +| _"As I find a good speed, it varies a bit in speed up/down even if I hold my foot still"_ | Use `?debug=1` to see what speed is calculated in the position the presenter wants to rest the foot in. Add more of that number in a sequence in the `pedal_speedMap` to flatten out the speed curve, i.e. `[1, 2, 3, 4, 4, 4, 4, 5, ...]` | + +**Note:** The default values are set up to work with the _Yamaha FC7_ expression pedal, and will probably not be good for pedals with one continuous linear range from fully released to fully depressed. A suggested configuration for such pedals \(i.e. the _Mission Engineering EP-1_\) will be like: + +| Query parameter | Suggestion | +| :---------------------- | :-------------------------------------- | +| `pedal_speedMap` | `[1, 2, 3, 4, 5, 7, 9, 12, 17, 19, 30]` | +| `pedal_reverseSpeedMap` | `-2` | +| `pedal_rangeRevMin` | `-1` | +| `pedal_rangeNeutralMin` | `0` | +| `pedal_rangeNeutralMax` | `1` | +| `pedal_rangeFwdMax` | `127` | + +#### Control using Nintendo Joycon \(_?mode=joycon_\) + +This mode uses the browsers Gamapad API and polls connected Joycons for their states on button-presses and joystick inputs. + +The Joycons can operate in 3 modes, the L-stick, the R-stick or both L+R sticks together. Reconnections and jumping between modes works, with one known limitation: **Transition from L+R to a single stick blocks all input, and requires a reconnect of the sticks you want to use.** This seems to be a bug in either the Joycons themselves or in the Gamepad API in general. + +| Query parameter | Type | Description | Default | +| :----------------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | +| `joycon_speedMap` | Array of numbers | Speeds to scroll by \(px. pr. frame - approx 60fps\) when scrolling forwards. The beginning of the forwards-range maps to the first number in this array, and the end of the forwards-range map to the end of this array. All values in between are being interpolated in a spline curve. | `[1, 2, 3, 4, 5, 8, 12, 30]` | +| `joycon_reverseSpeedMap` | Array of numbers | Same as `joycon_speedMap` but for the backwards range. | `[1, 2, 3, 4, 5, 8, 12, 30]` | +| `joycon_rangeRevMin` | number | The end of the backwards-range, full speed backwards. | `-1` | +| `joycon_rangeNeutralMin` | number | The beginning of the backwards-range. | `-0.25` | +| `joycon_rangeNeutralMax` | number | The minimum input to run forward, the start of the forward-range \(min speed\). This is also the end of any "deadband" you want filter out before starting moving forwards. | `0.25` | +| `joycon_rangeFwdMax` | number | The maximum input, the end of the forward-range \(max speed\) | `1` | +| `joycon_rightHandOffset` | number | A ratio to increase or decrease the R Joycon joystick sensitivity relative to the L Joycon. | `1.4` | +| `joycon_invertJoystick` | 0 / 1 | Invert the joystick direction. When enabled, pushing the joystick forward scrolls up instead of down. | `1` | + +- `joycon_rangeNeutralMin` has to be greater than `joycon_rangeRevMin` +- `joycon_rangeNeutralMax` has to be greater than `joycon_rangeNeutralMin` +- `joycon_rangeFwdMax` has to be greater than `joycon_rangeNeutralMax` + +![Nintendo Switch Joycons](/img/docs/main/features/nintendo-switch-joycons.jpg) + +You can turn on `?debug=1` to see how your input maps to an output. + +**Button map:** + +| **Button** | Acton | +| :--------- | :------------------------ | +| L2 / R2 | Go to the "On-air" story | +| L / R | Go to the "Next" story | +| Up / X | Go top the top | +| Left / Y | Go to the previous story | +| Right / A | Go to the following story | + +**Calibration guide:** + +| **Symptom** | Adjustment | +| :------------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| _"The prompter drifts upwards when I'm not doing anything"_ | Decrease `joycon_rangeNeutralMin` | +| _"The prompter drifts downwards when I'm not doing anything"_ | Increase `joycon_rangeNeutralMax` | +| _"It starts out fine, but runs too fast if I move too far"_ | Add more weight to the lower part of the `joycon_speedMap / joycon_reverseSpeedMap` by adding more low values early in the map, compared to the large numbers in the end. | +| _"I can't reach max speed backwards"_ | Increase `joycon_rangeRevMin` | +| _"I can't reach max speed forwards"_ | Decrease `joycon_rangeFwdMax` | +| _"As I find a good speed, it varies a bit in speed up/down even if I hold my finger still"_ | Use `?debug=1` to see what speed is calculated in the position the presenter wants to rest their finger in. Add more of that number in a sequence in the `joycon_speedMap` to flatten out the speed curve, i.e. `[1, 2, 3, 4, 4, 4, 4, 5, ...]` | + +#### Control using Xbox controller \(_?mode=xbox_\) + +This mode uses the browser's Gamepad API to control the prompter with an Xbox controller. It supports Xbox One, Xbox Series X|S, and compatible third-party controllers. + +The controller can be connected via Bluetooth or USB. **Note:** On macOS, Xbox controllers may not be recognized over USB due to driver limitations; Bluetooth is recommended. + +**Scroll control:** + +- **Right Trigger (RT):** Scroll forward - speed is proportional to trigger pressure +- **Left Trigger (LT):** Scroll backward - speed is proportional to trigger pressure + +**Button map:** + +| **Button** | **Action** | +| :---------------- | :------------------------ | +| A | Take (go to next part) | +| B | Go to the "On-air" story | +| X | Go to the previous story | +| Y | Go to the following story | +| LB (Left Bumper) | Go to the top | +| RB (Right Bumper) | Go to the "Next" story | +| D-Pad Up | Scroll up (fine control) | +| D-Pad Down | Scroll down (fine control)| + +**Configuration parameters:** + +| Query parameter | Type | Description | Default | +| :--------------------- | :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | +| `xbox_speedMap` | Array of numbers | Speeds to scroll by (px per frame, ~60fps) when scrolling forwards. Values are interpolated using a spline curve based on trigger pressure. | `[2, 3, 5, 6, 8, 12, 18, 45]` | +| `xbox_reverseSpeedMap` | Array of numbers | Same as `xbox_speedMap` but for the backwards range (left trigger). | `[2, 3, 5, 6, 8, 12, 18, 45]` | +| `xbox_triggerDeadZone` | number | Dead zone for the triggers, to prevent accidental scrolling. Value between 0 and 1. | `0.1` | + +You can turn on `?debug=1` to see how your trigger input maps to scroll speed. + +**Calibration guide:** + +| **Symptom** | **Adjustment** | +| :----------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------- | +| _"It starts scrolling when I'm not touching the trigger"_ | Increase `xbox_triggerDeadZone` (e.g., `0.15` or `0.2`) | +| _"I have to press too hard before it starts moving"_ | Decrease `xbox_triggerDeadZone` (e.g., `0.05`) | +| _"It scrolls too fast"_ | Use smaller values in `xbox_speedMap`, e.g., `[1, 2, 3, 4, 5, 8, 12, 30]` | +| _"It scrolls too slow"_ | Use larger values in `xbox_speedMap`, e.g., `[3, 6, 10, 15, 25, 40, 60, 100]` | +| _"Speed jumps too quickly from slow to fast"_ | Add more intermediate values to `xbox_speedMap` to create a smoother curve, e.g., `[1, 2, 3, 4, 5, 6, 8, 10, 15, 20, 30]` | diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/sofie-views-and-screens.mdx b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/sofie-views-and-screens.mdx new file mode 100644 index 00000000000..f0202708e18 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/sofie-views-and-screens.mdx @@ -0,0 +1,439 @@ +--- +sidebar_position: 2 +--- + +import Tabs from '@theme/Tabs' +import TabItem from '@theme/TabItem' + +# Sofie Views and Screens + +## Definitions + +- A _**View**_ is defined as a particular layout of Sofie's main user interface. + - A _**Mode**_ is one of several ways to configure a particular "View" of Sofie's main user interface. + - A _**Panel**_ is defined as an expandable/collapsible area of Sofie's main user interface. +- A _**Screen**_ is defined a layout intended to be used on an external display, in addition to with Sofie's main user interface. + +## Sofie Views + +### Lobby View + +![Rundown View](/img/docs/lobby-view.png) + +All existing rundowns are listed in the _Lobby View_. + +### Rundown View + +![Rundown View](/img/docs/main/features/active-rundown-example.png) + +The _Rundown View_ is the main view that the producer works in. + +![The Rundown View and naming conventions of components](/img/docs/main/sofie-naming-conventions.png) + +![Take Next](/img/docs/main/take-next.png) + +#### Take Point + +The Take point is currently playing [Part](#part) in the rundown, indicated by the "On Air" line in the GUI. +What's played on air is calculated from the timeline objects in the Pieces in the currently playing part. + +The Pieces inside of a Part determines what's going to happen, the could be indicating things like VT:s, cut to cameras, graphics, or what script the host is going to read. + +:::info +You can TAKE the next part by pressing _F12_ or the _Numpad Enter_ key. +::: + +#### Next Point + +The Next point is the next queued Part in the rundown. When the user clicks _Take_, the Next Part becomes the currently playing part, and the Next point is also moved. + +:::info +Change the Next point by right-clicking in the GUI, or by pressing \(Shift +\) F9 & F10. +::: + +#### Freeze-frame Countdown + +![Part is 1 second heavy, LiveSpeak piece has 7 seconds of playback until it freezes](/img/docs/main/freeze-frame-countdown.png) + +If a Piece has more or less content than the Part's expected duration allows, an additional counter with a Snowflake icon will be displayed, attached to the On Air line, counting down to the moment when content from that Piece will freeze-frame at the last frame. The time span in which the content from the Piece will be visible on the output, but will be frozen, is displayed with an overlay of icicles. + +#### Lookahead + +Elements in the [Next point](#next-point) \(or beyond\) might be pre-loaded or "put on preview", depending on the blueprints and playout devices used. This feature is called "Lookahead". + +#### Rundown View Modes + +In the top-right corner of the Segment, there's a button controlling the display style of a given Segment. The default display style of a Segment can be indicated by the [Blueprints](../concepts-and-architecture.md#blueprints), but the user can switch to a different mode at any time. You can also change the display mode of all Segments at once, using a button in the bottom-right corner of the Rundown View. + +All user interactions work in the Storyboard Mode and List Mode the same as in Timeline Mode: Takes, AdLibs, Holds, and moving the [Next Point](#next-point) around the Rundown. + +##### Timeline Mode + +The default mode for the Rundown. + +##### Storyboard Mode + +In the top-right corner of the Segment, there's a button controlling the display style of a given Segment. The default display style of a Segment can be indicated by the [Blueprints](../concepts-and-architecture.md#blueprints), but the User can switch to a different mode at any time. You can also change the display mode of all Segments at once, using a button in the bottom-right corner of the Rundown View. + +![Storyboard Mode](/img/docs/main/storyboard.png) + +The **_Storyboard_** mode is an alternative to the default **_Timeline_** mode. In Storyboard mode, the accurate placement in time of each Piece is not visualized, so that more Parts can be visualized at once in a single row. This can be particularly useful in Shows without very strict timing planning or where timing is not driven by the User, but rather some external factor; or in Shows where very long Parts are joined with very short ones: sports, events and debates. This mode also does not visualize the history of the playback: rather, it only shows what is currently On Air or is planned to go On Air. + +Storyboard mode selects a "main" Piece of the Part, using the same logic as the [Presenter Screen](#presenter-screen), and presents it with a larger, hover-scrub-enabled Piece for easy preview. The countdown to freeze-frame is displayed in the top-right hand corner of the Thumbnail, once less than 10 seconds remain to freeze-frame. The Transition Piece is displayed on top of the thumbnail. Other Pieces are placed below the thumbnail, stacked in order of playback. After a Piece goes off-air, it will disappear from the view. + +If no more Parts can be displayed in a given Segment, they are stacked in order on the right side of the Segment. The User can scroll through these Parts by click-and-dragging the Storyboard area, or using the mouse wheel - `Alt`+Wheel, if only a vertical wheel is present in the mouse. + +##### List Mode + +Another mode available to display a Segment is the List Mode. In this mode, each _Part_ and it's contents are being displayed as a mini-timeline and it's width is normalized to fit the screen, unless it's shorter than 30 seconds, in which case it will be scaled down accordingly. + +![List Mode](/img/docs/main/list_view.png) + +In this mode, the focus is on the "main" Piece of the Part. Additional _Lower-Third_ Pieces will be displayed on top of the main Piece. Infinite _Lower-Third_ Pieces and all other content can be displayed to the right of the mini-timeline as a set of indicators, one per every Layer. Clicking on those indicators will show a pop-up with the Pieces so that they can be investigated using _hover-scrub_. Indicators can be also shown for Ad-Libs assigned to a Part, for easier discovery by the User. Which Layers should be shown in the columns can be decided in the [Settings ● Layers](../configuration/settings-view.md#show-style) area. A special, larger indicator is reserved for the Script piece, which can be useful to display so-called _out-words_. + +If a Part has an _in-transition_ Piece, it will be displayed to the left of the Part's Take Point. + +This List Mode is designed to be used in productions that are mixing pre-planned and timed segments with more free-flowing production or mixing short live in-camera links with longer pre-produced clips, while trying to keep as much of the show in the viewport as possible, at the expense of hiding some of the content from the User and the _duration_ of the Part on screen having no bearing on it's _width_. This mode also allows Sofie to visualize content _beyond_ the planned duration of a Part. + +:::info +The Segment header area also shows the expected (planned) durations for all the Parts and will also show which Parts are sharing timing in a timing group using a _⌊_ symbol in the place of a counter. +::: + +All user interactions work in the Storyboard and List View mode the same as in Timeline mode: Takes, AdLibs, Holds and moving the [Next Point](#next-point) around the Rundown. + +#### Segment Header Countdowns + +![Each Segment has two clocks — the Segment Time Budget and a Segment Countdown](/img/docs/main/segment-budget-and-countdown.png) + + + +The clock on the left is an indicator of how much time has been spent playing Parts from that Segment in relation to how much time was planned for Parts in that Segment. If more time was spent playing than was planned for, this clock will turn red, there will be a **+** sign in front of it and will begin counting upwards. + + + +The clock on the right is a countdown to the beginning of a given segment. This takes into account unplayed time in the On Air Part and all unplayed Parts between the On Air Part and a given Segment. If there are no unplayed Parts between the On Air Part and the Segment, this counter will disappear. + + + +In the illustration above, the first Segment \(_Ny Sak_\) has been playing for 4 minutes and 25 seconds longer than it was planned for. The second segment \(_Direkte Strømstad\)_ is planned to play for 4 minutes and 40 seconds. There are 5 minutes and 46 seconds worth of content between the current On Air line \(which is in the first Segment\) and the second Segment. + +If you click on the Segment header countdowns, you can switch the _Segment Countdown_ to a _Segment OnAir Clock_ where this will show the time-of-day when a given Segment is expected to air. + +![Each Segment has two clocks - the Segment Time Budget and a Segment Countdown](/img/docs/main/features/segment-header-2.png) + +#### Rundown Dividers + +When using a workflow and blueprints that combine multiple NRCS Rundowns into a single Sofie Rundown \(such as when using the "Ready To Air" functionality in AP ENPS\), information about these individual NRCS Rundowns will be inserted into the Rundown View at the point where each of these incoming Rundowns start. + +![Rundown divider between two NRCS Rundowns in a "Ready To Air" Rundown](/img/docs/main/rundown-divider.png) + +For reference, these headers show the Name, Planned Start and Planned Duration of the individual NRCS Rundown. + +#### Shelf + +The shelf contains lists of AdLibs that can be played out. + +![Shelf](/img/docs/main/shelf.png) + +:::info +The Shelf can be opened by clicking the handle at the bottom of the screen, or by pressing the TAB key +::: + +#### Shelf Layouts + +The _Rundown View_ and the _Detached Shelf View_ UI can have multiple concurrent layouts for any given Show Style. The automatic selection mechanism works as follows: + +1. select the first layout of the `RUNDOWN_LAYOUT` type, +2. select the first layout of any type, +3. use the default layout \(no additional filters\), in the style of `RUNDOWN_LAYOUT`. + +To use a specific mode in these views, you can use the `?layout=...` query string, providing either the ID of the layout or a part of the name. This string will then be matched against all available layouts for the Show Style, and the first matching will be selected. For example, for a layout called `Stream Deck layout`, to open the currently active rundown's Detached Shelf use: + +`http://localhost:3000/activeRundown/studio0/shelf?layout=Stream` + +The Detached Shelf Screen with a custom `DASHBOARD_LAYOUT` allows displaying the Shelf on an auxiliary touch screen, tablet or a Stream Deck device. A specialized Stream Deck view will be used if the view is opened on a device with hardware characteristics matching a Stream Deck device. + +The shelf also contains additional elements, not controlled by the Rundown View Mode. These include Buckets and the Inspector. If needed, these components can be displayed or hidden using additional url arguments: + +| Query parameter | Description | +| :---------------------------------- | :------------------------------------------------------------------------ | +| Default | Display the rundown layout \(as selected\), all buckets and the inspector | +| `?display=layout,buckets,inspector` | A comma-separated list of features to be displayed in the shelf | +| `?buckets=0,1,...` | A comma-separated list of buckets to be displayed | + +- `display`: Available values are: `layout` \(for displaying the Rundown Layout\), `buckets` \(for displaying the Buckets\) and `inspector` \(for displaying the Inspector\). +- `buckets`: The buckets can be specified as base-0 indices of the buckets as seen by the user. This means that `?buckets=1` will display the second bucket as seen by the user when not filtering the buckets. This allows the user to decide which bucket is displayed on a secondary attached screen simply by reordering the buckets on their main view. + +_Note: the Inspector is limited in scope to a particular browser window/screen, so do not expect the contents of the inspector to sync across multiple screens._ + +For the purpose of running the system in a studio environment, there are some additional views that can be used for various purposes: + +#### Sidebar Panel + +##### Switchboard + +![Switchboard](/img/docs/main/switchboard.png) + +The Switchboard allows the producer to turn automation _On_ and _Off_ for sets of devices, as well as re-route automation control between devices - both with an active rundown and when no rundown is active in a [Studio](../concepts-and-architecture.md#system-studio-and-show-style). + +The Switchboard panel can be accessed from the Rundown View's right-hand Toolbar, by clicking on the Switchboard button, next to the Support panel button. + +:::info +Technically, the switchboard activates and deactivates Route Sets. The Route Sets are grouped by Exclusivity Group. If an Exclusivity Group contains exactly two elements with the `ACTIVATE_ONLY` mode, the Route Sets will be displayed on either side of the switch. Otherwise, they will be displayed separately in a list next to an _Off_ position. See also [Settings ● Route sets](../configuration/settings-view#route-sets). +::: + +##### Media Status Panel + +![Media Status panel](/img/docs/main/features/media-status-rundown-view-panel.png) + +This provides an overview of the status of the various Media assets required by +this Rundown for playback. You can sort these assets according to their playout +order, status, Source Layer Name and Piece Name by clicking on the table header. + +Note that while the _Filter..._ text field is focused, you will not be able to +use hotkey triggers for playout actions. You can remove the focus from the field +by pressing the Esc key. + +### Evaluations + +When a broadcast is done, users can input feedback about how the show went in an evaluation form. + +:::info +Evaluations can be configured to be sent to Slack, by setting the "Slack Webhook URL" in the [Settings View](../configuration/settings-view.md) under _Studio_. +::: + +### Settings View + +The [Settings View](../configuration/settings-view.md) is only available to users with the [Access Level](access-levels.md) set correctly. + +### Media Status View + +`/status/media` + +This view is a summary of all the media required for playback for Rundowns +present in this System. This view allows you to see if clips are ready for +playback or if they are still waiting to become available to be transferred +onto a playout system. + +![Media Status View](/img/docs/main/features/media-status.png) + +By default, the Media items are sorted according to their position in the +rundown, and the rundowns are in the same order as in the [Lobby View] +(#lobby-view). You can change the sorting order by clicking on the buttons in +the table header. + +The Rundown View also has a panel that presents this information in the [context of the current Rundown](#media-status-panel). + +### Available screens View + +`/countdowns/:studioId` + +The "Available screens" view provides a centralized location to discover and configure all available screens for a given studio. This page is particularly useful for setting up displays in a studio environment, as it allows you to: + +- Access quick links to common screens (Director, Overlay, Multiview, Active Rundown) +- Configure screens with custom parameters before opening them +- Generate properly formatted URLs with all desired options + +#### Quick Links + +The Quick Links section provides direct access to screens that don't require configuration: + +- **Director Screen** - Shows countdown timers for the director +- **Overlay Screen** - Transparent overlay for presenter displays +- **All Screens in a MultiViewer** - Grid view of all screens simultaneously +- **Active Rundown View** - Currently active rundown for secondary monitors + +#### Configurable Screens + +The Configurable Screens section uses collapsible accordion panels that let you customize settings before opening a screen: + +**Presenter Screen Configuration** +- Select a specific Presenter Layout from available layouts for the Show Style +- Generates URL with `presenterLayout` parameter + +**Camera Screen Configuration** +- Filter by specific Source Layer IDs (e.g., cameras, DVEs) +- Filter by Studio Labels to show only relevant cameras +- Enable fullscreen mode for mobile devices +- Generates URL with `sourceLayerIds`, `studioLabels`, and `fullscreen` parameters + +**Prompter Configuration** +- Configure display options (mirroring, font size, margins, read marker position) +- Select control modes (mouse, keyboard, shuttle devices, MIDI pedal, Joy-Con, Xbox controller) +- Fine-tune controller parameters (speed maps, dead zones, ranges) +- Generates URL with all selected parameters + +Each configuration form generates a complete URL that can be copied or opened directly. This eliminates the need to manually construct query strings for complex screen configurations. + +:::tip +Bookmark the "Available screens" view for your studio (e.g., `/countdowns/studio0`) for quick access when setting up displays or troubleshooting screen configurations. +::: + +## Sofie Screens + +### Prompter Screen + +`/prompter/:studioId` + +![Prompter Screen](/img/docs/main/features/prompter-example.png) + +A fullscreen page which displays the prompter text for the currently active rundown. The prompter can be controlled and configured in various ways, see more at the [Prompter](prompter.md) documentation. If no Rundown is active in a given studio, the [Screensaver](./sofie-views-and-screens.mdx#screensaver) will be displayed. + +### Director Screen + +`/countdowns/:studioId/director` + +![Director Screen](/img/docs/main/features/director-screen-example.png) + +A fullscreen page, intended to be shown to the director. It displays countdown timers for the current and next items in the rundown. If no Rundown is active in a given studio, the [Screensaver](./sofie-views-and-screens.mdx#screensaver) will be shown. + +#### AB Channel Display + +When using the AB Resolver for video playback (where clips are automatically assigned to video server channels A, B, C, etc.), the Presenter Screen can display which channel is currently assigned to each clip. This helps the director and operators identify which video server output is playing or will play next. + +The AB Channel Display appears as a small icon (A, B, C, etc.) next to clips that have AB session assignments. This feature can be enabled and configured in the [Show Style settings](../configuration/settings-view.md#ab-channel-display). + +:::info +AB Channel Display only appears for Pieces that have `abSessions` defined and where the ShowStyle's AB Channel Display configuration matches the Piece's source layer type or ID. +::: + +### Presenter Screen + +`/countdowns/:studioId/presenter` + +![Presenter Screen](/img/docs/main/features/presenter-screen-example.png) + +A fullscreen page, intended to be shown to the studio presenter. It displays countdown timers for the current and next items in the rundown. If no Rundown is active in a given studio, the [Screensaver](sofie-views-and-screens.mdx#screensaver) will be shown. + +This screen can be configured using query parameters: + +| Query parameter | Type | Description | Default | +| :---------------- | :----- | :--------------------------------------------------------------------------------------------------- | :------------------------------- | +| `presenterLayout` | string | The ID or partial name of a Presenter Layout to use. Matched against available layouts for the Show Style. | _(first available layout)_ | + +#### Presenter Screen Overlay + +`/countdowns/:studioId/overlay` + +![Presenter Screen Overlay](/img/docs/main/features/presenter-screen-overlay-example.png) + +A fullscreen page with transparent background, intended to be shown to the studio presenter as an overlay on top of the produced PGM signal. It displays a reduced amount of the information from the regular [Presenter Screen](sofie-views-and-screens.mdx#presenter-screen): the countdown to the end of the current Part, a summary preview \(type and name\) of the next item in the Rundown and the current time of day. If no Rundown is active it will show the name of the Studio. + +### Camera Position Screen + +`/countdowns/:studioId/camera` + +![Camera Position Screen](/img/docs/main/features/camera-view.jpg) + +A fullscreen page designed specifically for use on mobile devices or extra screens displaying a summary of the currently active Rundown, filtered for Parts containing Pieces matching particular Source Layers and Studio Labels. + +The Pieces are displayed as a Timeline, with the Pieces moving right-to-left as time progresses, and Parts being displayed from the current one being played up till the end of the Rundown. The closest (not necessarily _Next_) Part has a countdown timer in the top-right corner showing when it's expected to be Live. Each Part also has a Duration counter on the bottom-right. + +This screen can be configured using query parameters: + +| Query parameter | Type | Description | Default | +| :--------------- | :----- | :--------------------------------------------------------------------------------------------------------- | :----------- | +| `sourceLayerIds` | string | A comma-separated list of Source Layer IDs to be considered for display | _(show all)_ | +| `studioLabels` | string | A comma-separated list of Studio Labels (Piece `.content.studioLabel` values) to be considered for display | _(show all)_ | +| `fullscreen` | 0 / 1 | Should the screen be shown fullscreen on the device on first user interaction | 0 | + +Example: [http://127.0.0.1/countdowns/studio0/camera?sourceLayerIds=camera0,dve0&studioLabels=1,KAM%201,K1,KAM1&fullscreen=1](http://127.0.0.1/countdowns/studio0/camera?sourceLayerIds=camera0,dve0&studioLabels=1,KAM%201,K1,KAM1&fullscreen=1) + +### Active Rundown Screen + +`/activeRundown/:studioId` + +![Active Rundown Screen](/img/docs/main/features/active-rundown-example.png) + +A page which automatically displays the currently active rundown. Can be useful for the producer to have on a secondary screen. + +### Active Rundown Shelf Screen + +`/activeRundown/:studioId/shelf` + +![Active Rundown Shelf](/img/docs/main/features/active-rundown-shelf-example.png) + +A screen which automatically displays the currently active rundown, and shows the Shelf in fullscreen. Can be useful for the producer to have on a secondary screen. + +A shelf layout can be selected by modifying the query string, see [Shelf Layouts](#shelf-layouts). + +### Specific Rundown Shelf Screen + +`/rundown/:rundownId/shelf` + +Displays the Shelf in fullscreen for a rundown. + +### Multiview Screen + +`/countdowns/:studioId/multiview` + +A fullscreen page that displays multiple studio screens simultaneously in a grid layout. This is useful for monitoring all screens at once on a single display. The Multiview Screen embeds the following screens: + +- Presenter Screen +- Director Screen +- Prompter Screen +- Overlay Screen +- Camera Screen + +Each embedded screen shows a label to identify it. This screen is mostly intended for debugging use by developers, but may be useful in control rooms or production environments where operators need to monitor multiple displays at a glance. + +### Screensaver + +When big screen displays \(like Prompter Screen and the Presenter Screen\) do not have any meaningful content to show, an animated screensaver showing the current time and the next planned show will be displayed. If no Rundown is upcoming, the Studio name will be displayed. + +![A screensaver showing the next scheduled show](/img/docs/main/features/next-scheduled-show-example.png) + +### System Status Screen + +:::caution +Documentation for this feature is yet to be written. +::: + +System and devices statuses are displayed here. + +:::info +An API endpoint for the system status is also available under the URL `/health` +::: + +### Message Queue Screen + +:::caution +Documentation for this feature is yet to be written. +::: + +_Sofie Core_ can send messages to external systems \(such as metadata, as-run-logs\) while on air. + +These messages are retained for a period of time, and can be reviewed in this list. + +Messages that was not successfully sent can be inspected and re-sent here. + +### User Log Screen + +The user activity log contains a list of the user-actions that users have previously done. This is used in troubleshooting issues while on air. + +![User Log](/img/docs/main/features/user-log.png) + +#### Columns, explained + +##### Execution time + +The execution time column displays **coreDuration** + **gatewayDuration** \(**timelineResolveDuration**\)": + +- **coreDuration** : The time it took for Core to execute the command \(ie start-of-command 🠺 stored-result-into-database\) +- **gatewayDuration** : The time it took for Playout Gateway to execute the timeline \(ie stored-result-into-database 🠺 timeline-resolved 🠺 callback-to-core\) +- **timelineResolveDuration**: The duration it took in TSR \(in Playout Gateway\) to resolve the timeline + +Important to note is that **gatewayDuration** begins at the exact moment **coreDuration** ends. +So **coreDuration + gatewayDuration** is the full time it took from beginning-of-user-action to the timeline-resolved \(plus a little extra for the final callback for reporting the measurement\). + +##### Action + +Describes what action the user did; e g pressed a key, clicked a button, or selected a menu item. + +##### Method + +The internal name in _Sofie Core_ of what function was called + +##### Status + +The result of the operation. "Success" or an error message. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/system-health.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/system-health.md new file mode 100644 index 00000000000..11ab7046b4d --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/features/system-health.md @@ -0,0 +1,27 @@ +--- +sidebar_position: 11 +--- + +# System Health + +## Legacy healthcheck + +There is a legacy `/health` endpoint used by NRK systems. Its use is being phased out and will eventually be replaced by the new prometheus endpoint. + +## Prometheus + +From version 1.49, there is a prometheus `/metrics` endpoint exposed from Sofie. The metrics exposed from here will increase over time as we find more data to collect. + +Because Sofie is comprised of multiple worker-threads, each metric has a `threadName` label indicitating which it is from. In many cases this field will not matter, but it is useful for the default process metrics, and if your installation has multiple studios defined. + +Each thread exposes some default nodejs process metrics. These are defined by the [`prom-client`](https://github.com/siimon/prom-client#default-metrics) library we are using, and are best described there. + +The current Sofie metrics exposed are: + +| name | type | description | +| ------------------------------------------ | ------- | ------------------------------------------------------------------ | +| sofie_meteor_ddp_connections_total | Gauge | Number of open ddp connections | +| sofie_meteor_publication_subscribers_total | Gauge | Number of subscribers on a Meteor publication (ignoring arguments) | +| sofie_meteor_jobqueue_queue_total | Counter | Number of jobs put into each worker job queues | +| sofie_meteor_jobqueue_success | Counter | Number of successful jobs from each worker | +| sofie_meteor_jobqueue_queue_errors | Counter | Number of failed jobs from each worker | diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/further-reading.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/further-reading.md new file mode 100644 index 00000000000..22c0d3b3e93 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/further-reading.md @@ -0,0 +1,59 @@ +--- +description: This guide has a lot of links. Here they are all listed by section. +--- + +# Further Reading + +## Getting Started + +- [Sofie's Concepts & Architecture](concepts-and-architecture.md) +- [Gateways](concepts-and-architecture.md#gateways) +- [Blueprints](concepts-and-architecture.md#blueprints) + +- Ask questions in the [Sofie Slack Channel](https://sofietv.slack.com/join/shared_invite/zt-2bfz8l9lw-azLeDB55cvN2wvMgqL1alA#/shared-invite/email) + +## Installation & Setup + +### Installing Sofie Core + +- [Windows install for Docker](https://hub.docker.com/editions/community/docker-ce-desktop-windows) +- [Linux install instructions for Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/) +- [Linux install instructions for Docker Compose](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-18-04) +- [Sofie Core Docker File Download](https://hub.docker.com/r/sofietv/tv-automation-server-core) + +### Installing a Gateway + +#### Ingest Gateways and NRCS + +- [MOS Protocol Overview & Documentation](http://mosprotocol.com/) +- Information about ENPS on [The Associated Press' Website](https://workflow.ap.org/) +- Information about iNews: [Avid's Website](https://www.avid.com/solutions/news-production) + +**Google Spreadsheet Gateway** + +- [Demo Blueprints](https://github.com/SuperFlyTV/sofie-demo-blueprints/releases) on GitHub's website. +- [Example Rundown](https://docs.google.com/spreadsheets/d/1iyegRv5MxYYtlVu8uEEMkBYXsLL-71PAMrNW0ZfWRUw/edit?usp=sharing) provided by Sofie. +- [Google Sheets API](https://console.developers.google.com/apis/library/sheets.googleapis.com?) on the Google Developer website. + +### Additional Software & Hardware + +#### Installing CasparCG Server for Sofie + +- [CasparCG Server](https://github.com/CasparCG/server/releases) on GitHub. +- [Media Scanner](https://github.com/CasparCG/media-scanner/releases) on GitHub. +- [CasparCG Launcher](https://github.com/Sofie-Automation/sofie-casparcg-launcher/releases) on GitHub. +- [Microsoft Visual C++ 2017 Redistributable](https://aka.ms/vc14/vc_redist.x64.exe) on Microsoft's website. +- [Blackmagic Design's DeckLink Cards](https://www.blackmagicdesign.com/products/decklink/models) on Blackmagic Design's website. Check the [DeckLink cards](installation/installing-connections-and-additional-hardware/casparcg-server-installation.md#decklink-cards) section for compatibility. +- [Installing a DeckLink Card](https://documents.blackmagicdesign.com/UserManuals/DesktopVideoManual.pdf) as a PDF. +- [Blackmagic Design 'Desktop Video' Driver Download](https://www.blackmagicdesign.com/support/family/capture-and-playback) on Blackmagic Design's website. +- [CasparCG Server Configuration Validator](https://casparcg.net/validator/) + +**Additional Resources** + +- Viz graphics through MSE, info on the [Vizrt](https://www.vizrt.com/) website. +- Information about the [Blackmagic Design's HyperDeck](https://www.blackmagicdesign.com/products/hyperdeckstudio) + +## FAQ, Progress, and Issues + +- [MIT Licence](https://opensource.org/licenses/MIT) +- [Releases and Issues on GitHub](https://github.com/Sofie-Automation/Sofie-TV-automation/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3ARelease) diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/_category_.json b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/_category_.json new file mode 100644 index 00000000000..b6be4c9d358 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Installation", + "position": 3 +} diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/initial-sofie-core-setup.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/initial-sofie-core-setup.md new file mode 100644 index 00000000000..12cef7df14e --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/initial-sofie-core-setup.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 30 +--- + +# Initial Sofie Core Setup + +#### Prerequisites + +* [Installed and running _Sofie Core_](quick-install.md) + +Once _Sofie Core_ has been installed and is running you can begin setting it up. The first step is to navigate to the _Settings page_. Please review the [Sofie Access Level](../features/access-levels.md) page for assistance getting there. + +To upgrade to a newer version or installation of new blueprints, Sofie needs to run its "Upgrade database" procedure to migrate data and pre-fill various settings. You can do this by clicking the _Upgrade Database_ button in the menu. + +![Update Database Section of the Settings Page](/img/docs/getting-started/settings-page-full-update-db-r47.png) + +Fill in the form as prompted and continue by clicking _Run Migrations Procedure_. Sometimes you will need to go through multiple steps before the upgrade is finished. + +Next, you will need to add some [Blueprints](installing-blueprints.md) and add [Gateways](installing-a-gateway/intro.md) to allow _Sofie_ to interpret rundown data and then play out things. + +![Initial Studio Settings Page](/img/docs/getting-started/settings-page-initial-studio.png) + +Next, you will need to add some [Blueprints](installing-blueprints) and add [Gateways](installing-a-gateway/intro) to allow _Sofie_ to interpret rundown data and then play out things. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/_category_.json b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/_category_.json new file mode 100644 index 00000000000..ab70e591ba6 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Installing a Gateway", + "position": 50 +} \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/input-gateway.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/input-gateway.md new file mode 100644 index 00000000000..eeb3dc03600 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/input-gateway.md @@ -0,0 +1,53 @@ +--- +sidebar_position: 40 +--- + +# Input Gateway + +The Input Gateway handles control devices that are not capable of running a Web Browser. This allows Sofie to integrate directly with devices such as: Hardware Panels, GPI input, MIDI devices and external systems being able to send an HTTP Request. + +To install it, begin by downloading the latest release of [Input Gateway from GitHub](https://github.com/Sofie-Automation/sofie-input-gateway/releases). You can now run the `input-gateway.exe` file inside the extracted folder. A warning window may popup about the app being unrecognized. You can get around this by selecting _More Info_ and clicking _Run Anyways_. + +Much like [Package Manager](../installing-package-manager.md), the Sofie instance that Input Gateway needs to connect to is configured through command line arguments. A minimal configuration could look something like this. + +```bash +input-gateway.exe --host --port --https --id --token +``` + +If not connecting over HTTPS, remove the `--https` flag. + +Input Gateway can be launched from [CasparCG Launcher](../installing-connections-and-additional-hardware/casparcg-server-installation#installing-the-casparcg-launcher). This will make management and log collection easier on a production system. + +You can now open the _Sofie Core_, `http://localhost:3000`, and navigate to the _Settings page_. You will see your _Input Gateway_ under the _Devices_ section of the menu. In _Input Devices_ you can add devices that this instance of Input Gateway should handle. Some of the device integrations will allow you to customize the Feedback behavior. The _Device ID_ property will identify a given Input Device in the Studio, so this property can be used for fail-over purposes. + +## Supported devices and protocols + +Currently, input gateway supports: + +- Stream Deck panels +- Skaarhoj panels - _TCP Raw Panel_ mode +- X-Keys panels +- MIDI controllers +- OSC +- HTTP + +## Input Gateway-specific functions + +### Shift Registers + +Input Gateway supports the concept of _Shift Registers_. A Shift Register is an internal variable/state that can be modified using Actions, from within [Action Triggers](../../configuration/settings-view.md#actions). This allows for things such as pagination, _Hold Shift + Another Button_ scenarios, and others on input devices that don't support these features natively. _Shift Registers_ are also global for all devices attached to a single Input Gateway. This allows combining multiple Input devices into a single Control Surface. + +When one of the _Shift Registers_ is set to a value other than `0` (their default state), all triggers sent from that Input Gateway become prefixed with a serialized state of the state registers, making the combination of a _Shift Registers_ state and a trigger unique. + +If you would like to have the same trigger cause the same action in various Shift Register states, add multiple Triggers to the same Action, with different Shift Register combinations. + +Input Gateway supports an unlimited number of Shift Registers, Shift Register numbering starts at 0. + +### AdLib Tally + +Starting with version 0.5.0, Input Gateway can show additional information about the playout state of AdLibs. Select device integrations within Input Gateway support _Styles_ which allow elements of the HID devices to be specifically styled. These Style classes are matched with [Action Triggers](../../configuration/settings-view.md#action-triggers) using Style class names. You can configure additional _Style classes_ for when a given AdLib is "active" (currently playing) or "next" (i.e. will be playing after a take) appending a suffix `:active` and `:next` to a Style class name. + +### Further Reading + +- [Input Gateway Releases on GitHub](https://github.com/Sofie-Automation/sofie-input-gateway/releases) +- [Input Gateway GitHub Page for Developers](https://github.com/Sofie-Automation/sofie-input-gateway) diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/intro.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/intro.md new file mode 100644 index 00000000000..58c96512ad4 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/intro.md @@ -0,0 +1,41 @@ +--- +sidebar_label: Introduction +sidebar_position: 10 +--- +# Introduction: Installing a Gateway + +#### Prerequisites + +* [Installed and running Sofie Core](../quick-install.md) + +The _Sofie Core_ is the primary application for managing the broadcast, but it doesn't play anything out on it's own. A Gateway will establish the connection from _Sofie Core_ to other pieces of hardware or remote software. A basic setup may include the [Spreadsheet Gateway](rundown-or-newsroom-system-connection/google-spreadsheet.md) which will ingest a rundown from Google Sheets then, use the [Playout Gateway](playout-gateway.md) send commands to a CasparCG Server graphics playout, an ATEM vision mixer, and / or the [Sisyfos audio controller](https://github.com/olzzon/sisyfos-audio-controller). + + + +Setting up a gateway (also called Peripheral Device) from scratch generally is a five-step process: +1. Start the executable image and have it connect to Sofie Core +2. Assign the new Peripheral Device to a Studio +3. Configure the gateway inside the Sofie user interface, configure *sub-devices* \(MOS primary & secondary, video mixers, playout servers, HMI devices\) if applicable +4. Restart the gateway to apply the new settings +5. Verify connection on the *Status* page in Sofie + +:::tip +You can expect the initial connection in Step 1 to fail. This is expected. Peripheral Devices cannot be connected to Sofie unless they are assigned to a Studio. This initial connection is required to inform Sofie about the capabilities of the gateway and set up authorization tokens that will be expected by Sofie in subsequent connections. Do not be discouraged by the gateway shutting down or restarting and just follow the steps above as described. +::: + +### Gateways and their types and functions + +* [Playout Gateway](playout-gateway.md) - sends commands and modifies the state of devices in your Control Room and Studio: video servers, mixers, LED screens, lighting controllers & graphics systems +* [Package Manager](../installing-package-manager.md) - checks if media required for a successful production is where it should be, produces proxy versions for preview inside of Rundown View, does quality control of the media and provides feedback to the Blueprints and the User +* [Input Gateway](input-gateway.md) - receives signals from and provides support for *Human Interface Devices* devices such as Stream Decks, Skaarhoj panels and MIDI devices +* Live Status Gateway - provides support for external services that would like to know about the state of a Studio in Sofie, incl. currently playing Parts and Pieces, available AdLibs, etc. + +### Rundown & Newsroom Gateways + +* [Google Spreadsheet Gateway](rundown-or-newsroom-system-connection/google-spreadsheet.md) - supports creating Rundowns inside of Google Spreadsheet cloud service +* [iNEWS Gateway](rundown-or-newsroom-system-connection/inews-gateway.md) - integrates with Avid iNEWS via FTP +* [MOS Gateway](rundown-or-newsroom-system-connection/mos-gateway.md) - integrates with MOS-compatible NRCS systems (AP ENPS, CGI OpenMedia, Octopus Newsroom, Saga, among others) +* [Rundown Editor](../rundown-editor.md) - a minimal, self-contained Rundown creation utility + diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/playout-gateway.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/playout-gateway.md new file mode 100644 index 00000000000..5f4275a19bb --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/playout-gateway.md @@ -0,0 +1,9 @@ +--- +sidebar_position: 30 +--- + +# Playout Gateway + +The _Playout Gateway_ handles interaction with external pieces of hardware or software by sending commands that will playout rundown content. This gateway used to be developed separately but development has been moved into the main _Sofie Core_ component. + +The playout gateway service is included the example Docker Compose file found in the [Quick install](../installing-sofie-server-core.md) diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json new file mode 100644 index 00000000000..d0518625047 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Rundown or Newsroom System Connection", + "position": 15 +} \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/google-spreadsheet.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/google-spreadsheet.md new file mode 100644 index 00000000000..b9a86e4604e --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/google-spreadsheet.md @@ -0,0 +1,52 @@ +# Google Spreadsheet Gateway + +The Spreadsheet Gateway is an application for piping data between Sofie Core and Spreadsheets on Google Drive. + +### Installing the Spreadsheet Gateway + +If you are using the example Docker Compose file found in the [Quick install](../../installing-sofie-server-core.md), then the configuration for the Spreadsheet Gateway is includedin the `spreadsheet-gateway` docker-compose profile. + +You can activate the profile by setting `COMPOSE_PROFILES=spreadsheet-gateway` as an environment variable or by writing that to a file called `.env` in the same folder as the docker-compose file. For more information, see the [docker documentation on Compose profiles](https://docs.docker.com/compose/how-tos/profiles/). + +If you are not using the example docker-compose, please follow the [instructions listed on the GitHub page](https://github.com/SuperFlyTV/spreadsheet-gateway) labeled _Installation \(for developers\)_. + +### Example Blueprints for Spreadsheet Gateway + +To begin with, you will need to install a set of Blueprints that can handle the data being sent from the _Gateway_ to _Sofie Core_. Download the `demo-blueprints-r*.zip` file containing the blueprints you need from the [Demo Blueprints GitHub Repository](https://github.com/SuperFlyTV/sofie-demo-blueprints/releases). It is recommended to choose the newest release but, an older _Sofie Core_ version may require a different Blueprint version. The _Rundown page_ will warn you about any issue and display the desired versions. + +Instructions on how to install any Blueprint can be found in the [Installing Blueprints](../../installing-blueprints.md) section from earlier. + +### Spreadsheet Gateway Configuration + +Once the Gateway has been installed, you can navigate to the _Settings page_ and check the newly added Gateway is listed as _Spreadsheet Gateway_ under the _Devices section_. + +Before you select the Device, you want to add it to the current _Studio_ you are using. Select your current Studio from the menu and navigate to the _Attached Devices_ option. Click the _+_ icon and select the Spreadsheet Gateway. + +Now you can select the _Device_ from the _Devices menu_ and click the link provided to enable your Google Drive API to send files to the _Sofie Core_. The page that opens will look similar to the image below. + +![Nodejs Quickstart page](/img/docs/installation/installing-a-gateway/rundown-or-newsroom-system-connection/nodejs-quickstart.png) +xx +Make sure to follow the steps in **Create a project and enable the API** and enable the **Google Drive API** as well as the **Google Sheets API**. Your "APIs and services" Dashboard should now look as follows: + +![APIs and Services Dashboard](/img/docs/installation/installing-a-gateway/rundown-or-newsroom-system-connection/apis-and-services-dashboard.png) + +Now follow the steps in **Create credentials** and make sure to create an **OAuth Client ID** for a **Desktop App** and download the credentials file. + +![Create Credentials page](/img/docs/installation/installing-a-gateway/rundown-or-newsroom-system-connection/create-credentials.png) + +Use the button to download the configuration to a file and navigate back to _Sofie Core's Settings page_. Select the Spreadsheet Gateway, then click the _Browse_ button and upload the configuration file you just downloaded. A new link will appear to confirm access to your google drive account. Select the link and in the new window, select the Google account you would like to use. Currently, the Sofie Core Application is not verified with Google so you will need to acknowledge this and proceed passed the unverified page. Click the _Advanced_ button and then click _Go to QuickStart \( Unsafe \)_. + +After navigating through the prompts you are presented with your verification code. Copy this code into the input field on the _Settings page_ and the field should be removed. A message confirming the access token was saved will appear. + +You can now navigate to your Google Drive account and create a new folder for your rundowns. It is important that this folder has a unique name. Next, navigate back to _Sofie Core's Settings page_ and add the folder name to the appropriate input. + +The indicator should now read _Good, Watching folder 'Folder Name Here'_. Now you just need an example rundown.[ Navigate to this Google Sheets file](https://docs.google.com/spreadsheets/d/1iyegRv5MxYYtlVu8uEEMkBYXsLL-71PAMrNW0ZfWRUw/edit?usp=sharing) and select the _File_ menu and then select _Make a copy_. In the popup window, select _My Drive_ and then navigate to and select the rundowns folder you created earlier. + +At this point, one of two things will happen. If you have the Google Sheets API enabled, this is different from the Google Drive API you enabled earlier, then the Rundown you just copied will appear in the Rundown page and is accessible. The other outcome is the Spreadsheet Gateway status reads _Unknown, Initializing..._ which most likely means you need to enable the Google Sheets API. Navigate to the[ Google Sheets API Dashboard with this link](https://console.developers.google.com/apis/library/sheets.googleapis.com?) and click the _Enable_ button. Navigate back to _Sofie's Settings page_ and restart the Spreadsheet Gateway. The status should now read, _Good, Watching folder 'Folder Name Here'_ and the rundown will appear in the _Rundown page_. + +### Further Reading + +- [Demo Blueprints](https://github.com/SuperFlyTV/sofie-demo-blueprints/) GitHub Page for Developers +- [Example Rundown](https://docs.google.com/spreadsheets/d/1iyegRv5MxYYtlVu8uEEMkBYXsLL-71PAMrNW0ZfWRUw/edit?usp=sharing) provided by Sofie. +- [Google Sheets API](https://console.developers.google.com/apis/library/sheets.googleapis.com?) on the Google Developer website. +- [Spreadsheet Gateway](https://github.com/SuperFlyTV/spreadsheet-gateway) GitHub Page for Developers diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md new file mode 100644 index 00000000000..23daffc28a1 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md @@ -0,0 +1,8 @@ +# iNEWS Gateway + +The iNEWS Gateway communicates with an iNEWS system to ingest and remain in sync with a rundown. The rundowns will update in real time and any changes made will be seen from within your Rundown View. + +The setup for the iNEWS Gateway is already in the Docker Compose file you downloaded earlier. Remove the _\#_ symbols from the start of the section labelled `inews-gateway:` and make sure that other ingest gateway sections have a _\#_ prefix on each line. + +Although the iNEWS Gateway is available free of charge, an iNEWS license is not. Visit [Avid's website](https://www.avid.com/products/inews/how-to-buy) to find an iNEWS reseller that handles your geographic area. + diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md new file mode 100644 index 00000000000..2d5200d62eb --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md @@ -0,0 +1,21 @@ +--- +sidebar_position: 1 +--- +# Rundown & Newsroom Systems + +NewsRoom Computer Systems (NRCS) are software suites that manage various parts of news production. Many of these systems support some sort of Rundown creation module that allows authoring live show Rundowns by organizing them into units and sub-units such as Pages, Items, Cues, etc. + +Sofie Core doesn't talk directly to the newsroom systems, but instead via one of the Ingest Gateways. The purpose of these Gateways is to act as adapters for the various protocols used by these systems, while keeping as much fidelity as possible in the incoming data. + +Some of the currently available options in the Sofie ecosystem include Google Docs Spreadsheet Gateway, iNEWS Gateway, and the MOS Gateway which can handle interacting with any system that communicates via MOS \([Media Object Server Communications Protocol](http://mosprotocol.com/)\). + +[Rundown Editor](../../rundown-editor.md) is a special case of an Ingest Gateway that acts as a simple Rundown Editor itself. + +### Further Reading + +* [MOS Protocol Overview & Documentation](http://mosprotocol.com/) +* [iNEWS on Avid's Website](https://www.avid.com/products/inews/how-to-buy) +* [ENPS on The Associated Press' Website](https://www.ap.org/enps/support) + + + diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md new file mode 100644 index 00000000000..94179ad1757 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md @@ -0,0 +1,19 @@ +# MOS Gateway + +The MOS Gateway communicates with a device that supports the [MOS protocol](http://mosprotocol.com/wp-content/MOS-Protocol-Documents/MOS-Protocol-2.8.4-Current.htm) to ingest and remain in sync with a rundown. It can connect to any editorial system \(NRCS\) that uses version 2.8.4 of the MOS protocol, such as ENPS, and sync their rundowns with the _Sofie Core_. The rundowns are kept updated in real time and any changes made will be seen in the Sofie GUI. + +MOS 2.8.4 uses TCP Sockets to send XML messages between the NRCS and the Automation Systems. This is done via two open ports on the Automation System side (the *upper* and *lower* port) and two ports on the NRCS side (*upper* and *lower* as well). + +The setup for the MOS Gateway is handled in the Docker Compose in the [Quick Install](../../quick-install.md) page. Remove the _\#_ symbols from the start of the section labelled `mos-gateway:` and make sure that other ingest gateway sections have a _\#_ prefix. + +You will also need to configure your NRCS to connect to Sofie. Refer to your NRCS's documentation on how that needs to be done. + +After the Gateway is deployed, you will need to assign it to a Studio and you will need to go into *Settings* 🡒 *Studios* 🡒 *Your studio name* -> *Peripheral Devices* 🡒 *MOS gateway* 🡒 Edit and configure the MOS ID that this Gateway will use when talking to the NRCS. This needs to match the configuration within your NRCS. + +Then, in the *Ingest Devices* section of the *Peripheral Devices* page, use the **+** button to add a new *MOS device*. In *Peripheral Device ID* select *MOS gateway* and in *Device Type* select *MOS Device*. You will then be able to provide the MOS ID of your Primary and Secondary NRCS servers and enter their Hostname/IP Address and Upper and Lower Port information. + +:::warning +One thing to note if managing the `mos-gateway` manually: It needs a few ports open \(10540, 10541 by default\) for MOS-messages to be pushed to it from the NRCS. If the defaults are changed in Peripheral Device settings, this needs to be reflected by Docker configuration changes. +::: + + diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-blueprints.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-blueprints.md new file mode 100644 index 00000000000..a56fdce59a9 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-blueprints.md @@ -0,0 +1,46 @@ +--- +sidebar_position: 40 +--- + +# Installing Blueprints + +#### Prerequisites + +- [Installed and running Sofie Core](quick-install.md) +- [Initial Sofie Core Setup](initial-sofie-core-setup.md) + +Blueprints are little plug-in programs that runs inside _Sofie_. They are the logic that determines how _Sofie_ interacts with rundowns, hardware, and media. + +Blueprints are custom JavaScript scripts that you create yourself \(or download an existing one\). There are a set of example Blueprints for the Spreadsheet Gateway and Rundown Editor available for use here: [https://github.com/SuperFlyTV/sofie-demo-blueprints](https://github.com/SuperFlyTV/sofie-demo-blueprints). You can learn more about them in the [Blueprints section](../../for-developers/for-blueprint-developers/intro.md) + +To begin installing any Blueprint, navigate to the _Settings page_. Getting there is covered in the [Access Levels](../features/access-levels.md) page. + +![The Settings Page](/img/docs/getting-started/settings-page.jpg) + +To upload a new blueprint, click the _+_ icon next to Blueprints menu option. Select the newly created Blueprint and upload the local blueprint JS file. You will get a confirmation if the installation was successful. + +There are 3 types of blueprints: System, Studio and Show Style: + +### System Blueprint + +_System Blueprints handles some basic functionality on how the Sofie system will operate._ + +After you've uploaded your System Blueprint JS bundle, click _Assign_ in the blueprint-page to assign it as system-blueprint. + +### Studio Blueprint + +_Studio Blueprints determine how Sofie will interact with the hardware in your studio._ + +After you've uploaded your Studio Blueprint JS bundle, navigate to a Studio in the settings and assign the new Blueprint to it \(under the label _Blueprint_ \). + +After having installed the Blueprint, the Studio's baseline will need to be reloaded. On the Studio page, click the button _Reload Baseline_. This will also be needed whenever you have changed any settings. + +### Show Style Blueprint + +_Show Style Blueprints determine how your show will look / feel._ + +After you've uploaded your Show Style Blueprint JS bundle, navigate to a Show Style in the settings and assign the new Blueprint to it \(under the label _Blueprint_ \). + +### Further Reading + +- [Community Blueprints Supporting Spreadsheet Gateway and Rundown Editor](https://github.com/SuperFlyTV/sofie-demo-blueprints) diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/README.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/README.md new file mode 100644 index 00000000000..7310b1e577d --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/README.md @@ -0,0 +1,35 @@ +# Additional Software & Hardware + +#### Prerequisites + +* [Installed and running Sofie Core](../quick-install.md) +* [Installed Playout Gateway](../installing-a-gateway/playout-gateway.md) +* [Installed and configured Studio Blueprints](../installing-blueprints.md#installing-a-studio-blueprint) + +The following pages are broken up by equipment type that is supported by Sofie's Gateways. + +## Playout & Recording +* [CasparCG Graphics and Video Server](casparcg-server-installation.md) - _Graphics / Playout / Recording_ +* [Blackmagic Design's HyperDeck](https://www.blackmagicdesign.com/products/hyperdeckstudio) - _Recording_ +* [Quantel](http://www.quantel.com) Solutions - _Playout_ +* [Vizrt](https://www.vizrt.com/) Graphics Solutions - _Graphics / Playout_ + +## Vision Mixers +* [Blackmagic's ATEM](https://www.blackmagicdesign.com/products/atem) hardware vision mixers +* [vMix](https://www.vmix.com/) software vision mixer \(coming soon\) + +## Audio Mixers +* [Sisyfos](https://github.com/olzzon/sisyfos-audio-controller) audio controller +* [Lawo sound mixers_,_](https://www.lawo.com/applications/broadcast-production/audio-consoles.html) _using emberplus protocol_ +* Generic OSC \(open sound control\) + +## PTZ Cameras +* [Panasonic PTZ](https://pro-av.panasonic.net/en/products/ptz_camera_systems.html) cameras + +## Lights +* [Pharos](https://www.pharoscontrols.com/) light control + +## Other +* Generic OSC \(open sound control\) +* Generic HTTP requests \(to control http-REST interfaces\) +* Generic TCP-socket diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/_category_.json b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/_category_.json new file mode 100644 index 00000000000..aea5cfb8179 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Installing Connections and Additional Hardware", + "position": 60 +} \ No newline at end of file diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md new file mode 100644 index 00000000000..be682ca1d55 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md @@ -0,0 +1,224 @@ +--- +title: Installing CasparCG Server for Sofie +description: CasparCG Server +--- + +# Installing CasparCG Server for Sofie + +Although CasparCG Server is an open source program that is free to use for both personal and cooperate applications, the hardware needed to create and execute high quality graphics is not. You can get a preview running without any additional hardware but, it is not recommended to use CasparCG Server for production in this manner. To begin, you will install the CasparCG Server on your machine then add the additional configuration needed for your setup of choice. + +## Installing the CasparCG Server + +To begin, download the latest release of [CasparCG Server from GitHub](https://github.com/casparcg/server/releases). While some Sofie users have their own fork of CasparCG, we recommend the official builds. + +Once downloaded, extract the files into a folder and navigate inside. This folder contains your CasparCG Server Configuration file, `casparcg.config`, and your CasparCG Server executable, `casparcg.exe`. + +How you will configure the CasparCG Server will depend on the number of DeckLink cards your machine contains. The first subsection for each CasparCG Server setup, labeled _Channels_, will contain the unique portion of the configuration. The following is the majority of the configuration file that will be consistent between setups. + +```markup + + + debug + + + + media/ + log/ + data/ + template/ + + secret + + + + + + 5250 + AMCP + + + + + localhost + 8000 + + + +``` + +One additional note, the Server does require the configuration file be named `casparcg.config`. + +### Installing the CasparCG Launcher + +You can launch both of your CasparCG applications with the [CasparCG Launcher.](https://github.com/Sofie-Automation/sofie-casparcg-launcher) Download the `.exe` file in the latest release and once complete, move the file to the same folder as your `casparcg.exe` file. + +## Configuring Windows + +### Required Software + +Windows will require you install [Microsoft's Visual C++ 2015 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=52685) to run the CasparCG Server properly. Before downloading the redistributable, please ensure it is not already installed on your system. Open your programs list and in the popup window, you can search for _C++_ in the search field. If _Visual C++ 2015_ appears, you do not need install the redistributable. + +If you need to install redistributable then, navigate to [Microsoft's website](https://www.microsoft.com/en-us/download/details.aspx?id=52685) and download it from there. Once downloaded, you can run the `.exe` file and follow the prompts. + +## Hardware Recommendations + +Although CasparCG Server can be run on some lower end hardware, it is only recommended to do so for non-production uses. Below is a table of the minimum and preferred specs depending on what type of system you are using. + +| System Type | Min CPU | Pref CPU | Min GPU | Pref GPU | Min Storage | Pref Storage | +| :------------ | :--------------- | :------------------------ | :------- | :----------- | :------------- | :------------- | +| Development | i5 Gen 6i7 Gen 6 | GTX 1050 | GTX 1060 | GTX 1060 | NVMe SSD 500gb | | +| Prod, 1 Card | i7 Gen 6 | i7 Gen 7 | GTX 1060 | GTX 1070 | NVMe SSD 500gb | NVMe SSD 500gb | +| Prod, 2 Cards | i9 Gen 8 | i9 Gen 10 Extreme Edition | RTX 2070 | Quadro P4000 | Dual Drives | Dual Drives | + +For _dual drives_, it is recommended to use a smaller 250gb NVMe SSD for the operating system. Then a faster 1tb NVMe SSD for the CasparCG Server and media. It is also recommended to buy a drive with about 40% storage overhead. This is for SSD p~~e~~rformance reasons and Sofie will warn you about this if your drive usage exceeds 60%. + +### DeckLink Cards + +There are a few SDI cards made by Blackmagic Design that are supported by CasparCG. The base model, with four bi-directional input and outputs, is the [Duo 2](https://www.blackmagicdesign.com/products/decklink/techspecs/W-DLK-31). If you need additional channels, use the [Quad 2](https://www.blackmagicdesign.com/products/decklink/techspecs/W-DLK-30) which supports eight bi-directional inputs and outputs. Be aware the BNC connections are not the standard BNC type. B&H offers [Mini BNC to BNC connecters](https://www.bhphotovideo.com/c/product/1462647-REG/canare_cal33mb018_mini_rg59_12g_sdi_4k.html). Finally, for 4k support, use the [8K Pro](https://www.blackmagicdesign.com/products/decklink/techspecs/W-DLK-34) which has four bi-directional BNC connections and one reference connection. + +Here is the Blackmagic Design PDF for [installing your DeckLink card \( Desktop Video Device \).](https://documents.blackmagicdesign.com/UserManuals/DesktopVideoManual.pdf) + +Once the card in installed in your machine, you will need to download the controller from Blackmagic's website. Navigate to [this support page](https://www.blackmagicdesign.com/support/family/capture-and-playback), it will only display Desktop Video Support, and in the _Latest Downloads_ column download the most recent version of _Desktop Video_. Before installing, save your work because Blackmagic's installers will force you to restart your machine. + +Once booted back up, you should be able to launch the Desktop Video application and see your DeckLink card. + +![Blackmagic Design's Desktop Video Application](/img/docs/installation/installing-connections-and-additional-hardware/desktop-video.png) + +Click the icon in the center of the screen to open the setup window. Each production situation will very in frame rate and resolution so go through the settings and set what you know. Most things are set to standards based on your region so the default option will most likely be correct. + +![Desktop Video Settings](/img/docs/installation/installing-connections-and-additional-hardware/desktop-video-settings.png) + +If you chose a DeckLink Duo, then you will also need to set SDI connectors one and two to be your outputs. + +![DeckLink Duo SDI Output Settings](/img/docs/installation/installing-connections-and-additional-hardware/decklink_duo_card.png) + +## Hardware-specific Configurations + +### Preview Only \(Basic\) + +A preview only version of CasparCG Server does not lack any of the features of a production version. It is called a _preview only_ version because the standard outputs on a computer, without a DeckLink card, do not meet the requirements of a high quality broadcast graphics machine. It is perfectly suitable for development though. + +#### Required Hardware + +No additional hardware is required, just the computer you have been using to follow this guide. + +#### Configuration + +The default configuration will give you one preview window. No additional changes need to be made. + +### Single DeckLink Card \(Production Minimum\) + +#### Required Hardware + +To be production ready, you will need to output an SDI or HDMI signal from your production machine. CasparCG Server supports Blackmagic Design's DeckLink cards because they provide a key generator which will aid in keeping the alpha and fill channels of your graphics in sync. Please review the [DeckLink Cards](casparcg-server-installation.md#decklink-cards) section of this page to choose which card will best fit your production needs. + +#### Configuration + +You will need to add an additional consumer to your`caspar.config` file to output from your DeckLink card. After the screen consumer, add your new DeckLink consumer like so. + +```markup + + + 1080i5000 + stereo + + + 1 + true + + + + + 1 + 1 + true + stereo + normal + external_separate_device + false + 3 + + + + + +``` + +You may no longer need the screen consumer. If so, you can remove it and all of it's contents. This will dramatically improve overall performance. + +### Multiple DeckLink Cards \(Recommended Production Setup\) + +#### Required Hardware + +For a preferred production setup you want a minimum of two DeckLink Duo 2 cards. This is so you can use one card to preview your media, while your second card will support the program video and audio feeds. For CasparCG Server to recognize both cards, you need to add two additional channels to the `caspar.config` file. + +```markup + + + 1080i5000 + stereo + + + 1 + true + + + + + 1 + 1 + true + stereo + normal + external_separate_device + false + 3 + + + + + 2 + 2 + true + stereo + normal + external_separate_device + false + 3 + + + + + +``` + +### Validating the Configuration File + +Once you have setup the configuration file, you can use an online validator to check and make sure it is setup correctly. Navigate to the [CasparCG Server Config Validator](https://casparcg.net/validator/) and paste in your entire configuration file. If there are any errors, they will be displayed at the bottom of the page. + +### Launching the Server + +Launching the Server is the same for each hardware setup. This means you can run `casparcg-launcher.exe` and the server and media scanner will start. There will be two additional warning from Windows. The first is about the EXE file and can be bypassed by selecting _Advanced_ and then _Run Anyways_. The second menu will be about CasparCG Server attempting to access your firewall. You will need to allow access. + +A window will open and display the status for the server and scanner. You can start, stop, and/or restart the server from here if needed. An additional window should have opened as well. This is the main output of your CasparCG Server and will contain nothing but a black background for now. If you have a DeckLink card installed, its output will also be black. + +## Connecting Sofie to the CasparCG Server + +Now that your CasparCG Server software is running, you can connect it to the _Sofie Core_. Navigate back to the _Settings page_ and in the menu, select the _Playout Gateway_. If the _Playout Gateway's_ status does not read _Good_, then please review the [Installing and Setting up the Playout Gateway](../installing-a-gateway/playout-gateway.md) section of this guide. + +Under the Sub Devices section, you can add a new device with the _+_ button. Then select the pencil \( edit \) icon on the new device to open the sub device's settings. Select the _Device Type_ option and choose _CasparCG_ from the drop-down menu. Some additional fields will be added to the form. + +The _Host_ and _Launcher Host_ fields will be _localhost_. The _Port_ will be CasparCG's TCP port responsible for handling the AMCP commands. It defaults to 5052 in the `casparcg.config` file. The _Launcher Port_ will be the CasparCG Launcher's port for handling HTTP requests. It will default to 8005 and can be changed in the _Launcher's settings page_. Once all four fields are filled out, you can click the check mark to save the device. + +In the _Attached Sub Devices_ section, you should now see the status of the CasparCG Server. You may need to restart the Playout Gateway if the status is _Bad_. + +## Further Reading + +- [CasparCG Server Releases](https://github.com/CasparCG/server/releases) on GitHub. +- [Media Scanner Releases](https://github.com/CasparCG/media-scanner/releases) on GitHub. +- [CasparCG Launcher](https://github.com/Sofie-Automation/sofie-casparcg-launcher) on GitHub. +- [Microsoft Visual C++ 2015 Redistributable](https://www.microsoft.com/en-us/download/details.aspx?id=52685) on Microsoft's website. +- [Blackmagic Design's DeckLink Cards](https://www.blackmagicdesign.com/products/decklink/models) on Blackmagic's website. Check the [DeckLink cards](casparcg-server-installation.md#decklink-cards) section for compatibility. +- [Installing a DeckLink Card](https://documents.blackmagicdesign.com/UserManuals/DesktopVideoManual.pdf) as a PDF. +- [Desktop Video Download Page](https://www.blackmagicdesign.com/support/family/capture-and-playback) on Blackmagic's website. +- [CasparCG Configuration Validator](https://casparcg.net/validator/) diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md new file mode 100644 index 00000000000..9833fb45a43 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md @@ -0,0 +1,35 @@ +# Adding FFmpeg and FFprobe to your PATH on Windows + +Some parts of Sofie (specifically the Package Manager) require that [`FFmpeg`](https://www.ffmpeg.org/) and [`FFprobe`](https://ffmpeg.org/ffprobe.html) be available in your `PATH` environment variable. This guide will go over how to download these executables and add them to your `PATH`. + +### Installation + +1. `FFmpeg` and `FFprobe` can be downloaded from the [FFmpeg Downloads page](https://ffmpeg.org/download.html) under the "Get packages & executable files" heading. At the time of writing, there are two sources of Windows builds: `gyan.dev` and `BtbN` -- either one will work. +2. Once downloaded, extract the archive to some place permanent such as `C:\Program Files\FFmpeg`. + - You should end up with a `bin` folder inside of `C:\Program Files\FFmpeg` and in that `bin` folder should be three executables: `ffmpeg.exe`, `ffprobe.exe`, and `ffplay.exe`. +3. Open your Start Menu and type `path`. An option named "Edit the system environment variables" should come up. Click on that option to open the System Properties menu. + + ![Start Menu screenshot](/img/docs/edit_system_environment_variables.jpg) + +4. In the System Properties menu, click the "Environment Variables..." button at the bottom of the "Advanced" tab. + + ![System Properties screenshot](/img/docs/system_properties.png) + +5. If you installed `FFmpeg` and `FFprobe` to a system-wide location such as `C:\Program Files\FFmpeg`, select and edit the `Path` variable under the "System variables" heading. Else, if you installed them to some place specific to your user account, edit the `Path` variable under the "User variables for \" heading. + + ![Environment Variables screenshot](/img/docs/environment_variables.png) + +6. In the window that pops up when you click "Edit...", click "New" and enter the path to the `bin` folder you extracted earlier. Then, click OK to add it. + + ![Edit environment variable screenshot](/img/docs/edit_path_environment_variable.png) + +7. Click "OK" to close the Environment Variables window, and then click "OK" again to close the + System Properties window. +8. Verify that it worked by opening a Command Prompt and executing the following commands: + + ```cmd + ffmpeg -version + ffprobe -version + ``` + + If you see version output from both of those commands, then you are all set! If not, double check the paths you entered and try restarting your computer. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/vision-mixers.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/vision-mixers.md new file mode 100644 index 00000000000..5c3c9b02345 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/vision-mixers.md @@ -0,0 +1,13 @@ +# Configuring Vision Mixers + +## ATEM – Blackmagic Design + +The [Playout Gateway](../installing-a-gateway/playout-gateway.md) supports communicating with the entire line up of Blackmagic Design's ATEM vision mixers. + +### Connecting Sofie + +Once your ATEM is properly configured on the network, you can add it as a device to the Sofie Core. To begin, navigate to the _Settings page_ and select the _Playout Gateway_ under _Devices_. Under the _Sub Devices_ section, you can add a new device with the _+_ button. Edit it the new device with the pencil \( edit \) icon add the host IP and port for your ATEM. Once complete, you should see your ATEM in the _Attached Sub Devices_ section with a _Good_ status indicator. + +### Additional Information + +Sofie does not support connecting to a vision mixer hardware panels. All interacts with the vision mixers must be handled within a Rundown. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-package-manager.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-package-manager.md new file mode 100644 index 00000000000..a7dafa5a6f6 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-package-manager.md @@ -0,0 +1,205 @@ +--- +sidebar_position: 70 +--- + +# Installing Package Manager + +### Prerequisites + +- [Installed and running Sofie Core](quick-install.md) +- [Initial Sofie Core Setup](initial-sofie-core-setup.md) +- [Installed and configured Demo Blueprints](https://github.com/SuperFlyTV/sofie-demo-blueprints) +- [Installed, configured, and running CasparCG Server](installing-connections-and-additional-hardware/casparcg-server-installation.md) (Optional) +- [`FFmpeg` and `FFprobe` available in `PATH`](installing-connections-and-additional-hardware/ffmpeg-installation.md) + +Package Manager is used by Sofie to copy, analyze, and process media files. It is what powers Sofie's ability to copy media files to playout devices, to know when a media file is ready for playout, and to display details about media files in the rundown view such as scene changes, black frames, freeze frames, and more. + +Although Package Manager can be used to copy any kind of file to/from a wide array of devices, we'll be focusing on a basic CasparCG Server Server setup for this guide. + +:::caution + +Sofie supports only one Package Manager running for a Studio. Attaching more at a time will result in weird behaviour due to them fighting over reporting the statuses of packages. +If you feel like you need multiple, then you likely want to run Package Manager in the distributed setup instead. + +::: + + +## Installation For Development (Quick Start) + +Package Manager is a suite of standalone applications, separate from _Sofie Core_. This guide assumes that Package Manager will be running on the same computer as _CasparCG Server_ and _Sofie Core_, as that is the fastest way to set up a demo. To get all parts of _Package Manager_ up and running quickly, execute these commands: + +```bash +git clone https://github.com/Sofie-Automation/sofie-package-manager.git +cd sofie-package-manager +yarn install +yarn build +yarn start:single-app +``` + +On first startup, Package Manager will exit with the following message: + +``` +Not setup yet, exiting process! +To setup, go into Core and add this device to a Studio +``` + +This first run is necessary to get the Package Manager device registered with _Sofie Core_. We'll restart Package Manager later on in the [Configuration](#configuration) instructions. + +## Installation In Production + +Only one Package Manager can be running for a Sofie Studio. If you reached this point thinking of deploying multiple, you will want to follow the distributed setup. + +### Simple Setup + +For setups where you only need to interact with CasparCG on one machine, we provide pre-built executables for Windows (x64) systems. These can be found on the [Releases](https://github.com/Sofie-Automation/sofie-package-manager/releases) GitHub repository page for Package Manager. For a minimal installation, you'll need the `package-manager-single-app.exe` and `worker.exe`. Put them in a folder of your choice. You can also place `ffmpeg.exe` and `ffprobe.exe` alongside them, if you don't want to make them available in `PATH`. + +```bash +package-manager-single-app.exe --coreHost= --corePort= --deviceId= --deviceToken= +``` + +Package Manager can be launched from [CasparCG Launcher](./installing-connections-and-additional-hardware/casparcg-server-installation.md#installing-the-casparcg-launcher) alongside Caspar-CG. This will make management and log collection easier on a production Video Server. + +You can see a list of available options by running `package-manager-single-app.exe --help`. + +In some cases, you will need to run the HTTP proxy server component elsewhere so that it can be accessed from your Sofie UI machines. +For this, you can run the `sofietv/package-manager-http-server` docker image, which exposes its service on port 8080 and expects `/data/http-server` to be persistent storage. +When configuring the http proxy server in Sofie, you may need to follow extra configuration steps for this to work as expected. + +### Distributed Setup + +For setups where you need to interact with multiple CasparCG machines, or want a more resilient/scalable setup, Package Manager can be partially deployed in Docker, with just the workers running on each CasparCG machine. + +An example `docker-compose` of the setup is as follows: + +``` +services: + # Fix Ownership of HTTP Server + # Because docker volumes are owned by root by default + # And our images follow best-practise and don't run as root + change-vol-ownerships: + image: node:22-alpine3.22 + user: 'root' + volumes: + - http-server-data:/data/http-server + entrypoint: ['sh', '-c', 'chown -R node:node /data/http-server'] + + http-server: + image: ghcr.io/sofie-automation/sofie-package-manager-http-server:v1.52.0 + environment: + HTTP_SERVER_BASE_PATH: '/data/http-server' + ports: + - '8080:8080' + volumes: + - http-server-data:/data/http-server + depends_on: + change-vol-ownerships: + condition: service_completed_successfully + + workforce: + image: ghcr.io/sofie-automation/sofie-package-manager-workforce:v1.52.0 + ports: + - '8070:8070' # this needs to be exposed so that the workers can connect back to it + # environment: + # - WORKFORCE_ALLOW_NO_APP_CONTAINERS=1 # Uncomment this if your workers are in docker, to disable the check for no appContainers + + # You can deploy workers in docker too, which requires some additional configuration of your containers. + # This does not support FILESHARE accessors, they must be explicitly mounted as volumes + # You will likely want to deploy more than 1 worker + # worker0: + # image: ghcr.io/sofie-automation/sofie-package-manager-worker:v1.52.0 + # command: + # - --logLevel=debug + # - --workforceURL=ws://workforce:8070 + # - --costMultiplier=0.5 + # - --resourceId=docker + # - --networkIds=networkDocker + # volumes: + # - ./media-source:/data/source:ro + + package-manager: + depends_on: + - http-server + - workforce + image: ghcr.io/sofie-automation/sofie-package-manager-package-manager:v1.52.0 + environment: + CORE_HOST: '172.18.0.1' # the address for connecting back to Sofie core from this image + CORE_PORT: '3000' + DEVICE_ID: 'my-package-manager-id' + DEVICE_TOKEN: 'some-secret' + WORKFORCE_URL: 'ws://workforce:8070' # referencing the workforce component above + PACKAGE_MANAGER_PORT: '8060' + PACKAGE_MANAGER_URL: 'ws://insert-service-ip-here:8060' # the workers connect back to this address, so it needs to be accessible from the workers + # CONCURRENCY: 10 # How many expectation states can be evaluated at the same time + ports: + - '8060:8060' + +networks: + default: +volumes: + http-server-data: +``` + +In addition to this, you will need to run the appContainer and workers on each windows machine that package-manager needs access to: + +``` +./appContainer-node.exe + --appContainerId=caspar01 // This is a unique id for this instance of the appContainer + --workforceURL=ws://workforce-service-ip:8070 + --resourceId=caspar01 // This should also be set in the 'resource id' field of the `casparcgLocalFolder1` accessor. This is how Package Manager can identify which machine is which. + --networkIds=pm-net // This is not necessary, but can be useful for more complex setups +``` + +You can get the windows executables from [Releases](https://github.com/Sofie-Automation/sofie-package-manager/releases) GitHub repository page for Package Manager. You'll need the `appContainer-node.exe` and `worker.exe`. Put them in a folder of your choice. You can also place `ffmpeg.exe` and `ffprobe.exe` alongside them, if you don't want to make them available in `PATH`. + +Note that each appContainer needs to use a different resourceId and will need its own package containers set to use the same resourceIds if they need to access the local disk. This is how package-manager knows which workers have access to which machines.w + +## Configuration + +1. Open the _Sofie Core_ Settings page ([http://localhost:3000/settings?admin=1](http://localhost:3000/settings?admin=1)), click on your Studio, and then Peripheral Devices. +1. Click the plus button (`+`) in the Parent Devices section and configure the created device to be for your Package Manager. +1. On the sidebar under the current Studio, select to the Package Manager section. +1. Click the plus button under the Package Containers heading, then click the edit icon (pencil) to the right of the newly-created package container. +1. Give this package container an ID of `casparcgContainer0` and a label of `CasparCG Package Container`. +1. Click on the dropdown under "Playout devices which use this package container" and select `casparcg0`. + - If you don't have a `casparcg0` device, add it to the Playout Gateway under the Devices heading, then restart the Playout Gateway. + - If you are using the distributed setup, you will likely want to repeat this step for each CasparCG machine. You will also want to set `Resource Id` to match the `resourceId` value provided in the appContainer command line. +1. Click the plus button under "Accessors", then click the edit icon to the right of the newly-created accessor. +1. Give this accessor an ID of `local`, a Label of `Local`, an Accessor Type of `LOCAL`, and a Folder path matching your CasparCG `media` folder. Then, ensure that only the "Allow Read access" boxes are checked. Finally, click the done button (checkmark icon) in the bottom right. +1. Click the plus button under the Package Containers heading, then click the edit icon (pencil) to the right of the newly-created package container. +1. Give this package container an ID of `httpProxy0` and a label of `Proxy for thumbnails & preview`. +1. Click the plus button under "Accessors", then click the edit icon to the right of the newly-created accessor. +1. Give this accessor an ID of `http0`, a Label of `HTTP`, an Accessor Type of `HTTP_PROXY`, and a Base URL of `http://localhost:8080/package`. Then, ensure that both the "Allow Read access" and "Allow Write access" boxes are checked. Finally, click the done button (checkmark icon) in the bottom right. +1. Scroll back to the top of the page and select `Proxy for thumbnails & preview` for both "Package Containers to use for previews" and "Package Containers to use for thumbnails". +1. Your settings should look like this once all the above steps have been completed: + ![Package Manager demo settings](/img/docs/Package_Manager_demo_settings.png) +1. If Package Manager `start:single-app` is running, restart it. If not, start it (see the above [Installation instructions](#installation-for-development-quick-start) for the relevant command line). + +### Separate HTTP proxy server + +In some setups, the URL of the HTTP proxy server is different when accessing the Sofie UI and Package Manager. +You can use the 'Network ID' concept in Package Manager to provide guidance on which to use when. + +By adding `--networkIds=pm-net` (a semi colon separated list) when launching the exes on the CasparCG machine, the application will know to prefer certain accessors with matching values. + +Then in the Sofie UI: + +1. Return to the Package Manager settings under the studio +1. Expand the `httpProxy0` container. +1. Edit the `http0` accessor to have a `Base URL` that is accessible from the casparcg machines. +1. Set the `Network ID` to `pm-net` (matching what was passed in the command line) +1. Click the plus button under "Accessors", then click the edit icon to the right of the newly-created accessor. +1. Give this accessor an ID of `publicHttp0`, a Label of `Public HTTP Proxy Accessor`, an Accessor Type of `HTTP_PROXY`, and a Base URL that is accessible to your Sofie client network. Then, ensure that only the "Allow read access" box is checked. Finally, click the done button (checkmark icon) in the bottom right. + +## Usage + +In this basic configuration, Package Manager won't be copying any packages into your CasparCG Server media folder. Instead, it will simply check that the files in the rundown are present in your CasparCG Server media folder, and you'll have to manually place those files in the correct directory. However, thumbnail and preview generation will still function, as will status reporting. + +If you're using the demo rundown provided by the [Rundown Editor](rundown-editor.md), you should already see work statuses on the Package Status page ([Status > Packages](http://localhost:3000/status/expected-packages)). + +![Example Package Manager status display](/img/docs/Package_Manager_status_example.jpg) + +If all is good, head to the [Rundowns page](http://localhost:3000/rundowns) and open the demo rundown. + +### Further Reading + +- [Package Manager](https://github.com/Sofie-Automation/sofie-package-manager) on GitHub. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-sofie-server-core.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-sofie-server-core.md new file mode 100644 index 00000000000..7ee0c7ed29e --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-sofie-server-core.md @@ -0,0 +1,23 @@ +--- +sidebar_position: 35 +--- + +# Installing Sofie Core + +Our **[Quick install guide](quick-install.md)** provides a quick and easy way of deploying the various pieces of software needed for a production-quality deployment of Sofie using `docker compose`. This section provides some more insights for users choosing to install Sofie via alternative methods. + +The preferred way to install Sofie Core for production is using Docker via our officially published images inside Docker Hub: [https://hub.docker.com/u/sofietv](https://hub.docker.com/u/sofietv). Note that some of the images mentioned in this documentation are community-maintained and as such are not published by the `sofietv` Docker Hub organization. + +More advanced ways of deploying Sofie are possible and actively used by Sofie users, including [Podman](https://podman.io/), [Kubernetes](https://kubernetes.io/), [Salt](https://saltproject.io/), [Ansible](https://github.com/ansible/ansible) among others. Any deployment system that uses [OCI App Containers](https://opencontainers.org/) should be suitable. + +Sofie and it's Blueprint system is specifically built around the concept of Infrastructure-as-Code and Configuration-as-Code and we strongly advise using that methodology in production, rather than the manual route of using the User Interface for configuration. + +:::tip +While Sofie is using cloud-native technologies, it's workloads do not follow typical patterns seen in cloud software. When optimizing Sofie performance for production, make sure not to optimize for the amount of operations per second, but rather for fastest response time on a single request. +::: + +## Basic structure + +On a foundational level, Sofie Core is a [Meteor](https://docs.meteor.com/), [Node.js](https://nodejs.org/) web application that uses [MongoDB](https://www.mongodb.com) for its data persistence. + +Both the Sofie Gateways and User Agents using the Web User Interface connect to it via DDP, a WebSocket-based, Meteor-specific protocol. This protocol is used both for RPC and shared state synchronization. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/intro.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/intro.md new file mode 100644 index 00000000000..bcf3dd99481 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/intro.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 10 +--- +# Getting Started + +_Sofie_ can be installed in many different ways, depending on which platforms, needs, and features you desire. The _Sofie_ system consists of several applications that work together to provide complete broadcast automation system. Each of these components' installation will be covered in this guide. Additional information about the products or services mentioned alongside the Sofie Installation can be found on the [Further Reading](../further-reading.md). + +:::tip Quick Install +If you're looking to quickly evaluate Sofie to see if it's a good match for your needs, you can jump into our **[Quick Install guide](./quick-install.md)**. +::: + +There are four minimum required components to get a Sofie system up and running. First you need the [_Sofie Core_](quick-install.md), which is the brains of the operation. Then a set of [_Blueprints_](installing-blueprints.md) to handle and interpret incoming and outgoing data. Next, an [_Ingest Gateway_](installing-a-gateway/rundown-or-newsroom-system-connection/intro.md) to fetch the data for the Blueprints. Then finally, a [_Playout Gateway_](installing-a-gateway/playout-gateway.md) to send commands and change the state of your playout devices while you run your show. + +## Sofie Core Overview + +The _Sofie Core_ is the primary application for managing the broadcast but, it doesn't play anything out on it's own. You need to use Gateways to establish the connection from the _Sofie Core_ to other pieces of hardware or remote software. + +### Gateways + +Gateways are separate applications that bridge the gap between the _Sofie Core_ and other pieces of hardware or software services. At a minimum, you will need a _Playout Gateway_ so your timeline can interact with your playout system of choice. To install the _Playout Gateway_, visit the [Installing a Gateway](installing-a-gateway/intro.md) section of this guide and for a more in-depth look, please see [Gateways](../concepts-and-architecture.md#gateways). + +### Blueprints + +Blueprints can be described as the logic that determines how a studio and show should interact with one another. They interpret the data coming in from the rundowns and transform them into a rich set of playable elements \(_Segments_, _Parts_, _AdLibs,_ etc.\). The _Sofie Core_ has three main blueprint types, _System Blueprints_, _Studio Blueprints_, and _Showstyle Blueprints_. Installing _Sofie_ does not require you understand what these blueprints do, just that they are required for the _Sofie Core_ to work. If you would like to gain a deeper understanding of how _Blueprints_ work, please visit the [Blueprints](../../for-developers/for-blueprint-developers/intro.md) section. + diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/quick-install.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/quick-install.md new file mode 100644 index 00000000000..d9fc1331d15 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/quick-install.md @@ -0,0 +1,172 @@ +--- +sidebar_position: 20 +--- + +# Quick install + +## Installing for testing \(or production\) + +### **Prerequisites** + +* **Linux**: Install [Docker](https://docs.docker.com/install/linux/docker-ce/ubuntu/) and [docker-compose](https://www.digitalocean.com/community/tutorials/how-to-install-docker-compose-on-ubuntu-18-04). +* **Windows**: Install [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and use an *Ubuntu* terminal to install Docker and docker-compose. + +### Installation + +This docker-compose file automates the basic setup of the [Sofie-Core application](../../for-developers/libraries.md#main-application), the backend database and different Gateway options. + +```yaml +# This is NOT recommended to be used for a production deployment. +# It aims to quickly get an evaluation version of Sofie running and serve as a basis for how to set up a production deployment. +services: + db: + hostname: mongo + image: mongo:6.0 + restart: always + entrypoint: ['/usr/bin/mongod', '--replSet', 'rs0', '--bind_ip_all'] + # the healthcheck avoids the need to initiate the replica set + healthcheck: + test: test $$(mongosh --quiet --eval "try {rs.initiate()} catch(e) {rs.status().ok}") -eq 1 + interval: 10s + start_period: 30s + ports: + - '27017:27017' + volumes: + - db-data:/data/db + networks: + - sofie + + # Fix Ownership Snapshots mount + # Because docker volumes are owned by root by default + # And our images follow best-practise and don't run as root + change-vol-ownerships: + image: node:22-alpine + user: 'root' + volumes: + - sofie-store:/mnt/sofie-store + entrypoint: ['sh', '-c', 'chown -R node:node /mnt/sofie-store'] + + core: + hostname: core + image: sofietv/tv-automation-server-core:release52 + restart: always + ports: + - '3000:3000' # Same port as meteor uses by default + environment: + PORT: '3000' + MONGO_URL: 'mongodb://db:27017/meteor' + MONGO_OPLOG_URL: 'mongodb://db:27017/local' + ROOT_URL: 'http://localhost:3000' + SOFIE_STORE_PATH: '/mnt/sofie-store' + networks: + - sofie + volumes: + - sofie-store:/mnt/sofie-store + depends_on: + change-vol-ownerships: + condition: service_completed_successfully + db: + condition: service_healthy + + playout-gateway: + image: sofietv/tv-automation-playout-gateway:release52 + restart: always + environment: + DEVICE_ID: playoutGateway0 + CORE_HOST: core + CORE_PORT: '3000' + networks: + - sofie + - lan_access + depends_on: + - core + + # Choose one of the following images, depending on which type of ingest gateway is wanted. + + # spreadsheet-gateway: + # image: superflytv/sofie-spreadsheet-gateway:latest + # restart: always + # environment: + # DEVICE_ID: spreadsheetGateway0 + # CORE_HOST: core + # CORE_PORT: '3000' + # networks: + # - sofie + # depends_on: + # - core + + # mos-gateway: + # image: sofietv/tv-automation-mos-gateway:release52 + # restart: always + # ports: + # - "10540:10540" # MOS Lower port + # - "10541:10541" # MOS Upper port + # # - "10542:10542" # MOS query port - not used + # environment: + # DEVICE_ID: mosGateway0 + # CORE_HOST: core + # CORE_PORT: '3000' + # networks: + # - sofie + # depends_on: + # - core + + # inews-gateway: + # image: tv2media/inews-ftp-gateway:1.37.0-in-testing.20 + # restart: always + # command: yarn start -host core -port 3000 -id inewsGateway0 + # networks: + # - sofie + # depends_on: + # - core + + # rundown-editor: + # image: ghcr.io/superflytv/sofie-automation-rundown-editor:v2.2.4 + # restart: always + # ports: + # - '3010:3010' + # environment: + # PORT: '3010' + # networks: + # - sofie + # depends_on: + # - core + +networks: + sofie: + lan_access: + driver: bridge + +volumes: + db-data: + sofie-store: +``` + +Create a `Sofie` folder, copy the above content, and save it as `docker-compose.yaml` within the `Sofie` folder. + +Visit [Rundowns & Newsroom Systems](installing-a-gateway/rundown-or-newsroom-system-connection/intro.md) to see which _Ingest Gateway_ can be used in your specific production environment. If you don't have an NRCS that you would like to integrate with, you can use the [Rundown Editor](rundown-editor) as a simple Rundown creation utility. Navigate to the _ingest-gateway_ section of `docker-compose.yaml` and select which type of _ingest-gateway_ you'd like installed by uncommenting it. Save your changes. + +Open a terminal, execute `cd Sofie` and `sudo docker-compose up` \(or just `docker-compose up` on Windows\). This will download MongoDB and Sofie components' container images and start them up. The installation will be done when your terminal window will be filled with messages coming from `playout-gateway_1` and `core_1`. + +Once the installation is done, Sofie should be running on [http://localhost:3000](http://localhost:3000). Next, you need to make sure that the Playout Gateway and Ingest Gateway are connected to the default Studio that has been automatically created. Open the Sofie User Interface with [Configuration Access level](../features/access-levels#browser-based) by opening [http://localhost:3000/?admin=1](http://localhost:3000/?admin=1) in your Web Browser and navigate to _Settings_ 🡒 _Studios_ 🡒 _Default Studio_ 🡒 _Peripheral Devices_. In the _Parent Devices_ section, create a new Device using the **+** button, rename the device to _Playout Gateway_ and select _Playout gateway_ from the _Peripheral Device_ drop-down menu. Repeat this process for your _Ingest Gateway_ or _Sofie Rundown Editor_. + +:::note +Starting with Sofie version 1.52.0, `sofietv` container images will run as UID 1000. +::: + +### Tips for running in production + +There are some things not covered in this guide needed to run _Sofie_ in a production environment: + +- Logging: Collect, store and track error messages. [Kibana](https://www.elastic.co/kibana) and [logstash](https://www.elastic.co/logstash) is one way to do it. +- NGINX: It is customary to put a load-balancer in front of _Sofie Core_. +- Memory and CPU usage monitoring. + +## Installing for Development + +Installation instructions for installing Sofie-Core or the various gateways are available in the README file in their respective GitHub repos. + +Common prerequisites are [Node.js](https://nodejs.org/) and [Yarn](https://yarnpkg.com/). +Links to the repos are listed at [Applications & Libraries](../../for-developers/libraries.md). + +[_Sofie Core_ GitHub Page for Developers](https://github.com/Sofie-Automation/sofie-core) diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/rundown-editor.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/rundown-editor.md new file mode 100644 index 00000000000..686f7750db1 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/rundown-editor.md @@ -0,0 +1,18 @@ +--- +sidebar_position: 80 +--- + +# Sofie Rundown Editor + +Sofie Rundown Editor is a tool for creating and editing rundowns in a _demo_ environment of Sofie, without the use of an iNews, Spreadsheet or MOS Gateway + +### Connecting Sofie Rundown Editor + +After starting the Rundown Editor via the `docker-compose.yaml` specified in [Quick Start](./installing-sofie-server-core), this app requires a special bit of configuration to connect to Sofie. You need to open the Rundown Editor web interface at [http://localhost:3010/](http://localhost:3010/), go to _Settings_ and set _Core Connection Settings_ to: + +| Property | Value | +| -------- | ------ | +| Address | `core` | +| Port | `3000` | + +The header should change to _Core Status: Connected to core:3000_. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/intro.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/intro.md new file mode 100644 index 00000000000..e2e7ed4787b --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/intro.md @@ -0,0 +1,41 @@ +--- +sidebar_label: Introduction +sidebar_position: 0 +--- + +# Sofie User Guide + +## Key Features + +### Web-based GUI + +![Producer's / Director's View](/img/docs/Sofie_GUI_example.jpg) + +![Warnings and notifications are displayed to the user in the GUI](/img/docs/warnings-and-notifications.png) + +![The Host view, displaying time information and countdowns](/img/docs/host-view.png) + +![The prompter view](/img/docs/prompter-view.png) + +:::info +Tip: The different web views \(such as the host view and the prompter\) can easily be transmitted over an SDI signal using the HTML producer in [CasparCG](installation/installing-connections-and-additional-hardware/casparcg-server-installation.md). +::: + +### Modular Device Control + +Sofie controls playout devices \(such as vision and audio mixers, graphics and video playback\) via the Playout Gateway, using the [Timeline](concepts-and-architecture.md#timeline). +The Playout Gateway controls the devices and keeps track of their state and statuses, and lets the user know via the GUI if something's wrong that can affect the show. + +### _State-based Playout_ + +Sofie is using a state-based architecture to control playout. This means that each element in the show can be programmed independently - there's no need to take into account what has happened previously in the show; Sofie will make sure that the video is loaded and that the audio fader is tuned to the correct position, no matter what was played out previously. +This allows the producer to skip ahead or move backwards in a show, without the fear of things going wrong on air. + +### Modular Data Ingest + +Sofie features a modular ingest data-flow, allowing multiple types of input data to base rundowns on. Currently there is support for [MOS-based](http://mosprotocol.com) systems such as ENPS and iNEWS, as well as [Google Spreadsheets](installation/installing-a-gateway/rundown-or-newsroom-system-connection/google-spreadsheet.md), and more is in development. + +### Blueprints + +The [Blueprints](concepts-and-architecture.md#blueprints) are plugins to _Sofie_, which allows for customization and tailor-made show designs. +The blueprints are made different depending on how the input data \(rundowns\) look like, how the show-design look like, and what devices to control. diff --git a/packages/documentation/versioned_docs/version-26.03.0/user-guide/supported-devices.md b/packages/documentation/versioned_docs/version-26.03.0/user-guide/supported-devices.md new file mode 100644 index 00000000000..c6d28c131d2 --- /dev/null +++ b/packages/documentation/versioned_docs/version-26.03.0/user-guide/supported-devices.md @@ -0,0 +1,118 @@ +--- +sidebar_position: 1.5 +--- + +# Supported Playout Devices + +All playout devices are essentially driven through the _timeline_, which passes through _Sofie Core_ into the Playout Gateway where it is processed by the timeline-state-resolver. This page details which devices and what parts of the devices can be controlled through the timeline-state-resolver library. In general a blueprints developer can use the [timeline-state-resolver-types package](https://www.npmjs.com/package/timeline-state-resolver-types) to see the interfaces for the timeline objects used to control the devices. + +## Blackmagic Design's ATEM Vision Mixers + +We support almost all features of these devices except fairlight audio, camera controls and streaming capabilities. A non-inclusive list: + +- Control of camera inputs +- Transitions +- Full control of keyers +- Full control of DVE's +- Control of media pools +- Control of auxiliaries + +## CasparCG Server + + +- Video playback +- Graphics playback +- Recording / streaming +- Mixer parameters +- Transitions + +## HTTP Protocol + +- GET/POST/PUT/DELETE methods +- Pre-shared "Bearer" token authorization +- OAuth 2.0 Client Credentials flow +- Interval based watcher for status monitoring + +## Blackmagic Design HyperDeck + +- Recording + +## Lawo Powercore & MC2 Series + +- Control over faders + - Using the ramp function on the powercore +- Control of parameters in the ember tree + +## OSC protocol + +- Sending of integers, floats, strings, blobs +- Tweening \(transitioning between\) values + +Can be configured in TCP or UDP mode. + +## Panasonic PTZ Cameras + +- Recalling presets +- Setting zoom, zoom speed and recall speed + +## Pharos Lighting Control + +- Recalling scenes +- Recalling timelines + +## Grass Valley SQ Media Servers + +- Control of playback +- Looping +- Cloning + +_Note: some features are controlled through the Package Manager_ + +## Shotoku Camera Robotics + +- Cutting to shots +- Fading to shots + +## Singular Live + +- Control nodes + +## Sisyfos + +- On-air controls +- Fader levels +- Labels +- Hide / show channels + +## TCP Protocol + +- Sending messages + +## VizRT Viz MSE + +- Pilot elements +- Continue commands +- Loading all elements +- Clearing all elements + +## vMix + +- Full M/E control +- Audio control +- Streaming / recording control +- Fade to black +- Overlays +- Transforms +- Transitions + +## OBS + +_Through OBS 28+ WebSocket API (a.k.a v5 Protocol)_ + +- Current / Preview Scene +- Current Transition +- Recording +- Streaming +- Scene Item visibility +- Source Settings (FFmpeg source) +- Source Mute diff --git a/packages/documentation/versioned_sidebars/version-1.52.0-sidebars.json b/packages/documentation/versioned_sidebars/version-1.52.0-sidebars.json new file mode 100644 index 00000000000..d7c19231b42 --- /dev/null +++ b/packages/documentation/versioned_sidebars/version-1.52.0-sidebars.json @@ -0,0 +1,14 @@ +{ + "userGuide": [ + { + "type": "autogenerated", + "dirName": "user-guide" + } + ], + "forDevelopers": [ + { + "type": "autogenerated", + "dirName": "for-developers" + } + ] +} diff --git a/packages/documentation/versioned_sidebars/version-26.03.0-sidebars.json b/packages/documentation/versioned_sidebars/version-26.03.0-sidebars.json new file mode 100644 index 00000000000..d7c19231b42 --- /dev/null +++ b/packages/documentation/versioned_sidebars/version-26.03.0-sidebars.json @@ -0,0 +1,14 @@ +{ + "userGuide": [ + { + "type": "autogenerated", + "dirName": "user-guide" + } + ], + "forDevelopers": [ + { + "type": "autogenerated", + "dirName": "for-developers" + } + ] +} diff --git a/packages/documentation/versions.json b/packages/documentation/versions.json index 6f580bcd828..9e32aebfda7 100644 --- a/packages/documentation/versions.json +++ b/packages/documentation/versions.json @@ -1,4 +1,6 @@ [ + "26.03.0", + "1.52.0", "1.51.0", "1.50.0", "1.49.0", diff --git a/packages/webui/eslint.config.mjs b/packages/eslint.config.mjs similarity index 56% rename from packages/webui/eslint.config.mjs rename to packages/eslint.config.mjs index 2017e806bb7..83032f608d9 100644 --- a/packages/webui/eslint.config.mjs +++ b/packages/eslint.config.mjs @@ -1,8 +1,42 @@ import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' +import pluginYaml from 'eslint-plugin-yml' import pluginReact from 'eslint-plugin-react' import globals from 'globals' -const tmpRules = { +const extendedRules = await generateEslintConfig({ + ignores: [ + 'openapi/client', + 'openapi/server', + 'live-status-gateway/server', + 'live-status-gateway-api/server', + 'documentation', // Temporary? + 'webui/public', + 'webui/dist', + 'webui/src/fonts', + 'webui/src/meteor', + 'webui/vite.config.mts', // This errors because of tsconfig structure + ], +}) +extendedRules.push( + ...pluginYaml.configs['flat/recommended'], + { + files: ['**/*.yaml'], + + rules: { + 'yml/quotes': ['error', { prefer: 'single' }], + 'yml/spaced-comment': ['error'], + 'spaced-comment': ['off'], + }, + }, + { + files: ['openapi/**/*'], + rules: { + 'n/no-missing-import': 'off', // erroring on every single import + }, + } +) + +const tmpWebuiRules = { // Temporary rules to be removed over time '@typescript-eslint/ban-types': 'off', '@typescript-eslint/no-namespace': 'off', @@ -11,13 +45,9 @@ const tmpRules = { '@typescript-eslint/unbound-method': 'off', '@typescript-eslint/no-misused-promises': 'off', '@typescript-eslint/no-unnecessary-type-assertion': 'off', -} -const extendedRules = await generateEslintConfig({ - tsconfigName: 'tsconfig.eslint.json', - ignores: ['public', 'dist', 'src/fonts', 'src/meteor', 'vite.config.mts'], - disableNodeRules: true, -}) + 'n/file-extension-in-import': 'off', // many issues currently +} extendedRules.push( { settings: { @@ -29,7 +59,7 @@ extendedRules.push( pluginReact.configs.flat.recommended, pluginReact.configs.flat['jsx-runtime'], { - files: ['src/**/*'], + files: ['webui/src/**/*'], languageOptions: { globals: { ...globals.browser, @@ -39,24 +69,21 @@ extendedRules.push( rules: {}, }, { - files: ['src/**/*'], + // For some reason, the tsconfig has to be specified here explicitly + files: ['webui/src/**/*.ts', 'webui/src/**/*.tsx'], + languageOptions: { + parserOptions: { + project: './webui/tsconfig.eslint.json', + }, + }, + rules: {}, + }, + { + files: ['webui/src/**/*'], rules: { // custom 'no-inner-declarations': 'off', // some functions are unexported and placed inside a namespace next to related ones - // 'n/no-missing-import': [ - // 'error', - // { - // allowModules: ['meteor', 'mongodb'], - // tryExtensions: ['.js', '.json', '.node', '.ts', '.tsx', '.d.ts'], - // }, - // ], - // 'n/no-extraneous-import': [ - // 'error', - // { - // allowModules: ['meteor', 'mongodb'], - // }, - // ], - + 'n/no-unsupported-features/node-builtins': 'off', // webui code is not run in node.js 'n/no-extraneous-import': 'off', // because there are a lot of them as dev-dependencies 'n/no-missing-import': 'off', // erroring on every single import 'react/prop-types': 'off', // we don't use this @@ -64,7 +91,7 @@ extendedRules.push( '@typescript-eslint/no-empty-object-type': 'off', // many prop/state types are {} '@typescript-eslint/promise-function-async': 'off', // event handlers can't be async - ...tmpRules, + ...tmpWebuiRules, }, } ) diff --git a/packages/job-worker/eslint.config.mjs b/packages/job-worker/eslint.config.mjs deleted file mode 100644 index b9e5a88fd88..00000000000 --- a/packages/job-worker/eslint.config.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' - -export default generateEslintConfig({}) diff --git a/packages/job-worker/jest.config.js b/packages/job-worker/jest.config.js index c534a7fb397..e7da1d6c66d 100644 --- a/packages/job-worker/jest.config.js +++ b/packages/job-worker/jest.config.js @@ -16,6 +16,7 @@ module.exports = { ignoreCodes: [ 6133, // Declared but not used 6192, // All imports are unused + 151002, // hybrid module kind (Node16/18/Next) ], }, }, diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 9241feb122c..5dd185767e6 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -15,8 +15,7 @@ }, "homepage": "https://github.com/Sofie-Automation/sofie-core/blob/main/packages/job-worker#readme", "scripts": { - "lint:raw": "run -T eslint", - "lint": "run lint:raw .", + "lint": "run -T lint job-worker", "unit": "run -T jest", "test": "run lint && run unit", "watch": "run -T jest --watch --coverage=false", @@ -36,35 +35,28 @@ "/LICENSE" ], "dependencies": { - "@slack/webhook": "^7.0.4", - "@sofie-automation/blueprints-integration": "1.53.0-in-development", - "@sofie-automation/corelib": "1.53.0-in-development", - "@sofie-automation/shared-lib": "1.53.0-in-development", - "amqplib": "^0.10.5", + "@slack/webhook": "^7.0.6", + "@sofie-automation/blueprints-integration": "26.3.0-2", + "@sofie-automation/corelib": "26.3.0-2", + "@sofie-automation/shared-lib": "26.3.0-2", + "amqplib": "0.10.5", + "chrono-node": "^2.9.0", "deepmerge": "^4.3.1", - "elastic-apm-node": "^4.11.0", - "mongodb": "^6.12.0", + "elastic-apm-node": "^4.15.0", + "mongodb": "^6.21.0", "p-lazy": "^3.1.0", "p-timeout": "^4.1.0", "superfly-timeline": "9.2.0", - "threadedclass": "^1.2.2", + "threadedclass": "^1.3.0", "tslib": "^2.8.1", - "type-fest": "^4.33.0", + "type-fest": "^4.41.0", "underscore": "^1.13.7" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "lint-staged": { - "*.{js,css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx}": [ - "yarn lint:raw" - ] - }, "packageManager": "yarn@4.12.0", "devDependencies": { - "jest": "^29.7.0", - "jest-mock-extended": "^3.0.7", + "jest": "^30.2.0", + "jest-mock-extended": "^4.0.0", "typescript": "~5.7.3" } } diff --git a/packages/job-worker/scripts/babel-jest.mjs b/packages/job-worker/scripts/babel-jest.mjs index 4d782617379..c7902c0a379 100644 --- a/packages/job-worker/scripts/babel-jest.mjs +++ b/packages/job-worker/scripts/babel-jest.mjs @@ -1,7 +1,7 @@ // eslint-disable-next-line n/no-extraneous-import import babelJest from 'babel-jest' -export default babelJest.default.createTransformer({ +export default babelJest.createTransformer({ plugins: ['@babel/plugin-transform-modules-commonjs'], babelrc: false, configFile: false, diff --git a/packages/job-worker/src/__mocks__/context.ts b/packages/job-worker/src/__mocks__/context.ts index 85ba9eaa9e1..a7c40e746bd 100644 --- a/packages/job-worker/src/__mocks__/context.ts +++ b/packages/job-worker/src/__mocks__/context.ts @@ -12,6 +12,7 @@ import { IBlueprintSegment, ISegmentUserContext, IShowStyleContext, + IStudioSettings, IngestSegment, PlaylistTimingType, ShowStyleBlueprintManifest, @@ -60,7 +61,10 @@ import { processShowStyleBase, processShowStyleVariant } from '../jobs/showStyle import { defaultStudio } from './defaultCollectionObjects.js' import { convertStudioToJobStudio } from '../jobs/studio.js' -export function setupDefaultJobEnvironment(studioId?: StudioId): MockJobContext { +export function setupDefaultJobEnvironment( + studioId?: StudioId, + studioSettings?: Partial +): MockJobContext { const { mockCollections, jobCollections } = getMockCollections() // We don't bother 'saving' this to the db, as usually nothing will load it @@ -71,6 +75,16 @@ export function setupDefaultJobEnvironment(studioId?: StudioId): MockJobContext blueprintId: protectString('studioBlueprint0'), } + if (studioSettings) { + studio.settingsWithOverrides = { + ...studio.settingsWithOverrides, + defaults: { + ...studio.settingsWithOverrides.defaults, + ...studioSettings, + }, + } + } + return new MockJobContext(jobCollections, mockCollections, studio) } diff --git a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts index 88869a4da84..c8813447219 100644 --- a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts @@ -44,6 +44,12 @@ export function defaultRundownPlaylist(_id: RundownPlaylistId, studioId: StudioI type: PlaylistTimingType.None, }, rundownIdsInOrder: [], + + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], } } export function defaultRundown( @@ -111,8 +117,10 @@ export function defaultStudio(_id: StudioId): DBStudio { routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], + packageContainerSettingsWithOverrides: wrapDefaultObject({ + previewContainerIds: [], + thumbnailContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), diff --git a/packages/job-worker/src/blueprints/__tests__/context-OnSetAsNextContext.test.ts b/packages/job-worker/src/blueprints/__tests__/context-OnSetAsNextContext.test.ts index 5cdf53ed788..7bb1aaf9861 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-OnSetAsNextContext.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-OnSetAsNextContext.test.ts @@ -9,13 +9,22 @@ import { OnSetAsNextContext } from '../context/index.js' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' describe('Test blueprint api context', () => { async function getTestee(setManually = false, rehearsal?: boolean) { const mockActionService = mock() const mockPlayoutModel = mock() Object.defineProperty(mockPlayoutModel, 'playlist', { - get: () => ({ rehearsal }), + get: () => + ({ + rehearsal, + tTimers: [ + { index: 1, label: 'Timer 1', mode: null, state: null }, + { index: 2, label: 'Timer 2', mode: null, state: null }, + { index: 3, label: 'Timer 3', mode: null, state: null }, + ], + }) satisfies Partial, }) const context = new OnSetAsNextContext( { diff --git a/packages/job-worker/src/blueprints/__tests__/context-OnTakeContext.test.ts b/packages/job-worker/src/blueprints/__tests__/context-OnTakeContext.test.ts index 06319381fdb..8ea794c883d 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-OnTakeContext.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-OnTakeContext.test.ts @@ -9,12 +9,21 @@ import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { PlayoutModelImpl } from '../../playout/model/implementation/PlayoutModelImpl.js' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' describe('Test blueprint api context', () => { async function getTestee(rehearsal?: boolean) { const mockPlayoutModel = mock() Object.defineProperty(mockPlayoutModel, 'playlist', { - get: () => ({ rehearsal }), + get: () => + ({ + rehearsal, + tTimers: [ + { index: 1, label: 'Timer 1', mode: null, state: null }, + { index: 2, label: 'Timer 2', mode: null, state: null }, + { index: 3, label: 'Timer 3', mode: null, state: null }, + ], + }) satisfies Partial, }) const mockActionService = mock() const context = new OnTakeContext( diff --git a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts index 1dcd4e99a10..b61faf8c176 100644 --- a/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts +++ b/packages/job-worker/src/blueprints/__tests__/context-adlibActions.test.ts @@ -7,13 +7,22 @@ import { JobContext, ProcessedShowStyleCompound } from '../../jobs/index.js' import { mock } from 'jest-mock-extended' import { PartAndPieceInstanceActionService } from '../context/services/PartAndPieceInstanceActionService.js' import { ProcessedShowStyleConfig } from '../config.js' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' describe('Test blueprint api context', () => { async function getTestee(rehearsal?: boolean) { const mockActionService = mock() const mockPlayoutModel = mock() Object.defineProperty(mockPlayoutModel, 'playlist', { - get: () => ({ rehearsal }), + get: () => + ({ + rehearsal, + tTimers: [ + { index: 1, label: 'Timer 1', mode: null, state: null }, + { index: 2, label: 'Timer 2', mode: null, state: null }, + { index: 3, label: 'Timer 3', mode: null, state: null }, + ], + }) satisfies Partial, }) const context = new ActionExecutionContext( { diff --git a/packages/job-worker/src/blueprints/__tests__/lib.ts b/packages/job-worker/src/blueprints/__tests__/lib.ts index 2b0b5941e27..2b1104647e0 100644 --- a/packages/job-worker/src/blueprints/__tests__/lib.ts +++ b/packages/job-worker/src/blueprints/__tests__/lib.ts @@ -45,6 +45,8 @@ export function generateFakeBlueprint( system: undefined, }, + hasFixUpFunction: false, + blueprintVersion: '', integrationVersion: '', TSRVersion: '', diff --git a/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts b/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts index 92476b94be6..1d168e84f88 100644 --- a/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts +++ b/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts @@ -9,7 +9,7 @@ import { IBlueprintPieceDB, IBlueprintPieceInstance, IBlueprintResolvedPieceInstance, - IBlueprintSegment, + IBlueprintSegmentDB, IEventContext, IOnSetAsNextContext, } from '@sofie-automation/blueprints-integration' @@ -22,17 +22,21 @@ import { WatchedPackagesHelper } from './watchedPackages.js' import { PlayoutModel } from '../../playout/model/PlayoutModel.js' import { ReadonlyDeep } from 'type-fest' import { getCurrentTime } from '../../lib/index.js' -import { protectString } from '@sofie-automation/corelib/dist/protectedString' import { BlueprintQuickLookInfo } from '@sofie-automation/blueprints-integration/dist/context/quickLoopInfo' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { selectNewPartWithOffsets } from '../../playout/moveNextPart.js' import { getOrderedPartsAfterPlayhead } from '../../playout/lookahead/util.js' -import { convertPartToBlueprints } from './lib.js' +import { convertPartToBlueprints, emitIngestOperation } from './lib.js' +import { TTimersService } from './services/TTimersService.js' +import type { IPlaylistTTimer } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' +import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' export class OnSetAsNextContext extends ShowStyleUserContext implements IOnSetAsNextContext, IEventContext, IPartAndPieceInstanceActionContext { + readonly #tTimersService: TTimersService + public pendingMoveNextPart: { selectedPart: ReadonlyDeep | null } | undefined = undefined constructor( @@ -45,6 +49,7 @@ export class OnSetAsNextContext public readonly manuallySelected: boolean ) { super(contextInfo, context, showStyle, watchedPackages) + this.#tTimersService = TTimersService.withPlayoutModel(playoutModel, context) } public get quickLoopInfo(): BlueprintQuickLookInfo | null { @@ -79,7 +84,7 @@ export class OnSetAsNextContext return this.partAndPieceInstanceService.getResolvedPieceInstances(part) } - async getSegment(segment: 'current' | 'next'): Promise { + async getSegment(segment: 'current' | 'next'): Promise { return this.partAndPieceInstanceService.getSegment(segment) } @@ -120,9 +125,6 @@ export class OnSetAsNextContext pieceInstanceId: string, piece: Partial> ): Promise> { - if (protectString(pieceInstanceId) === this.playoutModel.playlist.currentPartInfo?.partInstanceId) { - throw new Error('Cannot update a Piece Instance from the current Part Instance') - } return this.partAndPieceInstanceService.updatePieceInstance(pieceInstanceId, piece) } @@ -160,7 +162,18 @@ export class OnSetAsNextContext return !!this.pendingMoveNextPart.selectedPart } + async emitIngestOperation(operation: unknown): Promise { + await emitIngestOperation(this.jobContext, this.playoutModel, operation) + } + getCurrentTime(): number { return getCurrentTime() } + + getTimer(index: RundownTTimerIndex): IPlaylistTTimer { + return this.#tTimersService.getTimer(index) + } + clearAllTimers(): void { + this.#tTimersService.clearAllTimers() + } } diff --git a/packages/job-worker/src/blueprints/context/OnTakeContext.ts b/packages/job-worker/src/blueprints/context/OnTakeContext.ts index ddca6bdcfc8..e028b31f1d8 100644 --- a/packages/job-worker/src/blueprints/context/OnTakeContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTakeContext.ts @@ -12,7 +12,7 @@ import { TSR, IBlueprintPlayoutDevice, IOnTakeContext, - IBlueprintSegment, + IBlueprintSegmentDB, } from '@sofie-automation/blueprints-integration' import { PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' @@ -30,9 +30,14 @@ import { } from './services/PartAndPieceInstanceActionService.js' import { BlueprintQuickLookInfo } from '@sofie-automation/blueprints-integration/dist/context/quickLoopInfo' import { getOrderedPartsAfterPlayhead } from '../../playout/lookahead/util.js' -import { convertPartToBlueprints } from './lib.js' +import { convertPartToBlueprints, emitIngestOperation } from './lib.js' +import type { IPlaylistTTimer } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' +import { TTimersService } from './services/TTimersService.js' +import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContext, IEventContext { + readonly #tTimersService: TTimersService + public isTakeAborted: boolean public partToQueueAfterTake: QueueablePartAndPieces | undefined @@ -61,6 +66,7 @@ export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContex ) { super(contextInfo, _context, showStyle, watchedPackages) this.isTakeAborted = false + this.#tTimersService = TTimersService.withPlayoutModel(_playoutModel, _context) } async getUpcomingParts(limit: number = 5): Promise> { @@ -80,7 +86,7 @@ export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContex async getResolvedPieceInstances(part: 'current' | 'next'): Promise { return this.partAndPieceInstanceService.getResolvedPieceInstances(part) } - async getSegment(segment: 'current' | 'next'): Promise { + async getSegment(segment: 'current' | 'next'): Promise { return this.partAndPieceInstanceService.getSegment(segment) } @@ -163,9 +169,10 @@ export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContex async executeTSRAction( deviceId: PeripheralDeviceId, actionId: string, - payload: Record + payload: Record, + timeoutMs?: number ): Promise { - return executePeripheralDeviceAction(this._context, deviceId, null, actionId, payload) + return executePeripheralDeviceAction(this._context, deviceId, timeoutMs ?? null, actionId, payload) } queuePartAfterTake(rawPart: IBlueprintPart, rawPieces: IBlueprintPiece[]): void { @@ -181,7 +188,18 @@ export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContex ) } + async emitIngestOperation(operation: unknown): Promise { + await emitIngestOperation(this._context, this._playoutModel, operation) + } + getCurrentTime(): number { return getCurrentTime() } + + getTimer(index: RundownTTimerIndex): IPlaylistTTimer { + return this.#tTimersService.getTimer(index) + } + clearAllTimers(): void { + this.#tTimersService.clearAllTimers() + } } diff --git a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts index a1c6849245f..5335d041bc6 100644 --- a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts @@ -13,10 +13,14 @@ import { PlayoutModel } from '../../playout/model/PlayoutModel.js' import { RundownEventContext } from './RundownEventContext.js' import { DBRundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { setTimelineDatastoreValue, removeTimelineDatastoreValue } from '../../playout/datastore.js' +import { TTimersService } from './services/TTimersService.js' +import type { IPlaylistTTimer } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' +import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' export class RundownActivationContext extends RundownEventContext implements IRundownActivationContext { private readonly _playoutModel: PlayoutModel private readonly _context: JobContext + readonly #tTimersService: TTimersService private readonly _previousState: IRundownActivationContextState private readonly _currentState: IRundownActivationContextState @@ -43,6 +47,8 @@ export class RundownActivationContext extends RundownEventContext implements IRu this._playoutModel = options.playoutModel this._previousState = options.previousState this._currentState = options.currentState + + this.#tTimersService = TTimersService.withPlayoutModel(this._playoutModel, this._context) } get previousState(): IRundownActivationContextState { @@ -59,9 +65,10 @@ export class RundownActivationContext extends RundownEventContext implements IRu async executeTSRAction( deviceId: PeripheralDeviceId, actionId: string, - payload: Record + payload: Record, + timeoutMs?: number ): Promise { - return executePeripheralDeviceAction(this._context, deviceId, null, actionId, payload) + return executePeripheralDeviceAction(this._context, deviceId, timeoutMs ?? null, actionId, payload) } async setTimelineDatastoreValue(key: string, value: unknown, mode: DatastorePersistenceMode): Promise { @@ -74,4 +81,11 @@ export class RundownActivationContext extends RundownEventContext implements IRu await removeTimelineDatastoreValue(this._context, key) }) } + + getTimer(index: RundownTTimerIndex): IPlaylistTTimer { + return this.#tTimersService.getTimer(index) + } + clearAllTimers(): void { + this.#tTimersService.clearAllTimers() + } } diff --git a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts index d8289be7d99..61e2dcb4863 100644 --- a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts +++ b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts @@ -3,6 +3,7 @@ import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceIns import { normalizeArrayToMap, omit } from '@sofie-automation/corelib/dist/lib' import { protectString, protectStringArray, unprotectStringArray } from '@sofie-automation/corelib/dist/protectedString' import { PlayoutPartInstanceModel } from '../../playout/model/PlayoutPartInstanceModel.js' +import { PlayoutModel } from '../../playout/model/PlayoutModel.js' import { ReadonlyDeep } from 'type-fest' import _ from 'underscore' import { ContextInfo } from './CommonContext.js' @@ -32,24 +33,41 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Piece' import { EXPECTED_INGEST_TO_PLAYOUT_TIME } from '@sofie-automation/shared-lib/dist/core/constants' import { getCurrentTime } from '../../lib/index.js' +import { TTimersService } from './services/TTimersService.js' +import type { + DBRundownPlaylist, + RundownTTimer, + RundownTTimerIndex, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { IPlaylistTTimer } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' export class SyncIngestUpdateToPartInstanceContext extends RundownUserContext implements ISyncIngestUpdateToPartInstanceContext { - private readonly _proposedPieceInstances: Map> + readonly #context: JobContext + readonly #playoutModel: PlayoutModel + readonly #proposedPieceInstances: Map> + readonly #tTimersService: TTimersService + readonly #changedTTimers = new Map() - private partInstance: PlayoutPartInstanceModel | null + #partInstance: PlayoutPartInstanceModel | null public get hasRemovedPartInstance(): boolean { - return !this.partInstance + return !this.#partInstance + } + + public get changedTTimers(): RundownTTimer[] { + return Array.from(this.#changedTTimers.values()) } constructor( - private readonly _context: JobContext, + context: JobContext, + playoutModel: PlayoutModel, contextInfo: ContextInfo, studio: ReadonlyDeep, showStyleCompound: ReadonlyDeep, + playlist: ReadonlyDeep, rundown: ReadonlyDeep, partInstance: PlayoutPartInstanceModel, proposedPieceInstances: ReadonlyDeep, @@ -58,32 +76,49 @@ export class SyncIngestUpdateToPartInstanceContext super( contextInfo, studio, - _context.getStudioBlueprintConfig(), + context.getStudioBlueprintConfig(), showStyleCompound, - _context.getShowStyleBlueprintConfig(showStyleCompound), + context.getShowStyleBlueprintConfig(showStyleCompound), rundown ) - this.partInstance = partInstance + this.#context = context + this.#playoutModel = playoutModel + this.#partInstance = partInstance + + this.#proposedPieceInstances = normalizeArrayToMap(proposedPieceInstances, '_id') + this.#tTimersService = new TTimersService( + playlist.tTimers, + (updatedTimer) => { + this.#changedTTimers.set(updatedTimer.index, updatedTimer) + }, + this.#playoutModel, + this.#context + ) + } - this._proposedPieceInstances = normalizeArrayToMap(proposedPieceInstances, '_id') + getTimer(index: RundownTTimerIndex): IPlaylistTTimer { + return this.#tTimersService.getTimer(index) + } + clearAllTimers(): void { + this.#tTimersService.clearAllTimers() } syncPieceInstance( pieceInstanceId: string, modifiedPiece?: Omit ): IBlueprintPieceInstance { - const proposedPieceInstance = this._proposedPieceInstances.get(protectString(pieceInstanceId)) + const proposedPieceInstance = this.#proposedPieceInstances.get(protectString(pieceInstanceId)) if (!proposedPieceInstance) { throw new Error(`PieceInstance "${pieceInstanceId}" could not be found`) } - if (!this.partInstance) throw new Error(`PartInstance has been removed`) + if (!this.#partInstance) throw new Error(`PartInstance has been removed`) // filter the submission to the allowed ones const piece = modifiedPiece ? postProcessPieces( - this._context, + this.#context, [ { ...modifiedPiece, @@ -92,9 +127,9 @@ export class SyncIngestUpdateToPartInstanceContext }, ], this.showStyleCompound.blueprintId, - this.partInstance.partInstance.rundownId, - this.partInstance.partInstance.segmentId, - this.partInstance.partInstance.part._id, + this.#partInstance.partInstance.rundownId, + this.#partInstance.partInstance.segmentId, + this.#partInstance.partInstance.part._id, this.playStatus === 'current' )[0] : proposedPieceInstance.piece @@ -103,7 +138,7 @@ export class SyncIngestUpdateToPartInstanceContext ...proposedPieceInstance, piece: piece, } - this.partInstance.mergeOrInsertPieceInstance(newPieceInstance) + this.#partInstance.mergeOrInsertPieceInstance(newPieceInstance) return convertPieceInstanceToBlueprints(newPieceInstance) } @@ -111,19 +146,19 @@ export class SyncIngestUpdateToPartInstanceContext insertPieceInstance(piece0: IBlueprintPiece): IBlueprintPieceInstance { const trimmedPiece: IBlueprintPiece = _.pick(piece0, IBlueprintPieceObjectsSampleKeys) - if (!this.partInstance) throw new Error(`PartInstance has been removed`) + if (!this.#partInstance) throw new Error(`PartInstance has been removed`) const piece = postProcessPieces( - this._context, + this.#context, [trimmedPiece], this.showStyleCompound.blueprintId, - this.partInstance.partInstance.rundownId, - this.partInstance.partInstance.segmentId, - this.partInstance.partInstance.part._id, + this.#partInstance.partInstance.rundownId, + this.#partInstance.partInstance.segmentId, + this.#partInstance.partInstance.part._id, this.playStatus === 'current' )[0] - const newPieceInstance = this.partInstance.insertPlannedPiece(piece) + const newPieceInstance = this.#partInstance.insertPlannedPiece(piece) return convertPieceInstanceToBlueprints(newPieceInstance.pieceInstance) } @@ -134,13 +169,13 @@ export class SyncIngestUpdateToPartInstanceContext throw new Error(`Cannot update PieceInstance "${pieceInstanceId}". Some valid properties must be defined`) } - if (!this.partInstance) throw new Error(`PartInstance has been removed`) + if (!this.#partInstance) throw new Error(`PartInstance has been removed`) - const pieceInstance = this.partInstance.getPieceInstance(protectString(pieceInstanceId)) + const pieceInstance = this.#partInstance.getPieceInstance(protectString(pieceInstanceId)) if (!pieceInstance) { throw new Error(`PieceInstance "${pieceInstanceId}" could not be found`) } - if (pieceInstance.pieceInstance.partInstanceId !== this.partInstance.partInstance._id) { + if (pieceInstance.pieceInstance.partInstanceId !== this.#partInstance.partInstance._id) { throw new Error(`PieceInstance "${pieceInstanceId}" does not belong to the current PartInstance`) } @@ -167,13 +202,13 @@ export class SyncIngestUpdateToPartInstanceContext return convertPieceInstanceToBlueprints(pieceInstance.pieceInstance) } updatePartInstance(updatePart: Partial): IBlueprintPartInstance { - if (!this.partInstance) throw new Error(`PartInstance has been removed`) + if (!this.#partInstance) throw new Error(`PartInstance has been removed`) // for autoNext, the new expectedDuration cannot be shorter than the time a part has been on-air for - const expectedDuration = updatePart.expectedDuration ?? this.partInstance.partInstance.part.expectedDuration - const autoNext = updatePart.autoNext ?? this.partInstance.partInstance.part.autoNext + const expectedDuration = updatePart.expectedDuration ?? this.#partInstance.partInstance.part.expectedDuration + const autoNext = updatePart.autoNext ?? this.#partInstance.partInstance.part.autoNext if (expectedDuration && autoNext) { - const onAir = this.partInstance.partInstance.timings?.reportedStartedPlayback + const onAir = this.#partInstance.partInstance.timings?.reportedStartedPlayback const minTime = Date.now() - (onAir ?? 0) + EXPECTED_INGEST_TO_PLAYOUT_TIME if (onAir && minTime > expectedDuration) { updatePart.expectedDuration = minTime @@ -185,31 +220,31 @@ export class SyncIngestUpdateToPartInstanceContext this.showStyleCompound.blueprintId ) - if (!this.partInstance.updatePartProps(playoutUpdatePart)) { + if (!this.#partInstance.updatePartProps(playoutUpdatePart)) { throw new Error(`Cannot update PartInstance. Some valid properties must be defined`) } - return convertPartInstanceToBlueprints(this.partInstance.partInstance) + return convertPartInstanceToBlueprints(this.#partInstance.partInstance) } removePartInstance(): void { if (this.playStatus !== 'next') throw new Error(`Only the 'next' PartInstance can be removed`) - this.partInstance = null + this.#partInstance = null } removePieceInstances(...pieceInstanceIds: string[]): string[] { - if (!this.partInstance) throw new Error(`PartInstance has been removed`) + if (!this.#partInstance) throw new Error(`PartInstance has been removed`) const rawPieceInstanceIdSet = new Set(protectStringArray(pieceInstanceIds)) - const pieceInstances = this.partInstance.pieceInstances.filter((p) => + const pieceInstances = this.#partInstance.pieceInstances.filter((p) => rawPieceInstanceIdSet.has(p.pieceInstance._id) ) const pieceInstanceIdsToRemove = pieceInstances.map((p) => p.pieceInstance._id) for (const id of pieceInstanceIdsToRemove) { - this.partInstance.removePieceInstance(id) + this.#partInstance.removePieceInstance(id) } return unprotectStringArray(pieceInstanceIdsToRemove) diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 0359871eb2a..80b4b312448 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -14,7 +14,7 @@ import { TSR, IBlueprintPlayoutDevice, StudioRouteSet, - IBlueprintSegment, + IBlueprintSegmentDB, } from '@sofie-automation/blueprints-integration' import { PartInstanceId, PeripheralDeviceId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { ReadonlyDeep } from 'type-fest' @@ -37,7 +37,10 @@ import { import { BlueprintQuickLookInfo } from '@sofie-automation/blueprints-integration/dist/context/quickLoopInfo' import { setNextPartFromPart } from '../../playout/setNext.js' import { getOrderedPartsAfterPlayhead } from '../../playout/lookahead/util.js' -import { convertPartToBlueprints } from './lib.js' +import { convertPartToBlueprints, emitIngestOperation } from './lib.js' +import { IPlaylistTTimer } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' +import { TTimersService } from './services/TTimersService.js' +import type { RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' export class DatastoreActionExecutionContext extends ShowStyleUserContext @@ -70,6 +73,8 @@ export class DatastoreActionExecutionContext /** Actions */ export class ActionExecutionContext extends ShowStyleUserContext implements IActionExecutionContext, IEventContext { + readonly #tTimersService: TTimersService + /** * Whether the blueprints requested a take to be performed at the end of this action * */ @@ -112,6 +117,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct private readonly partAndPieceInstanceService: PartAndPieceInstanceActionService ) { super(contextInfo, _context, showStyle, watchedPackages) + this.#tTimersService = TTimersService.withPlayoutModel(_playoutModel, _context) } async getUpcomingParts(limit: number = 5): Promise> { @@ -130,7 +136,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct return this.partAndPieceInstanceService.getResolvedPieceInstances(part) } - async getSegment(segment: 'current' | 'next'): Promise { + async getSegment(segment: 'current' | 'next'): Promise { return this.partAndPieceInstanceService.getSegment(segment) } @@ -260,9 +266,10 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct async executeTSRAction( deviceId: PeripheralDeviceId, actionId: string, - payload: Record + payload: Record, + timeoutMs?: number ): Promise { - return executePeripheralDeviceAction(this._context, deviceId, null, actionId, payload) + return executePeripheralDeviceAction(this._context, deviceId, timeoutMs ?? null, actionId, payload) } async setTimelineDatastoreValue(key: string, value: unknown, mode: DatastorePersistenceMode): Promise { @@ -277,7 +284,18 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct }) } + async emitIngestOperation(operation: unknown): Promise { + await emitIngestOperation(this._context, this._playoutModel, operation) + } + getCurrentTime(): number { return getCurrentTime() } + + getTimer(index: RundownTTimerIndex): IPlaylistTTimer { + return this.#tTimersService.getTimer(index) + } + clearAllTimers(): void { + this.#tTimersService.clearAllTimers() + } } diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index 23b50b18688..f16ee424c08 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -70,6 +70,10 @@ import { import type { PlayoutMutatablePart } from '../../playout/model/PlayoutPartInstanceModel.js' import { BlueprintQuickLookInfo } from '@sofie-automation/blueprints-integration/dist/context/quickLoopInfo' import { IngestPartNotifyItemReady } from '@sofie-automation/shared-lib/dist/ingest/rundownStatus' +import { PlayoutModel } from '../../playout/model/PlayoutModel.js' +import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { logger } from '../../logging.js' /** * Convert an object to have all the values of all keys (including optionals) be 'true' @@ -160,6 +164,8 @@ function convertPieceInstanceToBlueprintsInner( fromHold: pieceInstance.infinite.fromHold, fromPreviousPart: pieceInstance.infinite.fromPreviousPart, fromPreviousPlayhead: pieceInstance.infinite.fromPreviousPlayhead, + infiniteInstanceId: unprotectString(pieceInstance.infinite.infiniteInstanceId), + infiniteInstanceIndex: pieceInstance.infinite.infiniteInstanceIndex, }) : undefined, piece: convertPieceToBlueprints(pieceInstance.piece), @@ -713,3 +719,28 @@ export function createBlueprintQuickLoopInfo(playlist: ReadonlyDeep { + const refPartInstance = playoutModel.currentPartInstance ?? playoutModel.nextPartInstance + if (!refPartInstance) throw new Error('Cannot emit ingest operation when there is no current or next partInstance') + + const rundown = playoutModel.getRundown(refPartInstance.partInstance.rundownId) + if (!rundown) throw new Error('Cannot emit ingest operation when the partInstance has no rundown') + + await context + .queueIngestJob(IngestJobs.PlayoutExecuteChangeOperation, { + rundownExternalId: rundown.rundown.externalId, + segmentId: refPartInstance.partInstance.segmentId ?? null, + partId: refPartInstance.partInstance.part._id ?? null, + operation, + }) + .catch((e) => { + logger.warn(`Failed to queue ingest operation: ${stringifyError(e)}`) + + throw new Error('Internal error while queueing ingest operation') + }) +} diff --git a/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts b/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts index 68c807d764d..8f72b9f89d6 100644 --- a/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts +++ b/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts @@ -9,7 +9,7 @@ import { IBlueprintPieceDB, IBlueprintPieceInstance, IBlueprintResolvedPieceInstance, - IBlueprintSegment, + IBlueprintSegmentDB, OmitId, SomeContent, Time, @@ -145,7 +145,7 @@ export class PartAndPieceInstanceActionService { ) return resolvedInstances.map(convertResolvedPieceInstanceToBlueprints) } - getSegment(segment: 'current' | 'next'): IBlueprintSegment | undefined { + getSegment(segment: 'current' | 'next'): IBlueprintSegmentDB | undefined { const partInstance = this.#getPartInstance(segment) if (!partInstance) return undefined @@ -318,7 +318,10 @@ export class PartAndPieceInstanceActionService { const { pieceInstance } = foundPieceInstance - if (pieceInstance.pieceInstance.infinite?.fromPreviousPart) { + if ( + pieceInstance.pieceInstance.infinite?.fromPreviousPart && + pieceInstance.pieceInstance.partInstanceId === this._playoutModel.playlist.nextPartInfo?.partInstanceId + ) { throw new Error('Cannot update an infinite piece that is continued from a previous part') } diff --git a/packages/job-worker/src/blueprints/context/services/PersistantStateStore.ts b/packages/job-worker/src/blueprints/context/services/PersistantStateStore.ts index 6150eac0951..43b127e9c8d 100644 --- a/packages/job-worker/src/blueprints/context/services/PersistantStateStore.ts +++ b/packages/job-worker/src/blueprints/context/services/PersistantStateStore.ts @@ -1,32 +1,53 @@ import type { TimelinePersistentState } from '@sofie-automation/blueprints-integration' import type { BlueprintPlayoutPersistentStore } from '@sofie-automation/blueprints-integration/dist/context/playoutStore' import { clone } from '@sofie-automation/corelib/dist/lib' +import type { PlayoutModel } from '../../../playout/model/PlayoutModel.js' export class PersistentPlayoutStateStore implements BlueprintPlayoutPersistentStore { - #state: TimelinePersistentState | undefined - #hasChanges = false + #privateState: TimelinePersistentState | undefined + #hasPrivateChanges = false + #publicState: TimelinePersistentState | undefined + #hasPublicChanges = false - get hasChanges(): boolean { - return this.#hasChanges + constructor(privateState: TimelinePersistentState | undefined, publicState: TimelinePersistentState | undefined) { + this.#privateState = clone(privateState) + this.#publicState = clone(publicState) } - constructor(state: TimelinePersistentState | undefined) { - this.#state = clone(state) + saveToModel(model: PlayoutModel): void { + if (this.#hasPrivateChanges) model.setBlueprintPrivatePersistentState(this.#privateState) + if (this.#hasPublicChanges) model.setBlueprintPublicPersistentState(this.#publicState) } getAll(): Partial { - return this.#state || {} + return this.#privateState || {} } getKey(k: K): unknown { - return this.#state?.[k] + return this.#privateState?.[k] } setKey(k: K, v: unknown): void { - if (!this.#state) this.#state = {} - ;(this.#state as any)[k] = v - this.#hasChanges = true + if (!this.#privateState) this.#privateState = {} + ;(this.#privateState as any)[k] = v + this.#hasPrivateChanges = true } setAll(obj: unknown): void { - this.#state = obj - this.#hasChanges = true + this.#privateState = obj + this.#hasPrivateChanges = true + } + + getAllPublic(): Partial { + return this.#publicState || {} + } + getKeyPublic(k: K): unknown { + return this.#publicState?.[k] + } + setKeyPublic(k: K, v: unknown): void { + if (!this.#publicState) this.#publicState = {} + ;(this.#publicState as any)[k] = v + this.#hasPublicChanges = true + } + setAllPublic(obj: unknown): void { + this.#publicState = obj + this.#hasPublicChanges = true } } diff --git a/packages/job-worker/src/blueprints/context/services/TTimersService.ts b/packages/job-worker/src/blueprints/context/services/TTimersService.ts new file mode 100644 index 00000000000..ab0a67452da --- /dev/null +++ b/packages/job-worker/src/blueprints/context/services/TTimersService.ts @@ -0,0 +1,251 @@ +import type { + IPlaylistTTimer, + IPlaylistTTimerState, +} from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' +import type { + RundownTTimer, + RundownTTimerIndex, + TimerState, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' +import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import type { PlayoutModel } from '../../../playout/model/PlayoutModel.js' +import { ReadonlyDeep } from 'type-fest' +import { + createCountdownTTimer, + createFreeRunTTimer, + createTimeOfDayTTimer, + pauseTTimer, + restartTTimer, + resumeTTimer, + validateTTimerIndex, + recalculateTTimerProjections, +} from '../../../playout/tTimers.js' +import { getCurrentTime } from '../../../lib/index.js' +import type { JobContext } from '../../../jobs/index.js' + +export class TTimersService { + readonly timers: [PlaylistTTimerImpl, PlaylistTTimerImpl, PlaylistTTimerImpl] + + constructor( + timers: ReadonlyDeep, + emitChange: (updatedTimer: ReadonlyDeep) => void, + playoutModel: PlayoutModel, + jobContext: JobContext + ) { + this.timers = [ + new PlaylistTTimerImpl(timers[0], emitChange, playoutModel, jobContext), + new PlaylistTTimerImpl(timers[1], emitChange, playoutModel, jobContext), + new PlaylistTTimerImpl(timers[2], emitChange, playoutModel, jobContext), + ] + } + + static withPlayoutModel(playoutModel: PlayoutModel, jobContext: JobContext): TTimersService { + return new TTimersService( + playoutModel.playlist.tTimers, + (updatedTimer) => { + playoutModel.updateTTimer(updatedTimer) + }, + playoutModel, + jobContext + ) + } + + getTimer(index: RundownTTimerIndex): IPlaylistTTimer { + validateTTimerIndex(index) + return this.timers[index - 1] + } + clearAllTimers(): void { + for (const timer of this.timers) { + timer.clearTimer() + } + } +} + +export class PlaylistTTimerImpl implements IPlaylistTTimer { + readonly #emitChange: (updatedTimer: ReadonlyDeep) => void + readonly #playoutModel: PlayoutModel + readonly #jobContext: JobContext + + #timer: ReadonlyDeep + + get index(): RundownTTimerIndex { + return this.#timer.index + } + get label(): string { + return this.#timer.label + } + get state(): IPlaylistTTimerState | null { + const rawMode = this.#timer.mode + const rawState = this.#timer.state + + if (!rawMode || !rawState) return null + + const currentTime = rawState.paused ? rawState.duration : rawState.zeroTime - getCurrentTime() + + switch (rawMode.type) { + case 'countdown': + return { + mode: 'countdown', + currentTime, + duration: rawMode.duration, + paused: rawState.paused, + stopAtZero: rawMode.stopAtZero, + } + case 'freeRun': + return { + mode: 'freeRun', + currentTime, + paused: rawState.paused, + } + case 'timeOfDay': + return { + mode: 'timeOfDay', + currentTime, + targetTime: rawState.paused ? 0 : rawState.zeroTime, + targetRaw: rawMode.targetRaw, + stopAtZero: rawMode.stopAtZero, + } + default: + assertNever(rawMode) + return null + } + } + + constructor( + timer: ReadonlyDeep, + emitChange: (updatedTimer: ReadonlyDeep) => void, + playoutModel: PlayoutModel, + jobContext: JobContext + ) { + this.#timer = timer + this.#emitChange = emitChange + this.#playoutModel = playoutModel + this.#jobContext = jobContext + + validateTTimerIndex(timer.index) + } + + setLabel(label: string): void { + this.#timer = { + ...this.#timer, + label: label, + } + this.#emitChange(this.#timer) + } + clearTimer(): void { + this.#timer = { + ...this.#timer, + mode: null, + state: null, + } + this.#emitChange(this.#timer) + } + startCountdown(duration: number, options?: { stopAtZero?: boolean; startPaused?: boolean }): void { + this.#timer = { + ...this.#timer, + ...createCountdownTTimer(duration, { + stopAtZero: options?.stopAtZero ?? true, + startPaused: options?.startPaused ?? false, + }), + } + this.#emitChange(this.#timer) + } + startTimeOfDay(targetTime: string | number, options?: { stopAtZero?: boolean }): void { + this.#timer = { + ...this.#timer, + ...createTimeOfDayTTimer(targetTime, { + stopAtZero: options?.stopAtZero ?? true, + }), + } + this.#emitChange(this.#timer) + } + startFreeRun(options?: { startPaused?: boolean }): void { + this.#timer = { + ...this.#timer, + ...createFreeRunTTimer({ + startPaused: options?.startPaused ?? false, + }), + } + this.#emitChange(this.#timer) + } + pause(): boolean { + const newTimer = pauseTTimer(this.#timer) + if (!newTimer) return false + + this.#timer = newTimer + this.#emitChange(newTimer) + return true + } + resume(): boolean { + const newTimer = resumeTTimer(this.#timer) + if (!newTimer) return false + + this.#timer = newTimer + this.#emitChange(newTimer) + return true + } + restart(): boolean { + const newTimer = restartTTimer(this.#timer) + if (!newTimer) return false + + this.#timer = newTimer + this.#emitChange(newTimer) + return true + } + + clearProjected(): void { + this.#timer = { + ...this.#timer, + anchorPartId: undefined, + projectedState: undefined, + } + this.#emitChange(this.#timer) + } + + setProjectedAnchorPart(partId: string): void { + this.#timer = { + ...this.#timer, + anchorPartId: protectString(partId), + projectedState: undefined, // Clear manual projection + } + this.#emitChange(this.#timer) + + // Recalculate projections immediately since we already have the playout model + recalculateTTimerProjections(this.#jobContext, this.#playoutModel) + } + + setProjectedAnchorPartByExternalId(externalId: string): void { + const part = this.#playoutModel.getAllOrderedParts().find((p) => p.externalId === externalId) + if (!part) return + + this.setProjectedAnchorPart(unprotectString(part._id)) + } + + setProjectedTime(time: number, paused: boolean = false): void { + const projectedState: TimerState = paused + ? literal({ paused: true, duration: time - getCurrentTime() }) + : literal({ paused: false, zeroTime: time }) + + this.#timer = { + ...this.#timer, + anchorPartId: undefined, // Clear automatic anchor + projectedState, + } + this.#emitChange(this.#timer) + } + + setProjectedDuration(duration: number, paused: boolean = false): void { + const projectedState: TimerState = paused + ? literal({ paused: true, duration }) + : literal({ paused: false, zeroTime: getCurrentTime() + duration }) + + this.#timer = { + ...this.#timer, + anchorPartId: undefined, // Clear automatic anchor + projectedState, + } + this.#emitChange(this.#timer) + } +} diff --git a/packages/job-worker/src/blueprints/context/services/__tests__/PartAndPieceInstanceActionService.test.ts b/packages/job-worker/src/blueprints/context/services/__tests__/PartAndPieceInstanceActionService.test.ts index e8cfda6bb49..5977eb1449e 100644 --- a/packages/job-worker/src/blueprints/context/services/__tests__/PartAndPieceInstanceActionService.test.ts +++ b/packages/job-worker/src/blueprints/context/services/__tests__/PartAndPieceInstanceActionService.test.ts @@ -1499,6 +1499,81 @@ describe('Test blueprint api context', () => { expect(service.currentPartState).toEqual(ActionPartChange.SAFE_CHANGE) }) }) + + test('can update infinite piece from previous part if in current part instance', async () => { + const { jobContext, playlistId, allPartInstances } = await setupMyDefaultRundown() + + const currentPartInstance = allPartInstances[1] + + // Create an infinite piece instance continued from previous part + const pieceInstance: PieceInstance = { + _id: protectString('piece_infinite'), + rundownId: currentPartInstance.partInstance.rundownId, + partInstanceId: currentPartInstance.partInstance._id, + playlistActivationId: currentPartInstance.partInstance.playlistActivationId, + piece: { + _id: protectString('piece_infinite_p'), + externalId: '-', + enable: { start: 0 }, + name: 'infinite', + sourceLayerId: '', + outputLayerId: '', + startPartId: allPartInstances[0].partInstance.part._id, + content: {}, + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + lifespan: PieceLifespan.OutOnRundownEnd, + pieceType: IBlueprintPieceType.Normal, + invalid: false, + }, + infinite: { + infiniteInstanceId: getRandomId(), + infiniteInstanceIndex: 1, + infinitePieceId: protectString('piece_infinite_p'), + fromPreviousPart: true, + }, + } + + await jobContext.mockCollections.PieceInstances.insertOne(pieceInstance) + + await setPartInstances(jobContext, playlistId, currentPartInstance, undefined) + + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { service } = await getTestee(jobContext, playoutModel) + + // Updating it in CURRENT part should succeed + await expect( + service.updatePieceInstance(unprotectString(pieceInstance._id), { name: 'updated' }) + ).resolves.toBeTruthy() + }) + }) + + test('updating lifespan to infinite sets dynamicallyConvertedToInfinite', async () => { + const { jobContext, playlistId, allPartInstances } = await setupMyDefaultRundown() + + const currentPartInstance = allPartInstances[0] + const pieceInstance = (await jobContext.mockCollections.PieceInstances.findOne({ + partInstanceId: currentPartInstance.partInstance._id, + })) as PieceInstance + expect(pieceInstance).toBeTruthy() + expect(pieceInstance.piece.lifespan).toEqual(PieceLifespan.WithinPart) + expect(pieceInstance.infinite).toBeUndefined() + + await setPartInstances(jobContext, playlistId, currentPartInstance, undefined) + + await wrapWithPlayoutModel(jobContext, playlistId, async (playoutModel) => { + const { service } = await getTestee(jobContext, playoutModel) + + await service.updatePieceInstance(unprotectString(pieceInstance._id), { + lifespan: PieceLifespan.OutOnRundownEnd, + }) + + const updatedPieceInstance = playoutModel.findPieceInstance(pieceInstance._id)?.pieceInstance + expect(updatedPieceInstance).toBeTruthy() + expect(updatedPieceInstance?.pieceInstance.piece.lifespan).toEqual(PieceLifespan.OutOnRundownEnd) + expect(updatedPieceInstance?.pieceInstance.infinite).toBeTruthy() + expect(updatedPieceInstance?.pieceInstance.dynamicallyConvertedToInfinite).toBeTruthy() + }) + }) }) describe('stopPiecesOnLayers', () => { diff --git a/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts b/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts new file mode 100644 index 00000000000..72236e2d51b --- /dev/null +++ b/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts @@ -0,0 +1,1069 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import { useFakeCurrentTime, useRealCurrentTime } from '../../../../__mocks__/time.js' +import { TTimersService, PlaylistTTimerImpl } from '../TTimersService.js' +import type { PlayoutModel } from '../../../../playout/model/PlayoutModel.js' +import type { RundownTTimer, RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { mock, MockProxy } from 'jest-mock-extended' +import type { ReadonlyDeep } from 'type-fest' +import type { JobContext } from '../../../../jobs/index.js' + +function createMockJobContext(): MockProxy { + return mock() +} + +function createMockPlayoutModel(tTimers: [RundownTTimer, RundownTTimer, RundownTTimer]): MockProxy { + const mockPlayoutModel = mock() + const mockPlaylist = { + tTimers, + } as unknown as ReadonlyDeep + + Object.defineProperty(mockPlayoutModel, 'playlist', { + get: () => mockPlaylist, + configurable: true, + }) + + return mockPlayoutModel +} + +function createEmptyTTimers(): [RundownTTimer, RundownTTimer, RundownTTimer] { + return [ + { index: 1, label: 'Timer 1', mode: null, state: null }, + { index: 2, label: 'Timer 2', mode: null, state: null }, + { index: 3, label: 'Timer 3', mode: null, state: null }, + ] +} + +describe('TTimersService', () => { + beforeEach(() => { + useFakeCurrentTime(10000) + }) + + afterEach(() => { + useRealCurrentTime() + }) + + describe('constructor', () => { + it('should create three timer instances', () => { + const timers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(timers) + const mockJobContext = createMockJobContext() + + const service = new TTimersService(timers, updateFn, mockPlayoutModel, mockJobContext) + + expect(service.timers).toHaveLength(3) + expect(service.timers[0]).toBeInstanceOf(PlaylistTTimerImpl) + expect(service.timers[1]).toBeInstanceOf(PlaylistTTimerImpl) + expect(service.timers[2]).toBeInstanceOf(PlaylistTTimerImpl) + }) + }) + + it('from playout model', () => { + const mockPlayoutModel = createMockPlayoutModel(createEmptyTTimers()) + const mockJobContext = createMockJobContext() + + const service = TTimersService.withPlayoutModel(mockPlayoutModel, mockJobContext) + expect(service.timers).toHaveLength(3) + + const timer = service.getTimer(1) + expect(timer.index).toBe(1) + + timer.setLabel('New Label') + expect(mockPlayoutModel.updateTTimer).toHaveBeenCalledWith( + expect.objectContaining({ index: 1, label: 'New Label' }) + ) + }) + + describe('getTimer', () => { + it('should return the correct timer for index 1', () => { + const timers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(timers) + const mockJobContext = createMockJobContext() + + const service = new TTimersService(timers, updateFn, mockPlayoutModel, mockJobContext) + + const timer = service.getTimer(1) + + expect(timer).toBe(service.timers[0]) + }) + + it('should return the correct timer for index 2', () => { + const timers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(timers) + const mockJobContext = createMockJobContext() + + const service = new TTimersService(timers, updateFn, mockPlayoutModel, mockJobContext) + + const timer = service.getTimer(2) + + expect(timer).toBe(service.timers[1]) + }) + + it('should return the correct timer for index 3', () => { + const timers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(timers) + const mockJobContext = createMockJobContext() + + const service = new TTimersService(timers, updateFn, mockPlayoutModel, mockJobContext) + + const timer = service.getTimer(3) + + expect(timer).toBe(service.timers[2]) + }) + + it('should throw for invalid index', () => { + const timers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(timers) + const mockJobContext = createMockJobContext() + + const service = new TTimersService(timers, updateFn, mockPlayoutModel, mockJobContext) + + expect(() => service.getTimer(0 as RundownTTimerIndex)).toThrow('T-timer index out of range: 0') + expect(() => service.getTimer(4 as RundownTTimerIndex)).toThrow('T-timer index out of range: 4') + }) + }) + + describe('clearAllTimers', () => { + it('should call clearTimer on all timers', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'freeRun' } + tTimers[0].state = { paused: false, zeroTime: 5000 } + tTimers[1].mode = { type: 'countdown', duration: 60000, stopAtZero: true } + tTimers[1].state = { paused: false, zeroTime: 65000 } + + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + + const service = new TTimersService(tTimers, updateFn, mockPlayoutModel, mockJobContext) + + service.clearAllTimers() + + // updateTTimer should have been called 3 times (once for each timer) + expect(updateFn).toHaveBeenCalledTimes(3) + expect(updateFn).toHaveBeenCalledWith(expect.objectContaining({ index: 1, mode: null })) + expect(updateFn).toHaveBeenCalledWith(expect.objectContaining({ index: 2, mode: null })) + expect(updateFn).toHaveBeenCalledWith(expect.objectContaining({ index: 3, mode: null })) + }) + }) +}) + +describe('PlaylistTTimerImpl', () => { + beforeEach(() => { + useFakeCurrentTime(10000) + }) + + afterEach(() => { + useRealCurrentTime() + }) + + describe('getters', () => { + it('should return the correct index', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[1], updateFn, mockPlayoutModel, mockJobContext) + + expect(timer.index).toBe(2) + }) + + it('should return the correct label', () => { + const tTimers = createEmptyTTimers() + tTimers[1].label = 'Custom Label' + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[1], updateFn, mockPlayoutModel, mockJobContext) + + expect(timer.label).toBe('Custom Label') + }) + + it('should return null state when no mode is set', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + expect(timer.state).toBeNull() + }) + + it('should return running freeRun state', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'freeRun' } + tTimers[0].state = { paused: false, zeroTime: 15000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + expect(timer.state).toEqual({ + mode: 'freeRun', + currentTime: 5000, // 10000 - 5000 + paused: false, // pauseTime is null = running + }) + }) + + it('should return paused freeRun state', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'freeRun' } + tTimers[0].state = { paused: true, duration: 3000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + expect(timer.state).toEqual({ + mode: 'freeRun', + currentTime: 3000, // 8000 - 5000 + paused: true, // pauseTime is set = paused + }) + }) + + it('should return running countdown state', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { + type: 'countdown', + duration: 60000, + stopAtZero: true, + } + tTimers[0].state = { paused: false, zeroTime: 15000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + expect(timer.state).toEqual({ + mode: 'countdown', + currentTime: 5000, // 10000 - 5000 + duration: 60000, + paused: false, // pauseTime is null = running + stopAtZero: true, + }) + }) + + it('should return paused countdown state', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { + type: 'countdown', + duration: 60000, + stopAtZero: false, + } + tTimers[0].state = { paused: true, duration: 2000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + expect(timer.state).toEqual({ + mode: 'countdown', + currentTime: 2000, // 7000 - 5000 + duration: 60000, + paused: true, // pauseTime is set = paused + stopAtZero: false, + }) + }) + + it('should return timeOfDay state', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { + type: 'timeOfDay', + targetRaw: '15:30', + stopAtZero: true, + } + tTimers[0].state = { paused: false, zeroTime: 20000 } // 10 seconds in the future + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + expect(timer.state).toEqual({ + mode: 'timeOfDay', + currentTime: 10000, // targetTime - getCurrentTime() = 20000 - 10000 + targetTime: 20000, + targetRaw: '15:30', + stopAtZero: true, + }) + }) + + it('should return timeOfDay state with numeric targetRaw', () => { + const tTimers = createEmptyTTimers() + const targetTimestamp = 1737331200000 + tTimers[0].mode = { + type: 'timeOfDay', + targetRaw: targetTimestamp, + stopAtZero: false, + } + tTimers[0].state = { paused: false, zeroTime: targetTimestamp } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + expect(timer.state).toEqual({ + mode: 'timeOfDay', + currentTime: targetTimestamp - 10000, // targetTime - getCurrentTime() + targetTime: targetTimestamp, + targetRaw: targetTimestamp, + stopAtZero: false, + }) + }) + }) + + describe('setLabel', () => { + it('should update the label', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.setLabel('New Label') + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'New Label', + mode: null, + state: null, + }) + }) + }) + + describe('clearTimer', () => { + it('should clear the timer mode', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'freeRun' } + tTimers[0].state = { paused: false, zeroTime: 5000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.clearTimer() + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + }) + }) + }) + + describe('startCountdown', () => { + it('should start a running countdown with default options', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.startCountdown(60000) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: false, zeroTime: 70000 }, + }) + }) + + it('should start a paused countdown', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.startCountdown(30000, { startPaused: true, stopAtZero: false }) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'countdown', + duration: 30000, + stopAtZero: false, + }, + state: { paused: true, duration: 30000 }, + }) + }) + }) + + describe('startFreeRun', () => { + it('should start a running free-run timer', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.startFreeRun() + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'freeRun', + }, + state: { paused: false, zeroTime: 10000 }, + }) + }) + + it('should start a paused free-run timer', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.startFreeRun({ startPaused: true }) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'freeRun', + }, + state: { paused: true, duration: 0 }, + }) + }) + }) + + describe('startTimeOfDay', () => { + it('should start a timeOfDay timer with time string', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.startTimeOfDay('15:30') + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'timeOfDay', + targetRaw: '15:30', + stopAtZero: true, + }, + state: { + paused: false, + zeroTime: expect.any(Number), // new target time + }, + }) + }) + + it('should start a timeOfDay timer with numeric timestamp', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + const targetTimestamp = 1737331200000 + + timer.startTimeOfDay(targetTimestamp) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'timeOfDay', + targetRaw: targetTimestamp, + stopAtZero: true, + }, + state: { + paused: false, + zeroTime: targetTimestamp, + }, + }) + }) + + it('should start a timeOfDay timer with stopAtZero false', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.startTimeOfDay('18:00', { stopAtZero: false }) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: expect.objectContaining({ + type: 'timeOfDay', + targetRaw: '18:00', + stopAtZero: false, + }), + state: expect.objectContaining({ + paused: false, + zeroTime: expect.any(Number), + }), + }) + }) + + it('should start a timeOfDay timer with 12-hour format', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.startTimeOfDay('5:30pm') + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: expect.objectContaining({ + type: 'timeOfDay', + targetRaw: '5:30pm', + stopAtZero: true, + }), + state: expect.objectContaining({ + paused: false, + zeroTime: expect.any(Number), + }), + }) + }) + + it('should throw for invalid time string', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + expect(() => timer.startTimeOfDay('invalid')).toThrow('Unable to parse target time for timeOfDay T-timer') + }) + + it('should throw for empty time string', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + expect(() => timer.startTimeOfDay('')).toThrow('Unable to parse target time for timeOfDay T-timer') + }) + }) + + describe('pause', () => { + it('should pause a running freeRun timer', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'freeRun' } + tTimers[0].state = { paused: false, zeroTime: 5000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.pause() + + expect(result).toBe(true) + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'freeRun', + }, + state: { paused: true, duration: -5000 }, + }) + }) + + it('should pause a running countdown timer', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'countdown', duration: 60000, stopAtZero: true } + tTimers[0].state = { paused: false, zeroTime: 70000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.pause() + + expect(result).toBe(true) + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: true, duration: 60000 }, + }) + }) + + it('should return false for timer with no mode', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.pause() + + expect(result).toBe(false) + expect(updateFn).not.toHaveBeenCalled() + }) + + it('should return false for timeOfDay timer (does not support pause)', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { + type: 'timeOfDay', + targetRaw: '15:30', + stopAtZero: true, + } + tTimers[0].state = { paused: false, zeroTime: 20000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.pause() + + expect(result).toBe(false) + expect(updateFn).not.toHaveBeenCalled() + }) + }) + + describe('resume', () => { + it('should resume a paused freeRun timer', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'freeRun' } + tTimers[0].state = { paused: true, duration: -3000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.resume() + + expect(result).toBe(true) + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'freeRun', + }, + state: { paused: false, zeroTime: 7000 }, // adjusted for pause duration + }) + }) + + it('should return true but not change a running timer', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'freeRun' } + tTimers[0].state = { paused: false, zeroTime: 5000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.resume() + + // Returns true because timer supports resume, but it's already running + expect(result).toBe(true) + expect(updateFn).toHaveBeenCalled() + }) + + it('should return false for timer with no mode', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.resume() + + expect(result).toBe(false) + expect(updateFn).not.toHaveBeenCalled() + }) + + it('should return false for timeOfDay timer (does not support resume)', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { + type: 'timeOfDay', + targetRaw: '15:30', + stopAtZero: true, + } + tTimers[0].state = { paused: false, zeroTime: 20000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.resume() + + expect(result).toBe(false) + expect(updateFn).not.toHaveBeenCalled() + }) + }) + + describe('restart', () => { + it('should restart a countdown timer', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'countdown', duration: 60000, stopAtZero: true } + tTimers[0].state = { paused: false, zeroTime: 40000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.restart() + + expect(result).toBe(true) + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: false, zeroTime: 70000 }, // reset to now + duration + }) + }) + + it('should restart a paused countdown timer (stays paused)', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { + type: 'countdown', + duration: 60000, + stopAtZero: false, + } + tTimers[0].state = { paused: true, duration: 15000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.restart() + + expect(result).toBe(true) + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: false, + }, + state: { paused: true, duration: 60000 }, // reset to full duration, paused + }) + }) + + it('should return false for freeRun timer', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { type: 'freeRun' } + tTimers[0].state = { paused: false, zeroTime: 5000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.restart() + + expect(result).toBe(false) + expect(updateFn).not.toHaveBeenCalled() + }) + + it('should restart a timeOfDay timer with valid targetRaw', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { + type: 'timeOfDay', + targetRaw: '15:30', + stopAtZero: true, + } + tTimers[0].state = { paused: false, zeroTime: 5000 } // old target time + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.restart() + + expect(result).toBe(true) + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: { + type: 'timeOfDay', + targetRaw: '15:30', + stopAtZero: true, + }, + state: { + paused: false, + zeroTime: expect.any(Number), // new target time + }, + }) + }) + + it('should return false for timeOfDay timer with invalid targetRaw', () => { + const tTimers = createEmptyTTimers() + tTimers[0].mode = { + type: 'timeOfDay', + targetRaw: 'invalid-time-string', + stopAtZero: true, + } + tTimers[0].state = { paused: false, zeroTime: 5000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.restart() + + expect(result).toBe(false) + expect(updateFn).not.toHaveBeenCalled() + }) + + it('should return false for timer with no mode', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + const result = timer.restart() + + expect(result).toBe(false) + expect(updateFn).not.toHaveBeenCalled() + }) + }) + + describe('clearProjected', () => { + it('should clear both anchorPartId and projectedState', () => { + const tTimers = createEmptyTTimers() + tTimers[0].anchorPartId = 'part1' as any + tTimers[0].projectedState = { paused: false, zeroTime: 50000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.clearProjected() + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + projectedState: undefined, + }) + }) + + it('should work when projections are already cleared', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.clearProjected() + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + projectedState: undefined, + }) + }) + }) + + describe('setProjectedAnchorPart', () => { + it('should set anchorPartId and clear projectedState', () => { + const tTimers = createEmptyTTimers() + tTimers[0].projectedState = { paused: false, zeroTime: 50000 } + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.setProjectedAnchorPart('part123') + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: 'part123', + projectedState: undefined, + }) + }) + + it('should not queue job or throw error', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + // Should not throw + expect(() => timer.setProjectedAnchorPart('part456')).not.toThrow() + + // Job queue should not be called (recalculate is called directly) + expect(mockJobContext.queueStudioJob).not.toHaveBeenCalled() + }) + }) + + describe('setProjectedTime', () => { + it('should set projectedState with absolute time (not paused)', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.setProjectedTime(50000, false) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + projectedState: { paused: false, zeroTime: 50000 }, + }) + }) + + it('should set projectedState with absolute time (paused)', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.setProjectedTime(50000, true) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + projectedState: { paused: true, duration: 40000 }, // 50000 - 10000 (current time) + }) + }) + + it('should clear anchorPartId when setting manual projection', () => { + const tTimers = createEmptyTTimers() + tTimers[0].anchorPartId = 'part1' as any + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.setProjectedTime(50000) + + expect(updateFn).toHaveBeenCalledWith( + expect.objectContaining({ + anchorPartId: undefined, + }) + ) + }) + + it('should default paused to false when not provided', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.setProjectedTime(50000) + + expect(updateFn).toHaveBeenCalledWith( + expect.objectContaining({ + projectedState: { paused: false, zeroTime: 50000 }, + }) + ) + }) + }) + + describe('setProjectedDuration', () => { + it('should set projectedState with relative duration (not paused)', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.setProjectedDuration(30000, false) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + projectedState: { paused: false, zeroTime: 40000 }, // 10000 (current) + 30000 (duration) + }) + }) + + it('should set projectedState with relative duration (paused)', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.setProjectedDuration(30000, true) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + projectedState: { paused: true, duration: 30000 }, + }) + }) + + it('should clear anchorPartId when setting manual projection', () => { + const tTimers = createEmptyTTimers() + tTimers[0].anchorPartId = 'part1' as any + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.setProjectedDuration(30000) + + expect(updateFn).toHaveBeenCalledWith( + expect.objectContaining({ + anchorPartId: undefined, + }) + ) + }) + + it('should default paused to false when not provided', () => { + const tTimers = createEmptyTTimers() + const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) + + timer.setProjectedDuration(30000) + + expect(updateFn).toHaveBeenCalledWith( + expect.objectContaining({ + projectedState: { paused: false, zeroTime: 40000 }, + }) + ) + }) + }) +}) diff --git a/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts b/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts index 6a8090a4b47..d5ad3766fa5 100644 --- a/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts +++ b/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts @@ -27,9 +27,11 @@ export interface MutableIngestRundownChanges { allCacheObjectIds: SofieIngestDataCacheObjId[] } -export class MutableIngestRundownImpl - implements MutableIngestRundown -{ +export class MutableIngestRundownImpl< + TRundownPayload = unknown, + TSegmentPayload = unknown, + TPartPayload = unknown, +> implements MutableIngestRundown { readonly ingestRundown: Omit< SofieIngestRundownWithSource, 'segments' diff --git a/packages/job-worker/src/blueprints/ingest/MutableIngestSegmentImpl.ts b/packages/job-worker/src/blueprints/ingest/MutableIngestSegmentImpl.ts index 19209c8dbdc..20a7654f4dc 100644 --- a/packages/job-worker/src/blueprints/ingest/MutableIngestSegmentImpl.ts +++ b/packages/job-worker/src/blueprints/ingest/MutableIngestSegmentImpl.ts @@ -23,9 +23,10 @@ export interface MutableIngestSegmentChanges { originalExternalId: string } -export class MutableIngestSegmentImpl - implements MutableIngestSegment -{ +export class MutableIngestSegmentImpl< + TSegmentPayload = unknown, + TPartPayload = unknown, +> implements MutableIngestSegment { readonly #ingestSegment: Omit, 'rank' | 'parts'> #originalExternalId: string #segmentHasChanges = false diff --git a/packages/job-worker/src/db/collections.ts b/packages/job-worker/src/db/collections.ts index b52abdcf2a2..0d7060e7a09 100644 --- a/packages/job-worker/src/db/collections.ts +++ b/packages/job-worker/src/db/collections.ts @@ -83,8 +83,9 @@ export type IChangeStreamEvents }> = { change: [doc: ChangeStreamDocument] } -export interface IChangeStream }> - extends EventEmitter> { +export interface IChangeStream }> extends EventEmitter< + IChangeStreamEvents +> { readonly closed: boolean close(): Promise diff --git a/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts b/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts index 7631c647c5d..54b97fb0110 100644 --- a/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts +++ b/packages/job-worker/src/events/__tests__/externalMessageQueue.test.ts @@ -56,6 +56,11 @@ describe('Test external message queue static methods', () => { type: PlaylistTimingType.None, }, rundownIdsInOrder: [protectString('rundown_1')], + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], }) await context.mockCollections.Rundowns.insertOne({ _id: protectString('rundown_1'), @@ -201,6 +206,11 @@ describe('Test sending messages to mocked endpoints', () => { type: PlaylistTimingType.None, }, rundownIdsInOrder: [protectString('rundown_1')], + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], }) const rundown = (await context.mockCollections.Rundowns.findOne(rundownId)) as DBRundown diff --git a/packages/job-worker/src/ingest/__tests__/lib.ts b/packages/job-worker/src/ingest/__tests__/lib.ts index 6583b09bcd5..2f9af7cf9ab 100644 --- a/packages/job-worker/src/ingest/__tests__/lib.ts +++ b/packages/job-worker/src/ingest/__tests__/lib.ts @@ -29,6 +29,6 @@ export async function removeRundownPlaylistFromDb( await Promise.allSettled([ context.mockCollections.RundownPlaylists.remove({ _id: { $in: playlistIds } }), - rundowns.map(async (rd) => removeRundownFromDb(context, new FakeRundownLock(rd._id))), + ...rundowns.map(async (rd) => removeRundownFromDb(context, new FakeRundownLock(rd._id))), ]) } diff --git a/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance-computeCurrentPartIndex.test.ts b/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance-computeCurrentPartIndex.test.ts new file mode 100644 index 00000000000..6b688cf1d04 --- /dev/null +++ b/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance-computeCurrentPartIndex.test.ts @@ -0,0 +1,126 @@ +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { computeCurrentPartIndex } from '../syncChangesToPartInstance.js' + +describe('computeCurrentPartIndex', () => { + function createMockSegmentsAndParts() { + const segments = [ + { + _id: protectString('segment1'), + _rank: 1, + }, + { + _id: protectString('segment1b'), + _rank: 2, + }, + { + _id: protectString('segment2'), + _rank: 3, + }, + { + _id: protectString('segment3'), + _rank: 4, + }, + ] satisfies Partial[] + const parts = [ + { + _id: protectString('part1'), + segmentId: protectString('segment1'), + _rank: 1, + }, + { + _id: protectString('part2'), + segmentId: protectString('segment1'), + _rank: 2, + }, + { + _id: protectString('part3'), + segmentId: protectString('segment2'), + _rank: 1, + }, + { + _id: protectString('part4'), + segmentId: protectString('segment2'), + _rank: 2, + }, + { + _id: protectString('part5'), + segmentId: protectString('segment3'), + _rank: 1, + }, + { + _id: protectString('part6'), + segmentId: protectString('segment3'), + _rank: 2, + }, + { + _id: protectString('part7'), + segmentId: protectString('segment3'), + _rank: 3, + }, + ] satisfies Partial[] + + return { + segments: segments as DBSegment[], + parts: parts as DBPart[], + } + } + + it('match by id', () => { + const { segments, parts } = createMockSegmentsAndParts() + + const index = computeCurrentPartIndex(segments, parts, protectString('part3'), protectString('segment2'), 3) + expect(index).toBe(2) + }) + + it('interpolate by rank', () => { + const { segments, parts } = createMockSegmentsAndParts() + + const index = computeCurrentPartIndex(segments, parts, protectString('partY'), protectString('segment2'), 1.3) + expect(index).toBe(2.5) + }) + + it('before first part in segment', () => { + const { segments, parts } = createMockSegmentsAndParts() + + const index = computeCurrentPartIndex(segments, parts, protectString('partZ'), protectString('segment2'), 0) + expect(index).toBe(1.5) + }) + + it('after last part in segment', () => { + const { segments, parts } = createMockSegmentsAndParts() + + const index = computeCurrentPartIndex(segments, parts, protectString('partW'), protectString('segment2'), 3) + expect(index).toBe(3.5) + }) + + it('segment with no parts', () => { + const { segments, parts } = createMockSegmentsAndParts() + + const index = computeCurrentPartIndex(segments, parts, protectString('partV'), protectString('segment1b'), 1) + expect(index).toBe(1.5) + }) + + it('non-existing segment', () => { + const { segments, parts } = createMockSegmentsAndParts() + + const index = computeCurrentPartIndex(segments, parts, protectString('partU'), protectString('segmentX'), 1) + expect(index).toBeNull() + }) + + it('no parts at all', () => { + const segments: DBSegment[] = [] + const parts: DBPart[] = [] + + const index = computeCurrentPartIndex(segments, parts, protectString('partT'), protectString('segment1'), 1) + expect(index).toBeNull() + }) + + it('before first part', () => { + const { segments, parts } = createMockSegmentsAndParts() + + const index = computeCurrentPartIndex(segments, parts, protectString('partS'), protectString('segment1'), 0) + expect(index).toBe(-0.5) + }) +}) diff --git a/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts b/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts index b663ad3501c..6fd99f48620 100644 --- a/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts +++ b/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts @@ -84,7 +84,7 @@ describe('SyncChangesToPartInstancesWorker', () => { describe('syncChangesToPartInstance', () => { function createMockPlayoutModel(partialModel?: Partial>) { - return mock( + const mockPlayoutModel = mock( { currentPartInstance: null, nextPartInstance: partialModel?.nextPartInstance ?? null, @@ -96,6 +96,19 @@ describe('SyncChangesToPartInstancesWorker', () => { }, mockOptions ) + + Object.defineProperty(mockPlayoutModel, 'playlist', { + get: () => + ({ + tTimers: [ + { index: 1, label: 'Timer 1', mode: null, state: null }, + { index: 2, label: 'Timer 2', mode: null, state: null }, + { index: 3, label: 'Timer 3', mode: null, state: null }, + ], + }) satisfies Partial, + }) + + return mockPlayoutModel } function createMockPlayoutRundownModel(): PlayoutRundownModel { return mock({}, mockOptions) @@ -105,6 +118,9 @@ describe('SyncChangesToPartInstancesWorker', () => { { findPart: jest.fn(() => undefined), getGlobalPieces: jest.fn(() => []), + getAllOrderedParts: jest.fn(() => []), + getOrderedSegments: jest.fn(() => []), + findAdlibPiece: jest.fn(() => undefined), }, mockOptions ) @@ -315,6 +331,11 @@ describe('SyncChangesToPartInstancesWorker', () => { modified: 0, timing: { type: PlaylistTimingType.None }, rundownIdsInOrder: [], + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], } const segmentModel = new PlayoutSegmentModelImpl(segment, [part0]) diff --git a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts index b92cbe77665..cc40fff7157 100644 --- a/packages/job-worker/src/ingest/__tests__/updateNext.test.ts +++ b/packages/job-worker/src/ingest/__tests__/updateNext.test.ts @@ -34,6 +34,11 @@ async function createMockRO(context: MockJobContext): Promise { }, rundownIdsInOrder: [rundownId], + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], }) await context.mockCollections.Rundowns.insertOne({ diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index 47e26f850cb..18c981dc4be 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -29,6 +29,7 @@ import { clone, groupByToMapFunc } from '@sofie-automation/corelib/dist/lib' import { PlaylistLock } from '../jobs/lock.js' import { syncChangesToPartInstances } from './syncChangesToPartInstance.js' import { ensureNextPartIsValid } from './updateNext.js' +import { recalculateTTimerProjections } from '../playout/tTimers.js' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' import { getTranslatedMessage, ServerTranslatedMesssages } from '../notes.js' import _ from 'underscore' @@ -234,6 +235,16 @@ export async function CommitIngestOperation( // update the quickloop in case we did any changes to things involving marker playoutModel.updateQuickLoopState() + // wait for the ingest changes to save + await pSaveIngest + + // do some final playout checks, which may load back some Parts data + // Note: This should trigger a timeline update, one is already queued in the `deferAfterSave` above + await ensureNextPartIsValid(context, playoutModel) + + // Recalculate T-Timer projections after ingest changes + recalculateTTimerProjections(context, playoutModel) + playoutModel.deferAfterSave(() => { // Run in the background, we don't want to hold onto the lock to do this context @@ -248,13 +259,6 @@ export async function CommitIngestOperation( triggerUpdateTimelineAfterIngestData(context, playoutModel.playlistId) }) - // wait for the ingest changes to save - await pSaveIngest - - // do some final playout checks, which may load back some Parts data - // Note: This should trigger a timeline update, one is already queued in the `deferAfterSave` above - await ensureNextPartIsValid(context, playoutModel) - // save the final playout changes await playoutModel.saveAllToDatabase() } finally { @@ -524,7 +528,7 @@ async function updatePartInstancesBasicProperties( ) } - await Promise.all([ps]) + await Promise.all(ps) } /** @@ -613,6 +617,9 @@ export async function updatePlayoutAfterChangingRundownInPlaylist( const shouldUpdateTimeline = await ensureNextPartIsValid(context, playoutModel) + // Recalculate T-Timer projections after playlist changes + recalculateTTimerProjections(context, playoutModel) + if (playoutModel.playlist.activationId || shouldUpdateTimeline) { triggerUpdateTimelineAfterIngestData(context, playoutModel.playlistId) } diff --git a/packages/job-worker/src/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap b/packages/job-worker/src/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap index 8c1b68d4433..a6acb97b01a 100644 --- a/packages/job-worker/src/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap +++ b/packages/job-worker/src/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Test recieved mos ingest payloads mosRoCreate 1`] = ` { @@ -15,6 +15,26 @@ exports[`Test recieved mos ingest payloads mosRoCreate 1`] = ` "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -307,6 +327,26 @@ exports[`Test recieved mos ingest payloads mosRoCreate: replace existing 1`] = ` "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -591,6 +631,26 @@ exports[`Test recieved mos ingest payloads mosRoFullStory: Valid data 1`] = ` "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -896,6 +956,26 @@ exports[`Test recieved mos ingest payloads mosRoReadyToAir: Update ro 1`] = ` "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -1191,6 +1271,26 @@ exports[`Test recieved mos ingest payloads mosRoStatus: Update ro 1`] = ` "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -1484,6 +1584,26 @@ exports[`Test recieved mos ingest payloads mosRoStoryDelete: Remove segment 1`] "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -1745,6 +1865,26 @@ exports[`Test recieved mos ingest payloads mosRoStoryInsert: Into segment 1`] = "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -2051,6 +2191,26 @@ exports[`Test recieved mos ingest payloads mosRoStoryInsert: New segment 1`] = ` "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -2365,6 +2525,26 @@ exports[`Test recieved mos ingest payloads mosRoStoryMove: Move whole segment to "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -2662,6 +2842,26 @@ exports[`Test recieved mos ingest payloads mosRoStoryMove: Within segment 1`] = "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -2959,6 +3159,26 @@ exports[`Test recieved mos ingest payloads mosRoStoryReplace: Same segment 1`] = "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -3255,6 +3475,26 @@ exports[`Test recieved mos ingest payloads mosRoStorySwap: Swap across segments "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -3544,6 +3784,26 @@ exports[`Test recieved mos ingest payloads mosRoStorySwap: Swap across segments2 "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -3865,6 +4125,26 @@ exports[`Test recieved mos ingest payloads mosRoStorySwap: With first in same se "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, @@ -4162,6 +4442,26 @@ exports[`Test recieved mos ingest payloads mosRoStorySwap: Within same segment 1 "5meLdE_m5k28xXw1vtX2JX8mSYQ_", ], "studioId": "mockStudio4", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, diff --git a/packages/job-worker/src/ingest/runOperation.ts b/packages/job-worker/src/ingest/runOperation.ts index 3bdb1bbf0a5..928bf09824a 100644 --- a/packages/job-worker/src/ingest/runOperation.ts +++ b/packages/job-worker/src/ingest/runOperation.ts @@ -14,6 +14,7 @@ import { UserOperationChange, SofieIngestSegment, IngestChangeType, + PlayoutOperationChange, } from '@sofie-automation/blueprints-integration' import { MutableIngestRundownImpl } from '../blueprints/ingest/MutableIngestRundownImpl.js' import { ProcessIngestDataContext } from '../blueprints/context/index.js' @@ -37,7 +38,7 @@ export enum ComputedIngestChangeAction { export interface UpdateIngestRundownChange { ingestRundown: IngestRundownWithSource - changes: NrcsIngestChangeDetails | UserOperationChange + changes: NrcsIngestChangeDetails | UserOperationChange | PlayoutOperationChange } export type UpdateIngestRundownResult = UpdateIngestRundownChange | ComputedIngestChangeAction @@ -277,6 +278,7 @@ async function updateSofieIngestRundown( payload: undefined, userEditStates: {}, rundownSource: nrcsIngestRundown.rundownSource, + playlistExternalId: nrcsIngestRundown.playlistExternalId, } satisfies Complete, false ) diff --git a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts index 6f8352751c5..41de01b1bfa 100644 --- a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts +++ b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts @@ -33,8 +33,9 @@ import { convertNoteToNotification } from '../notifications/util.js' import { PlayoutRundownModel } from '../playout/model/PlayoutRundownModel.js' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { setNextPart } from '../playout/setNext.js' -import { PartId, RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartId, RundownId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import type { WrappedShowStyleBlueprint } from '../blueprints/cache.js' +import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' type PlayStatus = 'previous' | 'current' | 'next' export interface PartInstanceToSync { @@ -129,12 +130,14 @@ export class SyncChangesToPartInstancesWorker { const syncContext = new SyncIngestUpdateToPartInstanceContext( this.#context, + this.#playoutModel, { name: `Update to ${existingPartInstance.partInstance.part.externalId}`, identifier: `rundownId=${existingPartInstance.partInstance.part.rundownId},segmentId=${existingPartInstance.partInstance.part.segmentId}`, }, this.#context.studio, this.#showStyle, + this.#playoutModel.playlist, instanceToSync.playoutRundownModel.rundown, existingPartInstance, proposedPieceInstances, @@ -152,6 +155,11 @@ export class SyncChangesToPartInstancesWorker { newResultData, instanceToSync.playStatus ) + + // Persist t-timer changes + for (const timer of syncContext.changedTTimers) { + this.#playoutModel.updateTTimer(timer) + } } catch (err) { logger.error(`Error in showStyleBlueprint.syncIngestUpdateToPartInstance: ${stringifyError(err)}`) @@ -189,7 +197,7 @@ export class SyncChangesToPartInstancesWorker { } } - collectNewIngestDataToSync( + private collectNewIngestDataToSync( partId: PartId, instanceToSync: PartInstanceToSync, proposedPieceInstances: PieceInstance[] @@ -204,7 +212,18 @@ export class SyncChangesToPartInstancesWorker { if (adLibPiece) referencedAdlibs.push(convertAdLibPieceToBlueprints(adLibPiece)) } + const allModelParts = this.#ingestModel.getAllOrderedParts() + return { + allParts: allModelParts.map((part) => convertPartToBlueprints(part.part)), + currentPartIndex: computeCurrentPartIndex( + this.#ingestModel.getOrderedSegments().map((s) => s.segment), + allModelParts.map((p) => p.part), + partId, + instanceToSync.existingPartInstance.partInstance.segmentId, + instanceToSync.existingPartInstance.partInstance.part._rank + ), + part: instanceToSync.newPart ? convertPartToBlueprints(instanceToSync.newPart) : undefined, pieceInstances: proposedPieceInstances.map(convertPieceInstanceToBlueprints), adLibPieces: @@ -480,3 +499,71 @@ function findLastUnorphanedPartInstanceInSegment( part: previousPart, } } + +/** + * Compute an approximate (possibly non-integer) index of the part within all parts + * This is used to give the blueprints an idea of where the part is within the rundown + * Note: this assumes each part has a unique integer rank, which is what ingest will produce + * @returns The approximate index, or `null` if the part could not be placed + */ +export function computeCurrentPartIndex( + allOrderedSegments: ReadonlyDeep[], + allOrderedParts: ReadonlyDeep[], + partId: PartId, + segmentId: SegmentId, + targetRank: number +): number | null { + // Exact match by part id + const exactIdx = allOrderedParts.findIndex((p) => p._id === partId) + if (exactIdx !== -1) return exactIdx + + // Find the segment object + const segment = allOrderedSegments.find((s) => s._id === segmentId) + if (!segment) return null + + // Prepare parts with their global indices + const partsWithGlobal = allOrderedParts.map((p, globalIndex) => ({ part: p, globalIndex })) + + // Parts in the same segment + const partsInSegment = partsWithGlobal.filter((pg) => pg.part.segmentId === segmentId) + + if (partsInSegment.length === 0) { + // Segment has no parts: place between the previous/next parts by segment order + const segmentRank = segment._rank + + const prev = partsWithGlobal.findLast((pg) => { + const seg = allOrderedSegments.find((s) => s._id === pg.part.segmentId) + return !!seg && seg._rank < segmentRank + }) + + const next = partsWithGlobal.find((pg) => { + const seg = allOrderedSegments.find((s) => s._id === pg.part.segmentId) + return !!seg && seg._rank > segmentRank + }) + + if (prev && next) return (prev.globalIndex + next.globalIndex) / 2 + if (prev) return prev.globalIndex + 0.5 + if (next) return next.globalIndex - 0.5 + + // No parts at all + return null + } + + // There are parts in the segment: decide placement by rank within the segment. + + const nextIdx = partsInSegment.findIndex((pg) => pg.part._rank > targetRank) + if (nextIdx === -1) { + // After last + return partsInSegment[partsInSegment.length - 1].globalIndex + 0.5 + } + + if (nextIdx === 0) { + // Before first + return partsInSegment[0].globalIndex - 0.5 + } + + // Between two adjacent parts: interpolate by their ranks (proportionally) + const prev = partsInSegment[nextIdx - 1] + const next = partsInSegment[nextIdx] + return prev.globalIndex + (next.globalIndex - prev.globalIndex) / 2 +} diff --git a/packages/job-worker/src/ingest/userOperation.ts b/packages/job-worker/src/ingest/userOperation.ts index 640c9d26753..4eefff76640 100644 --- a/packages/job-worker/src/ingest/userOperation.ts +++ b/packages/job-worker/src/ingest/userOperation.ts @@ -1,7 +1,11 @@ -import { UserExecuteChangeOperationProps } from '@sofie-automation/corelib/dist/worker/ingest' +import { + PlayoutExecuteChangeOperationProps, + UserExecuteChangeOperationProps, +} from '@sofie-automation/corelib/dist/worker/ingest' import { JobContext } from '../jobs/index.js' import { UpdateIngestRundownResult, runIngestUpdateOperationBase } from './runOperation.js' import { IngestChangeType } from '@sofie-automation/blueprints-integration' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' export async function handleUserExecuteChangeOperation( context: JobContext, @@ -21,3 +25,23 @@ export async function handleUserExecuteChangeOperation( } satisfies UpdateIngestRundownResult }) } + +export async function handlePlayoutExecuteChangeOperation( + context: JobContext, + data: PlayoutExecuteChangeOperationProps +): Promise { + await runIngestUpdateOperationBase(context, data, async (nrcsIngestObjectCache) => { + const nrcsIngestRundown = nrcsIngestObjectCache.fetchRundown() + if (!nrcsIngestRundown) throw new Error(`Rundown "${data.rundownExternalId}" not found`) + + return { + ingestRundown: nrcsIngestRundown, + changes: { + source: IngestChangeType.Playout, + currentSegmentId: unprotectString(data.segmentId), + currentPartId: unprotectString(data.partId), + operation: data.operation, + }, + } satisfies UpdateIngestRundownResult + }) +} diff --git a/packages/job-worker/src/jobs/showStyle.ts b/packages/job-worker/src/jobs/showStyle.ts index ef99237d269..0a3b3b5cabe 100644 --- a/packages/job-worker/src/jobs/showStyle.ts +++ b/packages/job-worker/src/jobs/showStyle.ts @@ -16,11 +16,10 @@ export interface ProcessedShowStyleVariant extends Omit pre-flattened */ -export interface ProcessedShowStyleBase - extends Omit< - DBShowStyleBase, - 'sourceLayersWithOverrides' | 'outputLayersWithOverrides' | 'blueprintConfigWithOverrides' - > { +export interface ProcessedShowStyleBase extends Omit< + DBShowStyleBase, + 'sourceLayersWithOverrides' | 'outputLayersWithOverrides' | 'blueprintConfigWithOverrides' +> { sourceLayers: SourceLayers outputLayers: OutputLayers blueprintConfig: IBlueprintConfig diff --git a/packages/job-worker/src/jobs/studio.ts b/packages/job-worker/src/jobs/studio.ts index 16368fec1a5..ed59d6920ab 100644 --- a/packages/job-worker/src/jobs/studio.ts +++ b/packages/job-worker/src/jobs/studio.ts @@ -10,16 +10,15 @@ import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settin /** * A lightly processed version of DBStudio, with any ObjectWithOverrides pre-flattened */ -export interface JobStudio - extends Omit< - DBStudio, - | 'mappingsWithOverrides' - | 'blueprintConfigWithOverrides' - | 'settingsWithOverrides' - | 'routeSetsWithOverrides' - | 'routeSetExclusivityGroupsWithOverrides' - | 'packageContainersWithOverrides' - > { +export interface JobStudio extends Omit< + DBStudio, + | 'mappingsWithOverrides' + | 'blueprintConfigWithOverrides' + | 'settingsWithOverrides' + | 'routeSetsWithOverrides' + | 'routeSetExclusivityGroupsWithOverrides' + | 'packageContainersWithOverrides' +> { /** Mappings between the physical devices / outputs and logical ones */ mappings: MappingsExt diff --git a/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap b/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap index d99635086b3..964548f28ef 100644 --- a/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap +++ b/packages/job-worker/src/playout/__tests__/__snapshots__/playout.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Playout API Basic rundown control 1`] = ` [ @@ -77,6 +77,26 @@ exports[`Playout API Basic rundown control 4`] = ` "resetTime": 0, "rundownIdsInOrder": [], "studioId": "mockStudio0", + "tTimers": [ + { + "index": 1, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 2, + "label": "", + "mode": null, + "state": null, + }, + { + "index": 3, + "label": "", + "mode": null, + "state": null, + }, + ], "timing": { "type": "none", }, diff --git a/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap b/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap index 69c95348f78..f35d3d96bf6 100644 --- a/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap +++ b/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap @@ -1,6 +1,40 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing -exports[`Timeline Adlib pieces Current part with preroll 1`] = ` +exports[`Timeline Multi-gateway mode In transitions inTransition with existing infinites 1`] = ` +[ + { + "_id": "mockStudio0", + "generated": 12345, + "generationVersions": { + "blueprintId": "studioBlueprint0", + "blueprintVersion": "0.0.0", + "core": "0.0.0-test", + "studio": "asdf", + }, + "timelineBlob": "[]", + "timelineHash": "randomId9011", + }, +] +`; + +exports[`Timeline Multi-gateway mode Infinite Pieces Infinite Piece has stable timing across timeline regenerations after onPlayoutPlaybackChanged 1`] = ` +[ + { + "_id": "mockStudio0", + "generated": 12345, + "generationVersions": { + "blueprintId": "studioBlueprint0", + "blueprintVersion": "0.0.0", + "core": "0.0.0-test", + "studio": "asdf", + }, + "timelineBlob": "[]", + "timelineHash": "randomId9010", + }, +] +`; + +exports[`Timeline Single-gateway mode Adlib pieces Current part with preroll 1`] = ` [ { "_id": "mockStudio0", @@ -17,7 +51,7 @@ exports[`Timeline Adlib pieces Current part with preroll 1`] = ` ] `; -exports[`Timeline Adlib pieces Current part with preroll and adlib preroll 1`] = ` +exports[`Timeline Single-gateway mode Adlib pieces Current part with preroll and adlib preroll 1`] = ` [ { "_id": "mockStudio0", @@ -34,7 +68,7 @@ exports[`Timeline Adlib pieces Current part with preroll and adlib preroll 1`] = ] `; -exports[`Timeline Basic rundown 1`] = ` +exports[`Timeline Single-gateway mode Basic rundown 1`] = ` [ { "_id": "mockStudio0", @@ -51,7 +85,7 @@ exports[`Timeline Basic rundown 1`] = ` ] `; -exports[`Timeline Basic rundown 2`] = ` +exports[`Timeline Single-gateway mode Basic rundown 2`] = ` [ { "_id": "mockStudio0", @@ -68,7 +102,7 @@ exports[`Timeline Basic rundown 2`] = ` ] `; -exports[`Timeline In transitions Basic inTransition 1`] = ` +exports[`Timeline Single-gateway mode In transitions Basic inTransition 1`] = ` [ { "_id": "mockStudio0", @@ -85,7 +119,7 @@ exports[`Timeline In transitions Basic inTransition 1`] = ` ] `; -exports[`Timeline In transitions Basic inTransition with contentDelay + preroll 1`] = ` +exports[`Timeline Single-gateway mode In transitions Basic inTransition with contentDelay + preroll 1`] = ` [ { "_id": "mockStudio0", @@ -102,7 +136,7 @@ exports[`Timeline In transitions Basic inTransition with contentDelay + preroll ] `; -exports[`Timeline In transitions Basic inTransition with contentDelay 1`] = ` +exports[`Timeline Single-gateway mode In transitions Basic inTransition with contentDelay 1`] = ` [ { "_id": "mockStudio0", @@ -119,7 +153,7 @@ exports[`Timeline In transitions Basic inTransition with contentDelay 1`] = ` ] `; -exports[`Timeline In transitions Basic inTransition with planned pieces 1`] = ` +exports[`Timeline Single-gateway mode In transitions Basic inTransition with planned pieces 1`] = ` [ { "_id": "mockStudio0", @@ -136,7 +170,7 @@ exports[`Timeline In transitions Basic inTransition with planned pieces 1`] = ` ] `; -exports[`Timeline In transitions Preroll 1`] = ` +exports[`Timeline Single-gateway mode In transitions Preroll 1`] = ` [ { "_id": "mockStudio0", @@ -153,7 +187,7 @@ exports[`Timeline In transitions Preroll 1`] = ` ] `; -exports[`Timeline In transitions inTransition disabled 1`] = ` +exports[`Timeline Single-gateway mode In transitions inTransition disabled 1`] = ` [ { "_id": "mockStudio0", @@ -170,7 +204,7 @@ exports[`Timeline In transitions inTransition disabled 1`] = ` ] `; -exports[`Timeline In transitions inTransition is disabled during hold 1`] = ` +exports[`Timeline Single-gateway mode In transitions inTransition is disabled during hold 1`] = ` [ { "_id": "mockStudio0", @@ -187,7 +221,7 @@ exports[`Timeline In transitions inTransition is disabled during hold 1`] = ` ] `; -exports[`Timeline In transitions inTransition with existing infinites 1`] = ` +exports[`Timeline Single-gateway mode In transitions inTransition with existing infinites 1`] = ` [ { "_id": "mockStudio0", @@ -204,7 +238,7 @@ exports[`Timeline In transitions inTransition with existing infinites 1`] = ` ] `; -exports[`Timeline In transitions inTransition with new infinite 1`] = ` +exports[`Timeline Single-gateway mode In transitions inTransition with new infinite 1`] = ` [ { "_id": "mockStudio0", @@ -221,7 +255,7 @@ exports[`Timeline In transitions inTransition with new infinite 1`] = ` ] `; -exports[`Timeline Infinite Pieces Infinite Piece has stable timing across timeline regenerations with/without plannedStartedPlayback 1`] = ` +exports[`Timeline Single-gateway mode Infinite Pieces Infinite Piece has stable timing across timeline regenerations with/without plannedStartedPlayback 1`] = ` [ { "_id": "mockStudio0", @@ -238,7 +272,7 @@ exports[`Timeline Infinite Pieces Infinite Piece has stable timing across timeli ] `; -exports[`Timeline Out transitions Basic outTransition 1`] = ` +exports[`Timeline Single-gateway mode Out transitions Basic outTransition 1`] = ` [ { "_id": "mockStudio0", @@ -255,7 +289,7 @@ exports[`Timeline Out transitions Basic outTransition 1`] = ` ] `; -exports[`Timeline Out transitions outTransition + inTransition 1`] = ` +exports[`Timeline Single-gateway mode Out transitions outTransition + inTransition 1`] = ` [ { "_id": "mockStudio0", @@ -272,7 +306,7 @@ exports[`Timeline Out transitions outTransition + inTransition 1`] = ` ] `; -exports[`Timeline Out transitions outTransition + preroll (2) 1`] = ` +exports[`Timeline Single-gateway mode Out transitions outTransition + preroll (2) 1`] = ` [ { "_id": "mockStudio0", @@ -289,7 +323,7 @@ exports[`Timeline Out transitions outTransition + preroll (2) 1`] = ` ] `; -exports[`Timeline Out transitions outTransition + preroll 1`] = ` +exports[`Timeline Single-gateway mode Out transitions outTransition + preroll 1`] = ` [ { "_id": "mockStudio0", @@ -306,7 +340,7 @@ exports[`Timeline Out transitions outTransition + preroll 1`] = ` ] `; -exports[`Timeline Out transitions outTransition is disabled during hold 1`] = ` +exports[`Timeline Single-gateway mode Out transitions outTransition is disabled during hold 1`] = ` [ { "_id": "mockStudio0", diff --git a/packages/job-worker/src/playout/__tests__/lib.ts b/packages/job-worker/src/playout/__tests__/lib.ts index cc6073d5817..f48d1d3ccb7 100644 --- a/packages/job-worker/src/playout/__tests__/lib.ts +++ b/packages/job-worker/src/playout/__tests__/lib.ts @@ -16,13 +16,13 @@ export async function getSelectedPartInstances( }> { const [currentPartInstance, nextPartInstance, previousPartInstance] = await Promise.all([ playlist.currentPartInfo - ? await context.directCollections.PartInstances.findOne(playlist.currentPartInfo.partInstanceId) + ? context.directCollections.PartInstances.findOne(playlist.currentPartInfo.partInstanceId) : null, playlist.nextPartInfo - ? await context.directCollections.PartInstances.findOne(playlist.nextPartInfo.partInstanceId) + ? context.directCollections.PartInstances.findOne(playlist.nextPartInfo.partInstanceId) : null, playlist.previousPartInfo - ? await context.directCollections.PartInstances.findOne(playlist.previousPartInfo.partInstanceId) + ? context.directCollections.PartInstances.findOne(playlist.previousPartInfo.partInstanceId) : null, ]) diff --git a/packages/job-worker/src/playout/__tests__/tTimers.test.ts b/packages/job-worker/src/playout/__tests__/tTimers.test.ts new file mode 100644 index 00000000000..d323e939abd --- /dev/null +++ b/packages/job-worker/src/playout/__tests__/tTimers.test.ts @@ -0,0 +1,663 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { useFakeCurrentTime, useRealCurrentTime } from '../../__mocks__/time.js' +import { + validateTTimerIndex, + pauseTTimer, + resumeTTimer, + restartTTimer, + createCountdownTTimer, + createFreeRunTTimer, + calculateNextTimeOfDayTarget, + createTimeOfDayTTimer, +} from '../tTimers.js' +import type { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' + +describe('tTimers utils', () => { + beforeEach(() => { + useFakeCurrentTime(10000) // Set a fixed time for tests + }) + + afterEach(() => { + useRealCurrentTime() + }) + + describe('validateTTimerIndex', () => { + it('should accept valid indices 1, 2, 3', () => { + expect(() => validateTTimerIndex(1)).not.toThrow() + expect(() => validateTTimerIndex(2)).not.toThrow() + expect(() => validateTTimerIndex(3)).not.toThrow() + }) + + it('should reject index 0', () => { + expect(() => validateTTimerIndex(0)).toThrow('T-timer index out of range: 0') + }) + + it('should reject index 4', () => { + expect(() => validateTTimerIndex(4)).toThrow('T-timer index out of range: 4') + }) + + it('should reject negative indices', () => { + expect(() => validateTTimerIndex(-1)).toThrow('T-timer index out of range: -1') + }) + + it('should reject NaN', () => { + expect(() => validateTTimerIndex(NaN)).toThrow('T-timer index out of range: NaN') + }) + + it('should reject fractional indices', () => { + expect(() => validateTTimerIndex(1.5)).toThrow('T-timer index out of range: 1.5') + expect(() => validateTTimerIndex(2.1)).toThrow('T-timer index out of range: 2.1') + }) + }) + + describe('pauseTTimer', () => { + it('should pause a running countdown timer', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: false, zeroTime: 70000 }, // 60 seconds from now + } + + const result = pauseTTimer(timer) + + expect(result).toEqual({ + index: 1, + label: 'Test', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: true, duration: 60000 }, // Captured remaining time + }) + }) + + it('should pause a running freeRun timer', () => { + const timer: RundownTTimer = { + index: 2, + label: 'Test', + mode: { + type: 'freeRun', + }, + state: { paused: false, zeroTime: 5000 }, // Started 5 seconds ago + } + + const result = pauseTTimer(timer) + + expect(result).toEqual({ + index: 2, + label: 'Test', + mode: { + type: 'freeRun', + }, + state: { paused: true, duration: -5000 }, // Elapsed time (negative for counting up) + }) + }) + + it('should return unchanged countdown timer if already paused', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: true, duration: 30000 }, // already paused + } + + const result = pauseTTimer(timer) + + expect(result).toBe(timer) // same reference, unchanged + }) + + it('should return unchanged freeRun timer if already paused', () => { + const timer: RundownTTimer = { + index: 2, + label: 'Test', + mode: { + type: 'freeRun', + }, + state: { paused: true, duration: 5000 }, // already paused + } + + const result = pauseTTimer(timer) + + expect(result).toBe(timer) // same reference, unchanged + }) + + it('should return null for timer with no mode', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: null, + state: null, + } + + expect(pauseTTimer(timer)).toBeNull() + }) + }) + + describe('resumeTTimer', () => { + it('should resume a paused countdown timer', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: true, duration: 30000 }, // 30 seconds remaining + } + + const result = resumeTTimer(timer) + + expect(result).toEqual({ + index: 1, + label: 'Test', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: false, zeroTime: 40000 }, // now (10000) + duration (30000) + }) + }) + + it('should resume a paused freeRun timer', () => { + const timer: RundownTTimer = { + index: 2, + label: 'Test', + mode: { + type: 'freeRun', + }, + state: { paused: true, duration: -5000 }, // 5 seconds elapsed + } + + const result = resumeTTimer(timer) + + expect(result).toEqual({ + index: 2, + label: 'Test', + mode: { + type: 'freeRun', + }, + state: { paused: false, zeroTime: 5000 }, // now (10000) + duration (-5000) + }) + }) + + it('should return countdown timer unchanged if already running', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: false, zeroTime: 70000 }, // already running + } + + const result = resumeTTimer(timer) + + expect(result).toBe(timer) // same reference + }) + + it('should return freeRun timer unchanged if already running', () => { + const timer: RundownTTimer = { + index: 2, + label: 'Test', + mode: { + type: 'freeRun', + }, + state: { paused: false, zeroTime: 5000 }, // already running + } + + const result = resumeTTimer(timer) + + expect(result).toBe(timer) // same reference + }) + + it('should return null for timer with no mode', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: null, + state: null, + } + + expect(resumeTTimer(timer)).toBeNull() + }) + }) + + describe('restartTTimer', () => { + it('should restart a running countdown timer', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: false, zeroTime: 40000 }, // Partway through + } + + const result = restartTTimer(timer) + + expect(result).toEqual({ + index: 1, + label: 'Test', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: false, zeroTime: 70000 }, // now (10000) + duration (60000) + }) + }) + + it('should restart a paused countdown timer (stays paused)', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: false, + }, + state: { paused: true, duration: 15000 }, // Paused with time remaining + } + + const result = restartTTimer(timer) + + expect(result).toEqual({ + index: 1, + label: 'Test', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: false, + }, + state: { paused: true, duration: 60000 }, // Reset to full duration, still paused + }) + }) + + it('should return null for freeRun timer', () => { + const timer: RundownTTimer = { + index: 2, + label: 'Test', + mode: { + type: 'freeRun', + }, + state: { paused: false, zeroTime: 5000 }, + } + + expect(restartTTimer(timer)).toBeNull() + }) + + it('should return null for timer with no mode', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: null, + state: null, + } + + expect(restartTTimer(timer)).toBeNull() + }) + }) + + describe('createCountdownTTimer', () => { + it('should create a running countdown timer', () => { + const result = createCountdownTTimer(60000, { + stopAtZero: true, + startPaused: false, + }) + + expect(result).toEqual({ + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: false, zeroTime: 70000 }, // now (10000) + duration (60000) + }) + }) + + it('should create a paused countdown timer', () => { + const result = createCountdownTTimer(30000, { + stopAtZero: false, + startPaused: true, + }) + + expect(result).toEqual({ + mode: { + type: 'countdown', + duration: 30000, + stopAtZero: false, + }, + state: { paused: true, duration: 30000 }, + }) + }) + + it('should throw for zero duration', () => { + expect(() => + createCountdownTTimer(0, { + stopAtZero: true, + startPaused: false, + }) + ).toThrow('Duration must be greater than zero') + }) + + it('should throw for negative duration', () => { + expect(() => + createCountdownTTimer(-1000, { + stopAtZero: true, + startPaused: false, + }) + ).toThrow('Duration must be greater than zero') + }) + }) + + describe('createFreeRunTTimer', () => { + it('should create a running freeRun timer', () => { + const result = createFreeRunTTimer({ startPaused: false }) + + expect(result).toEqual({ + mode: { + type: 'freeRun', + }, + state: { paused: false, zeroTime: 10000 }, // now + }) + }) + + it('should create a paused freeRun timer', () => { + const result = createFreeRunTTimer({ startPaused: true }) + + expect(result).toEqual({ + mode: { + type: 'freeRun', + }, + state: { paused: true, duration: 0 }, + }) + }) + }) + + describe('calculateNextTimeOfDayTarget', () => { + // Mock date to 2026-01-19 10:00:00 UTC for predictable tests + const MOCK_DATE = new Date('2026-01-19T10:00:00Z').getTime() + + beforeEach(() => { + jest.useFakeTimers() + jest.setSystemTime(MOCK_DATE) + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should return number input unchanged (unix timestamp)', () => { + const timestamp = 1737331200000 // Some future timestamp + expect(calculateNextTimeOfDayTarget(timestamp)).toBe(timestamp) + }) + + it('should return null for null/undefined/empty input', () => { + expect(calculateNextTimeOfDayTarget('' as string)).toBeNull() + expect(calculateNextTimeOfDayTarget(' ')).toBeNull() + }) + + // 24-hour time formats + it('should parse 24-hour time HH:mm', () => { + const result = calculateNextTimeOfDayTarget('13:34') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T13:34:00.000Z') + }) + + it('should parse 24-hour time H:mm (single digit hour)', () => { + const result = calculateNextTimeOfDayTarget('9:05') + expect(result).not.toBeNull() + // 9:05 is in the past (before 10:00), so chrono bumps to tomorrow + expect(new Date(result!).toISOString()).toBe('2026-01-20T09:05:00.000Z') + }) + + it('should parse 24-hour time with seconds HH:mm:ss', () => { + const result = calculateNextTimeOfDayTarget('14:30:45') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T14:30:45.000Z') + }) + + // 12-hour time formats + it('should parse 12-hour time with pm', () => { + const result = calculateNextTimeOfDayTarget('5:13pm') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T17:13:00.000Z') + }) + + it('should parse 12-hour time with PM (uppercase)', () => { + const result = calculateNextTimeOfDayTarget('5:13PM') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T17:13:00.000Z') + }) + + it('should parse 12-hour time with am', () => { + const result = calculateNextTimeOfDayTarget('9:30am') + expect(result).not.toBeNull() + // 9:30am is in the past (before 10:00), so chrono bumps to tomorrow + expect(new Date(result!).toISOString()).toBe('2026-01-20T09:30:00.000Z') + }) + + it('should parse 12-hour time with space before am/pm', () => { + const result = calculateNextTimeOfDayTarget('3:45 pm') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T15:45:00.000Z') + }) + + it('should parse 12-hour time with seconds', () => { + const result = calculateNextTimeOfDayTarget('11:30:15pm') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T23:30:15.000Z') + }) + + // Date + time formats + it('should parse date with time (slash separator)', () => { + const result = calculateNextTimeOfDayTarget('1/19/2026 15:43') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T15:43:00.000Z') + }) + + it('should parse date with time and seconds', () => { + const result = calculateNextTimeOfDayTarget('1/19/2026 15:43:30') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T15:43:30.000Z') + }) + + it('should parse date with 12-hour time', () => { + const result = calculateNextTimeOfDayTarget('1/19/2026 3:43pm') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T15:43:00.000Z') + }) + + // ISO 8601 format + it('should parse ISO 8601 format', () => { + const result = calculateNextTimeOfDayTarget('2026-01-19T15:43:00') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T15:43:00.000Z') + }) + + it('should parse ISO 8601 with timezone', () => { + const result = calculateNextTimeOfDayTarget('2026-01-19T15:43:00+01:00') + expect(result).not.toBeNull() + // +01:00 means the time is 1 hour ahead of UTC, so 15:43 +01:00 = 14:43 UTC + expect(new Date(result!).toISOString()).toBe('2026-01-19T14:43:00.000Z') + }) + + // Natural language formats (chrono-node strength) + it('should parse natural language date', () => { + const result = calculateNextTimeOfDayTarget('January 19, 2026 at 3:30pm') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T15:30:00.000Z') + }) + + it('should parse "noon"', () => { + const result = calculateNextTimeOfDayTarget('noon') + expect(result).not.toBeNull() + expect(new Date(result!).toISOString()).toBe('2026-01-19T12:00:00.000Z') + }) + + it('should parse "midnight"', () => { + const result = calculateNextTimeOfDayTarget('midnight') + expect(result).not.toBeNull() + // Midnight is in the past (before 10:00), so chrono bumps to tomorrow + expect(new Date(result!).toISOString()).toBe('2026-01-20T00:00:00.000Z') + }) + + // Edge cases + it('should return null for invalid time string', () => { + expect(calculateNextTimeOfDayTarget('not a time')).toBeNull() + }) + + it('should return null for gibberish', () => { + expect(calculateNextTimeOfDayTarget('asdfghjkl')).toBeNull() + }) + }) + + describe('createTimeOfDayTTimer', () => { + // Mock date to 2026-01-19 10:00:00 UTC for predictable tests + const MOCK_DATE = new Date('2026-01-19T10:00:00Z').getTime() + + beforeEach(() => { + jest.useFakeTimers() + jest.setSystemTime(MOCK_DATE) + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should create a timeOfDay timer with valid time string', () => { + const result = createTimeOfDayTTimer('15:30', { stopAtZero: true }) + + expect(result).toEqual({ + mode: { + type: 'timeOfDay', + stopAtZero: true, + targetRaw: '15:30', + }, + state: { + paused: false, + zeroTime: expect.any(Number), // Parsed target time + }, + }) + }) + + it('should create a timeOfDay timer with numeric timestamp', () => { + const timestamp = 1737331200000 + const result = createTimeOfDayTTimer(timestamp, { stopAtZero: false }) + + expect(result).toEqual({ + mode: { + type: 'timeOfDay', + targetRaw: timestamp, + stopAtZero: false, + }, + state: { + paused: false, + zeroTime: timestamp, + }, + }) + }) + + it('should throw for invalid time string', () => { + expect(() => createTimeOfDayTTimer('invalid', { stopAtZero: true })).toThrow( + 'Unable to parse target time for timeOfDay T-timer' + ) + }) + + it('should throw for empty string', () => { + expect(() => createTimeOfDayTTimer('', { stopAtZero: true })).toThrow( + 'Unable to parse target time for timeOfDay T-timer' + ) + }) + }) + + describe('restartTTimer with timeOfDay', () => { + // Mock date to 2026-01-19 10:00:00 UTC for predictable tests + const MOCK_DATE = new Date('2026-01-19T10:00:00Z').getTime() + + beforeEach(() => { + jest.useFakeTimers() + jest.setSystemTime(MOCK_DATE) + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should restart a timeOfDay timer with valid targetRaw', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: { + type: 'timeOfDay', + targetRaw: '15:30', + stopAtZero: true, + }, + state: { paused: false, zeroTime: 1737300000000 }, + } + + const result = restartTTimer(timer) + + expect(result).not.toBeNull() + expect(result?.mode).toEqual(timer.mode) + expect(result?.state).toEqual({ + paused: false, + zeroTime: expect.any(Number), // new target time + }) + if (!result || !result.state || result.state.paused) { + throw new Error('Expected running timeOfDay timer state') + } + expect(result.state.zeroTime).toBeGreaterThan(1737300000000) + }) + + it('should return null for timeOfDay timer with invalid targetRaw', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: { + type: 'timeOfDay', + targetRaw: 'invalid', + stopAtZero: true, + }, + state: { paused: false, zeroTime: 1737300000000 }, + } + + const result = restartTTimer(timer) + + expect(result).toBeNull() + }) + + it('should return null for timeOfDay timer with unix timestamp', () => { + const timer: RundownTTimer = { + index: 1, + label: 'Test', + mode: { + type: 'timeOfDay', + targetRaw: 1737300000000, + stopAtZero: true, + }, + state: { paused: false, zeroTime: 1737300000000 }, + } + + const result = restartTTimer(timer) + + expect(result).toBeNull() + }) + }) +}) diff --git a/packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts b/packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts new file mode 100644 index 00000000000..6704e8255ed --- /dev/null +++ b/packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts @@ -0,0 +1,211 @@ +import { setupDefaultJobEnvironment, MockJobContext } from '../../__mocks__/context.js' +import { handleRecalculateTTimerProjections } from '../tTimersJobs.js' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { literal } from '@sofie-automation/corelib/dist/lib' + +describe('tTimersJobs', () => { + let context: MockJobContext + + beforeEach(() => { + context = setupDefaultJobEnvironment() + }) + + describe('handleRecalculateTTimerProjections', () => { + it('should handle studio with active playlists', async () => { + // Create an active playlist + const playlistId = protectString('playlist1') + + await context.directCollections.RundownPlaylists.insertOne( + literal({ + _id: playlistId, + externalId: 'test', + studioId: context.studioId, + name: 'Test Playlist', + created: 0, + modified: 0, + currentPartInfo: null, + nextPartInfo: null, + previousPartInfo: null, + rundownIdsInOrder: [], + timing: { + type: 'none' as any, + }, + activationId: protectString('activation1'), + rehearsal: false, + holdState: undefined, + tTimers: [ + { + index: 1, + label: 'Timer 1', + mode: null, + state: null, + }, + { + index: 2, + label: 'Timer 2', + mode: null, + state: null, + }, + { + index: 3, + label: 'Timer 3', + mode: null, + state: null, + }, + ], + }) + ) + + // Should complete without errors + await expect(handleRecalculateTTimerProjections(context)).resolves.toBeUndefined() + }) + + it('should handle studio with no active playlists', async () => { + // Create an inactive playlist + const playlistId = protectString('playlist1') + + await context.directCollections.RundownPlaylists.insertOne( + literal({ + _id: playlistId, + externalId: 'test', + studioId: context.studioId, + name: 'Inactive Playlist', + created: 0, + modified: 0, + currentPartInfo: null, + nextPartInfo: null, + previousPartInfo: null, + rundownIdsInOrder: [], + timing: { + type: 'none' as any, + }, + activationId: undefined, // Not active + rehearsal: false, + holdState: undefined, + tTimers: [ + { + index: 1, + label: 'Timer 1', + mode: null, + state: null, + }, + { + index: 2, + label: 'Timer 2', + mode: null, + state: null, + }, + { + index: 3, + label: 'Timer 3', + mode: null, + state: null, + }, + ], + }) + ) + + // Should complete without errors (just does nothing) + await expect(handleRecalculateTTimerProjections(context)).resolves.toBeUndefined() + }) + + it('should handle multiple active playlists', async () => { + // Create multiple active playlists + const playlistId1 = protectString('playlist1') + const playlistId2 = protectString('playlist2') + + await context.directCollections.RundownPlaylists.insertOne( + literal({ + _id: playlistId1, + externalId: 'test1', + studioId: context.studioId, + name: 'Active Playlist 1', + created: 0, + modified: 0, + currentPartInfo: null, + nextPartInfo: null, + previousPartInfo: null, + rundownIdsInOrder: [], + timing: { + type: 'none' as any, + }, + activationId: protectString('activation1'), + rehearsal: false, + holdState: undefined, + tTimers: [ + { + index: 1, + label: 'Timer 1', + mode: null, + state: null, + }, + { + index: 2, + label: 'Timer 2', + mode: null, + state: null, + }, + { + index: 3, + label: 'Timer 3', + mode: null, + state: null, + }, + ], + }) + ) + + await context.directCollections.RundownPlaylists.insertOne( + literal({ + _id: playlistId2, + externalId: 'test2', + studioId: context.studioId, + name: 'Active Playlist 2', + created: 0, + modified: 0, + currentPartInfo: null, + nextPartInfo: null, + previousPartInfo: null, + rundownIdsInOrder: [], + timing: { + type: 'none' as any, + }, + activationId: protectString('activation2'), + rehearsal: false, + holdState: undefined, + tTimers: [ + { + index: 1, + label: 'Timer 1', + mode: null, + state: null, + }, + { + index: 2, + label: 'Timer 2', + mode: null, + state: null, + }, + { + index: 3, + label: 'Timer 3', + mode: null, + state: null, + }, + ], + }) + ) + + // Should complete without errors, processing both playlists + await expect(handleRecalculateTTimerProjections(context)).resolves.toBeUndefined() + }) + + it('should handle playlist deleted between query and lock', async () => { + // This test is harder to set up properly, but the function should handle it + // by checking if playlist exists after acquiring lock + await expect(handleRecalculateTTimerProjections(context)).resolves.toBeUndefined() + }) + }) +}) diff --git a/packages/job-worker/src/playout/__tests__/timeline.test.ts b/packages/job-worker/src/playout/__tests__/timeline.test.ts index 340c55ef11a..6f52a1529a8 100644 --- a/packages/job-worker/src/playout/__tests__/timeline.test.ts +++ b/packages/job-worker/src/playout/__tests__/timeline.test.ts @@ -67,6 +67,7 @@ import { PlayoutPartInstanceModel } from '../model/PlayoutPartInstanceModel.js' import { PlayoutPartInstanceModelImpl } from '../model/implementation/PlayoutPartInstanceModelImpl.js' import { mock } from 'jest-mock-extended' import { QuickLoopService } from '../model/services/QuickLoopService.js' +import { getCurrentTime } from '../../lib/time.js' /** * An object used to represent the simplified timeline structure. @@ -266,7 +267,7 @@ function checkTimingsRaw( } } -/** Perform a take and check the selected part ids are as expected */ +/** Perform a take and check the selected part ids are as expected. Wait for 1500ms before doing a take. */ async function doTakePart( context: MockJobContext, playlistId: RundownPlaylistId, @@ -458,670 +459,342 @@ interface SelectedPartInstances { } describe('Timeline', () => { - let context: MockJobContext - let showStyle: ReadonlyDeep - beforeEach(async () => { - restartRandomId() + describe('Single-gateway mode', () => { + let context: MockJobContext + let showStyle: ReadonlyDeep + beforeEach(async () => { + restartRandomId() - context = setupDefaultJobEnvironment() + context = setupDefaultJobEnvironment() - useFakeCurrentTime() + useFakeCurrentTime() - showStyle = await setupMockShowStyleCompound(context) + showStyle = await setupMockShowStyleCompound(context) - // Ignore calls to queueEventJob, they are expected - context.queueEventJob = async () => Promise.resolve() - }) - afterEach(() => { - useRealCurrentTime() - }) - test('Basic rundown', async () => { - await setupMockPeripheralDevice( - context, - PeripheralDeviceCategory.PLAYOUT, - PeripheralDeviceType.PLAYOUT, - PERIPHERAL_SUBTYPE_PROCESS - ) - - const { rundownId: rundownId0, playlistId: playlistId0 } = await setupDefaultRundownPlaylist(context) - expect(rundownId0).toBeTruthy() - expect(playlistId0).toBeTruthy() - - const getRundown0 = async () => { - return (await context.directCollections.Rundowns.findOne(rundownId0)) as DBRundown - } - const getPlaylist0 = async () => { - const playlist = (await context.directCollections.RundownPlaylists.findOne( - playlistId0 - )) as DBRundownPlaylist - playlist.activationId = playlist.activationId ?? undefined - return playlist - } - - await expect(getRundown0()).resolves.toBeTruthy() - await expect(getPlaylist0()).resolves.toBeTruthy() - - const parts = await context.directCollections.Parts.findFetch({ rundownId: rundownId0 }) - - await expect(getPlaylist0()).resolves.toMatchObject({ - activationId: undefined, - rehearsal: false, + // Ignore calls to queueEventJob, they are expected + context.queueEventJob = async () => Promise.resolve() }) - - { - // Prepare and activate in rehersal: - await handleActivateRundownPlaylist(context, { playlistId: playlistId0, rehearsal: false }) - const { currentPartInstance, nextPartInstance } = await getSelectedPartInstances( + afterEach(() => { + useRealCurrentTime() + }) + test('Basic rundown', async () => { + await setupMockPeripheralDevice( context, - await getPlaylist0() + PeripheralDeviceCategory.PLAYOUT, + PeripheralDeviceType.PLAYOUT, + PERIPHERAL_SUBTYPE_PROCESS ) - expect(currentPartInstance).toBeFalsy() - expect(nextPartInstance).toBeTruthy() - expect(nextPartInstance!.part._id).toEqual(parts[0]._id) - await expect(getPlaylist0()).resolves.toMatchObject({ - activationId: expect.stringMatching(/^randomId/), - rehearsal: false, - currentPartInfo: null, - // nextPartInstanceId: parts[0]._id, - }) - } - { - // Take the first Part: - await handleTakeNextPart(context, { playlistId: playlistId0, fromPartInstanceId: null }) - const { currentPartInstance, nextPartInstance } = await getSelectedPartInstances( - context, - await getPlaylist0() - ) - expect(currentPartInstance).toBeTruthy() - expect(nextPartInstance).toBeTruthy() - expect(currentPartInstance!.part._id).toEqual(parts[0]._id) - expect(nextPartInstance!.part._id).toEqual(parts[1]._id) - // expect(getPlaylist0()).toMatchObject({ - // currentPartInstanceId: parts[0]._id, - // nextPartInstanceId: parts[1]._id, - // }) - } + const { rundownId: rundownId0, playlistId: playlistId0 } = await setupDefaultRundownPlaylist(context) + expect(rundownId0).toBeTruthy() + expect(playlistId0).toBeTruthy() - await runJobWithPlayoutModel(context, { playlistId: playlistId0 }, null, async (playoutModel) => { - await updateTimeline(context, playoutModel) - }) + const getRundown0 = async () => { + return (await context.directCollections.Rundowns.findOne(rundownId0)) as DBRundown + } + const getPlaylist0 = async () => { + const playlist = (await context.directCollections.RundownPlaylists.findOne( + playlistId0 + )) as DBRundownPlaylist + playlist.activationId = playlist.activationId ?? undefined + return playlist + } - expect(fixSnapshot(await context.directCollections.Timelines.findFetch())).toMatchSnapshot() + await expect(getRundown0()).resolves.toBeTruthy() + await expect(getPlaylist0()).resolves.toBeTruthy() + + const parts = await context.directCollections.Parts.findFetch({ rundownId: rundownId0 }) - { - // Deactivate rundown: - await handleDeactivateRundownPlaylist(context, { playlistId: playlistId0 }) await expect(getPlaylist0()).resolves.toMatchObject({ activationId: undefined, - currentPartInfo: null, - nextPartInfo: null, + rehearsal: false, }) - } - - expect(fixSnapshot(await context.directCollections.Timelines.findFetch())).toMatchSnapshot() - }) - - /** - * Perform a test to check how a transition is formed on the timeline. - * This simulates two takes then allows for analysis of the state. - * @param name Name of the test - * @param customRundownFactory Factory to produce the rundown to play - * @param checkFcn Function used to check the resulting timeline - * @param timeout Override the timeout of the test - */ - function testTransitionTimings( - name: string, - customRundownFactory: ( - context: MockJobContext, - playlistId: RundownPlaylistId, - rundownId: RundownId, - showStyle: ReadonlyDeep - ) => Promise, - checkFcn: ( - rundownId: RundownId, - timeline: null, - currentPartInstance: DBPartInstance, - previousPartInstance: DBPartInstance, - checkTimings: (timings: PartTimelineTimings) => Promise, - previousTakeTime: number - ) => Promise, - timeout?: number - ) { - // eslint-disable-next-line jest/expect-expect - test( - // eslint-disable-next-line jest/valid-title - name, - async () => - runTimelineTimings( - customRundownFactory, - async (playlistId, rundownId, parts, _getPartInstances, checkTimings) => { - // Take the first Part: - const { currentPartInstance: currentPartInstance0 } = await doTakePart( - context, - playlistId, - null, - parts[0]._id, - parts[1]._id - ) - - // Report the first part as having started playback - const previousTakeTime = 10000 - await doAutoPlayoutPlaybackChangedForPart( - context, - playlistId, - currentPartInstance0!._id, - previousTakeTime - ) - // Take the second Part: - const { currentPartInstance, previousPartInstance } = await doTakePart( - context, - playlistId, - parts[0]._id, - parts[1]._id, - null - ) + { + // Prepare and activate in rehersal: + await handleActivateRundownPlaylist(context, { playlistId: playlistId0, rehearsal: false }) + const { currentPartInstance, nextPartInstance } = await getSelectedPartInstances( + context, + await getPlaylist0() + ) + expect(currentPartInstance).toBeFalsy() + expect(nextPartInstance).toBeTruthy() + expect(nextPartInstance!.part._id).toEqual(parts[0]._id) + await expect(getPlaylist0()).resolves.toMatchObject({ + activationId: expect.stringMatching(/^randomId/), + rehearsal: false, + currentPartInfo: null, + // nextPartInstanceId: parts[0]._id, + }) + } - // Report the second part as having started playback - await doAutoPlayoutPlaybackChangedForPart( - context, - playlistId, - currentPartInstance!._id, - previousTakeTime + 10000 - ) + { + // Take the first Part: + await handleTakeNextPart(context, { playlistId: playlistId0, fromPartInstanceId: null }) + const { currentPartInstance, nextPartInstance } = await getSelectedPartInstances( + context, + await getPlaylist0() + ) + expect(currentPartInstance).toBeTruthy() + expect(nextPartInstance).toBeTruthy() + expect(currentPartInstance!.part._id).toEqual(parts[0]._id) + expect(nextPartInstance!.part._id).toEqual(parts[1]._id) + // expect(getPlaylist0()).toMatchObject({ + // currentPartInstanceId: parts[0]._id, + // nextPartInstanceId: parts[1]._id, + // }) + } - // Run the result check - await checkFcn( - rundownId, - null, - currentPartInstance!, - previousPartInstance!, - checkTimings, - previousTakeTime - ) - } - ), - timeout - ) - } + await runJobWithPlayoutModel(context, { playlistId: playlistId0 }, null, async (playoutModel) => { + await updateTimeline(context, playoutModel) + }) - /** - * Perform a test to check how a timeline is formed - * This simulates two takes then allows for analysis of the state. - * @param customRundownFactory Factory to produce the rundown to play - * @param fcn Function to perform some playout operations and check the results - */ - async function runTimelineTimings( - customRundownFactory: ( - context: MockJobContext, - playlistId: RundownPlaylistId, - rundownId: RundownId, - showStyle: ReadonlyDeep - ) => Promise, - fcn: ( - playlistId: RundownPlaylistId, - rundownId: RundownId, - parts: DBPart[], - getPartInstances: () => Promise, - checkTimings: (timings: PartTimelineTimings) => Promise - ) => Promise - ) { - const rundownId0: RundownId = getRandomId() - const playlistId0 = await context.mockCollections.RundownPlaylists.insertOne( - defaultRundownPlaylist(protectString('playlist_' + rundownId0), context.studioId) - ) - - const rundownId = await customRundownFactory(context, playlistId0, rundownId0, showStyle) - expect(rundownId0).toBe(rundownId) - - const rundown = (await context.directCollections.Rundowns.findOne(rundownId0)) as Rundown - expect(rundown).toBeTruthy() + expect(fixSnapshot(await context.directCollections.Timelines.findFetch())).toMatchSnapshot() - { - const playlist = (await context.directCollections.RundownPlaylists.findOne( - playlistId0 - )) as DBRundownPlaylist - expect(playlist).toBeTruthy() + { + // Deactivate rundown: + await handleDeactivateRundownPlaylist(context, { playlistId: playlistId0 }) + await expect(getPlaylist0()).resolves.toMatchObject({ + activationId: undefined, + currentPartInfo: null, + nextPartInfo: null, + }) + } - // Ensure this is defined to something, for the jest matcher - playlist.activationId = playlist.activationId ?? undefined + expect(fixSnapshot(await context.directCollections.Timelines.findFetch())).toMatchSnapshot() + }) - expect(playlist).toMatchObject({ - activationId: undefined, - rehearsal: false, - }) + /** + * Perform a test to check how a transition is formed on the timeline. + * This simulates two takes then allows for analysis of the state. + * @param name Name of the test + * @param customRundownFactory Factory to produce the rundown to play + * @param checkFcn Function used to check the resulting timeline + * @param timeout Override the timeout of the test + */ + function testTransitionTimings( + name: string, + customRundownFactory: ( + context: MockJobContext, + playlistId: RundownPlaylistId, + rundownId: RundownId, + showStyle: ReadonlyDeep + ) => Promise, + checkFcn: ( + rundownId: RundownId, + timeline: null, + currentPartInstance: DBPartInstance, + previousPartInstance: DBPartInstance, + checkTimings: (timings: PartTimelineTimings) => Promise, + previousTakeTime: number + ) => Promise, + timeout?: number + ) { + // eslint-disable-next-line jest/expect-expect + test( + // eslint-disable-next-line jest/valid-title + name, + async () => + runTimelineTimings( + customRundownFactory, + async (playlistId, rundownId, parts, _getPartInstances, checkTimings) => { + // Take the first Part: + const { currentPartInstance: currentPartInstance0 } = await doTakePart( + context, + playlistId, + null, + parts[0]._id, + parts[1]._id + ) + + // Report the first part as having started playback + const previousTakeTime = 10000 + await doAutoPlayoutPlaybackChangedForPart( + context, + playlistId, + currentPartInstance0!._id, + previousTakeTime + ) + + // Take the second Part: + const { currentPartInstance, previousPartInstance } = await doTakePart( + context, + playlistId, + parts[0]._id, + parts[1]._id, + null + ) + + // Report the second part as having started playback + await doAutoPlayoutPlaybackChangedForPart( + context, + playlistId, + currentPartInstance!._id, + previousTakeTime + 10000 + ) + + // Run the result check + await checkFcn( + rundownId, + null, + currentPartInstance!, + previousPartInstance!, + checkTimings, + previousTakeTime + ) + } + ), + timeout + ) } - const parts = await getSortedPartsForRundown(context, rundown._id) + /** + * Perform a test to check how a timeline is formed + * This simulates two takes then allows for analysis of the state. + * @param customRundownFactory Factory to produce the rundown to play + * @param fcn Function to perform some playout operations and check the results + */ + async function runTimelineTimings( + customRundownFactory: ( + context: MockJobContext, + playlistId: RundownPlaylistId, + rundownId: RundownId, + showStyle: ReadonlyDeep + ) => Promise, + fcn: ( + playlistId: RundownPlaylistId, + rundownId: RundownId, + parts: DBPart[], + getPartInstances: () => Promise, + checkTimings: (timings: PartTimelineTimings) => Promise + ) => Promise + ) { + const rundownId0: RundownId = getRandomId() + const playlistId0 = await context.mockCollections.RundownPlaylists.insertOne( + defaultRundownPlaylist(protectString('playlist_' + rundownId0), context.studioId) + ) + + const rundownId = await customRundownFactory(context, playlistId0, rundownId0, showStyle) + expect(rundownId0).toBe(rundownId) - // Prepare and activate in rehersal: - await doActivatePlaylist(context, playlistId0, parts[0]._id) + const rundown = (await context.directCollections.Rundowns.findOne(rundownId0)) as Rundown + expect(rundown).toBeTruthy() - const getPartInstances = async (): Promise => { - const playlist = (await context.directCollections.RundownPlaylists.findOne( - playlistId0 - )) as DBRundownPlaylist - expect(playlist).toBeTruthy() - const res = await getSelectedPartInstances(context, playlist) + { + const playlist = (await context.directCollections.RundownPlaylists.findOne( + playlistId0 + )) as DBRundownPlaylist + expect(playlist).toBeTruthy() - async function wrapPartInstance( - partInstance: DBPartInstance | null - ): Promise { - if (!partInstance) return undefined + // Ensure this is defined to something, for the jest matcher + playlist.activationId = playlist.activationId ?? undefined - const pieceInstances = await context.directCollections.PieceInstances.findFetch({ - partInstanceId: partInstance?._id, + expect(playlist).toMatchObject({ + activationId: undefined, + rehearsal: false, }) - return new PlayoutPartInstanceModelImpl(partInstance, pieceInstances, false, mock()) - } - - return { - currentPartInstance: await wrapPartInstance(res.currentPartInstance), - nextPartInstance: await wrapPartInstance(res.nextPartInstance), - previousPartInstance: await wrapPartInstance(res.previousPartInstance), } - } - - const checkTimings = async (timings: PartTimelineTimings) => { - // Check the calculated timings - const timeline = await context.directCollections.Timelines.findOne(context.studio._id) - expect(timeline).toBeTruthy() - // console.log('objs', JSON.stringify(timeline?.timeline?.map((o) => o.id) || [], undefined, 4)) + const parts = await getSortedPartsForRundown(context, rundown._id) - await doUpdateTimeline(context, playlistId0) + // Prepare and activate in rehersal: + await doActivatePlaylist(context, playlistId0, parts[0]._id) + + const getPartInstances = async (): Promise => { + const playlist = (await context.directCollections.RundownPlaylists.findOne( + playlistId0 + )) as DBRundownPlaylist + expect(playlist).toBeTruthy() + const res = await getSelectedPartInstances(context, playlist) + + async function wrapPartInstance( + partInstance: DBPartInstance | null + ): Promise { + if (!partInstance) return undefined + + const pieceInstances = await context.directCollections.PieceInstances.findFetch({ + partInstanceId: partInstance?._id, + }) + return new PlayoutPartInstanceModelImpl( + partInstance, + pieceInstances, + false, + mock() + ) + } - const { currentPartInstance, previousPartInstance } = await getPartInstances() - return checkTimingsRaw(rundownId0, timeline, currentPartInstance!, previousPartInstance, timings) - } + return { + currentPartInstance: await wrapPartInstance(res.currentPartInstance), + nextPartInstance: await wrapPartInstance(res.nextPartInstance), + previousPartInstance: await wrapPartInstance(res.previousPartInstance), + } + } - // Run the required steps - await fcn(playlistId0, rundownId0, parts, getPartInstances, checkTimings) + const checkTimings = async (timings: PartTimelineTimings) => { + // Check the calculated timings + const timeline = await context.directCollections.Timelines.findOne(context.studio._id) + expect(timeline).toBeTruthy() - // Deactivate rundown: - await doDeactivatePlaylist(context, playlistId0) + // console.log('objs', JSON.stringify(timeline?.timeline?.map((o) => o.id) || [], undefined, 4)) - const timelinesEnd = await context.directCollections.Timelines.findFetch() - expect(fixSnapshot(timelinesEnd)).toMatchSnapshot() - } + await doUpdateTimeline(context, playlistId0) - describe('In transitions', () => { - testTransitionTimings( - 'Basic inTransition', - setupRundownWithInTransition, - async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { - await checkTimings({ - // old part is extended by 1000ms due to transition keepalive - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, - currentPieces: { - // pieces are delayed by the content delay - piece010: { - controlObj: { start: 0 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }, - currentInfinitePieces: {}, - previousOutTransition: undefined, - }) - } - ) - - testTransitionTimings( - 'Basic inTransition with planned pieces', - setupRundownWithInTransitionPlannedPiece, - async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { - await checkTimings({ - // old part is extended by 1000ms due to transition keepalive - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, - currentPieces: { - // pieces are delayed by the content delay - piece010: { - controlObj: { start: 500 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - // transition piece - piece011: { - controlObj: { start: 0 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - // pieces are delayed by the content delay - piece012: { - controlObj: { start: 1500, duration: 1000 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }, - currentInfinitePieces: {}, - previousOutTransition: undefined, - }) - } - ) - - testTransitionTimings( - 'Preroll', - setupRundownWithPreroll, - async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { - await checkTimings({ - // old part is extended due to preroll - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 500` }, - currentPieces: { - // main piece - piece010: { - controlObj: { start: 500 }, - childGroup: { preroll: 500, postroll: 0 }, - }, - }, - currentInfinitePieces: {}, - previousOutTransition: undefined, - }) - } - ) - - testTransitionTimings( - 'Basic inTransition with contentDelay', - setupRundownWithInTransitionContentDelay, - async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { - await checkTimings({ - // old part is extended due to preroll and transition keepalive - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, - currentPieces: { - // pieces are delayed by the content delay - piece010: { - controlObj: { start: 500 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - // transition piece - piece011: { - controlObj: { start: 0 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }, - currentInfinitePieces: {}, - previousOutTransition: undefined, - }) - } - ) - - testTransitionTimings( - 'Basic inTransition with contentDelay + preroll', - setupRundownWithInTransitionContentDelayAndPreroll, - async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { - await checkTimings({ - // old part is extended due to preroll and transition keepalive - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, - currentPieces: { - // pieces are delayed by the content delay - piece010: { - controlObj: { start: 500 }, - childGroup: { preroll: 250, postroll: 0 }, - }, - // transition piece - piece011: { - controlObj: { start: 0 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }, - currentInfinitePieces: {}, - previousOutTransition: undefined, - }) - } - ) - - testTransitionTimings( - 'inTransition with existing infinites', - setupRundownWithInTransitionExistingInfinite, - async ( - _rundownId0, - _timeline, - currentPartInstance, - _previousPartInstance, - checkTimings, - previousTakeTime - ) => { - await checkTimings({ - // old part is extended due to transition keepalive - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, - currentPieces: { - // pieces are delayed by the content delay - piece010: { - controlObj: { start: 500 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - // transition piece - piece011: { - controlObj: { start: 0 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }, - currentInfinitePieces: { - piece002: { - // Should still be based on the time of previousPart, offset for preroll - partGroup: { start: previousTakeTime - 500 }, - pieceGroup: { - controlObj: { start: 500 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }, - }, - previousOutTransition: undefined, - }) - } - ) - - testTransitionTimings( - 'inTransition with new infinite', - setupRundownWithInTransitionNewInfinite, - async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { - await checkTimings({ - // old part is extended due to transition keepalive - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, - currentPieces: { - // pieces are delayed by the content delay - piece010: { - controlObj: { start: 500 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - // transition piece - piece011: { - controlObj: { start: 0 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }, - currentInfinitePieces: { - piece012: { - // Delay get applied to the pieceGroup inside the partGroup - partGroup: { start: `#${getPartGroupId(currentPartInstance)}.start` }, - pieceGroup: { - controlObj: { start: 500 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }, - }, - previousOutTransition: undefined, - }) + const { currentPartInstance, previousPartInstance } = await getPartInstances() + return checkTimingsRaw(rundownId0, timeline, currentPartInstance!, previousPartInstance, timings) } - ) - // eslint-disable-next-line jest/expect-expect - test('inTransition is disabled during hold', async () => - runTimelineTimings( - setupRundownWithInTransitionEnableHold, - async (playlistId, _rundownId, parts, getPartInstances, checkTimings) => { - // Take the first Part: - await doTakePart(context, playlistId, null, parts[0]._id, parts[1]._id) + // Run the required steps + await fcn(playlistId0, rundownId0, parts, getPartInstances, checkTimings) - // activate hold mode - await handleActivateHold(context, { playlistId: playlistId }) + // Deactivate rundown: + await doDeactivatePlaylist(context, playlistId0) - await doTakePart(context, playlistId, parts[0]._id, parts[1]._id, null) + const timelinesEnd = await context.directCollections.Timelines.findFetch() + expect(fixSnapshot(timelinesEnd)).toMatchSnapshot() + } - const { currentPartInstance } = await getPartInstances() + describe('In transitions', () => { + testTransitionTimings( + 'Basic inTransition', + setupRundownWithInTransition, + async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { await checkTimings({ - // old part ends immediately - previousPart: { end: `#${getPartGroupId(currentPartInstance!.partInstance)}.start + 0` }, + // old part is extended by 1000ms due to transition keepalive + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, currentPieces: { - // pieces are not delayed + // pieces are delayed by the content delay piece010: { controlObj: { start: 0 }, childGroup: { preroll: 0, postroll: 0 }, }, - // no in transition - piece011: null, }, currentInfinitePieces: {}, previousOutTransition: undefined, }) } - )) - - testTransitionTimings( - 'inTransition disabled', - setupRundownWithInTransitionDisabled, - async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { - await checkTimings({ - // old part is not extended - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 0` }, - currentPieces: { - // pieces are not delayed - piece010: { - controlObj: { start: 0 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - // no transition piece - piece011: null, - }, - currentInfinitePieces: {}, - previousOutTransition: undefined, - }) - } - ) - }) - - describe('Out transitions', () => { - testTransitionTimings( - 'Basic outTransition', - setupRundownWithOutTransition, - async (_rundownId0, _timeline, currentPartInstance, previousPartInstance, checkTimings) => { - await checkTimings({ - // old part is extended by 1000ms due to transition keepalive - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, - currentPieces: { - // pieces are delayed by outTransition time - piece010: { - controlObj: { start: 1000 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }, - currentInfinitePieces: {}, - // outTransitionPiece is inserted - previousOutTransition: { - controlObj: { start: `#${getPartGroupId(previousPartInstance)}.end - 1000` }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }) - } - ) - - testTransitionTimings( - 'outTransition + preroll', - setupRundownWithOutTransitionAndPreroll, - async (_rundownId0, _timeline, currentPartInstance, previousPartInstance, checkTimings) => { - await checkTimings({ - // old part is extended by 1000ms due to transition keepalive - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, - currentPieces: { - // pieces are delayed by outTransition time - piece010: { - // 1000ms out transition, 250ms preroll - controlObj: { start: 1000 }, - childGroup: { preroll: 250, postroll: 0 }, - }, - }, - currentInfinitePieces: {}, - // outTransitionPiece is inserted - previousOutTransition: { - controlObj: { start: `#${getPartGroupId(previousPartInstance)}.end - 1000` }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }) - } - ) - - testTransitionTimings( - 'outTransition + preroll (2)', - setupRundownWithOutTransitionAndPreroll2, - async (_rundownId0, _timeline, currentPartInstance, previousPartInstance, checkTimings) => { - await checkTimings({ - // old part is extended by 1000ms due to transition keepalive - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, - currentPieces: { - // pieces are delayed by outTransition time - piece010: { - // 250ms out transition, 1000ms preroll. preroll takes precedence - controlObj: { start: 1000 }, - childGroup: { preroll: 1000, postroll: 0 }, - }, - }, - currentInfinitePieces: {}, - // outTransitionPiece is inserted - previousOutTransition: { - controlObj: { start: `#${getPartGroupId(previousPartInstance)}.end - 250` }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }) - } - ) - - testTransitionTimings( - 'outTransition + inTransition', - setupRundownWithOutTransitionAndInTransition, - async (_rundownId0, _timeline, currentPartInstance, previousPartInstance, checkTimings) => { - await checkTimings({ - // old part is extended by 1000ms due to transition keepalive - previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 600` }, // 600ms outtransiton & 250ms transition keepalive - currentPieces: { - // pieces are delayed by in transition preroll time - piece010: { - // inTransPieceTlObj + 300 contentDelay - controlObj: { start: 650 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - // in transition is delayed by outTransition time - piece011: { - // 600 - 250 = 350 - controlObj: { start: 350, duration: 500 }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }, - currentInfinitePieces: {}, - // outTransitionPiece is inserted - previousOutTransition: { - controlObj: { start: `#${getPartGroupId(previousPartInstance)}.end - 600` }, - childGroup: { preroll: 0, postroll: 0 }, - }, - }) - } - ) - - // eslint-disable-next-line jest/expect-expect - test('outTransition is disabled during hold', async () => - runTimelineTimings( - setupRundownWithOutTransitionEnableHold, - async (playlistId, _rundownId, parts, getPartInstances, checkTimings) => { - // Take the first Part: - await doTakePart(context, playlistId, null, parts[0]._id, parts[1]._id) - - // activate hold mode - await handleActivateHold(context, { playlistId: playlistId }) - - await doTakePart(context, playlistId, parts[0]._id, parts[1]._id, null) + ) - const { currentPartInstance } = await getPartInstances() + testTransitionTimings( + 'Basic inTransition with planned pieces', + setupRundownWithInTransitionPlannedPiece, + async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { await checkTimings({ - previousPart: { end: `#${getPartGroupId(currentPartInstance!.partInstance)}.start + 500` }, // note: this seems odd, but the pieces are delayed to compensate + // old part is extended by 1000ms due to transition keepalive + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, currentPieces: { + // pieces are delayed by the content delay piece010: { - controlObj: { start: 500 }, // note: Offset matches extension of previous partGroup + controlObj: { start: 500 }, + childGroup: { preroll: 0, postroll: 0 }, + }, + // transition piece + piece011: { + controlObj: { start: 0 }, + childGroup: { preroll: 0, postroll: 0 }, + }, + // pieces are delayed by the content delay + piece012: { + controlObj: { start: 1500, duration: 1000 }, childGroup: { preroll: 0, postroll: 0 }, }, }, @@ -1129,502 +802,1223 @@ describe('Timeline', () => { previousOutTransition: undefined, }) } - )) - }) - - describe('Adlib pieces', () => { - async function doStartAdlibPiece(playlistId: RundownPlaylistId, adlibSource: AdLibPiece) { - await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => { - const currentPartInstance = playoutModel.currentPartInstance as PlayoutPartInstanceModel - expect(currentPartInstance).toBeTruthy() - - const rundown = playoutModel.getRundown( - currentPartInstance.partInstance.rundownId - ) as PlayoutRundownModel - expect(rundown).toBeTruthy() - - return innerStartOrQueueAdLibPiece( - context, - playoutModel, - rundown, - false, - currentPartInstance, - adlibSource - ) - }) - } - - async function doSimulatePiecePlaybackTimings(playlistId: RundownPlaylistId, time: Time, objectCount: number) { - const timelineComplete = (await context.directCollections.Timelines.findOne( - context.studioId - )) as TimelineComplete - expect(timelineComplete).toBeTruthy() - - const rawTimelineObjs = deserializeTimelineBlob(timelineComplete.timelineBlob) - const nowObjs = rawTimelineObjs.filter((obj) => !Array.isArray(obj.enable) && obj.enable.start === 'now') - expect(nowObjs).toHaveLength(objectCount) - - const results = nowObjs.map((obj) => ({ - id: obj.id, - time: time, - })) - // console.log('Sending trigger for:', results) - - await handleTimelineTriggerTime(context, { results }) - - await doUpdateTimeline(context, playlistId) - } - - test('Current part with preroll', async () => - runTimelineTimings( - async ( - context: MockJobContext, - playlistId: RundownPlaylistId, - rundownId: RundownId, - showStyle: ReadonlyDeep - ): Promise => { - const sourceLayerIds = Object.keys(showStyle.sourceLayers) - - await setupRundownBase( - context, - playlistId, - rundownId, - showStyle, - {}, - { - piece0: { prerollDuration: 500 }, - piece1: { prerollDuration: 50, sourceLayerId: sourceLayerIds[3] }, - } - ) - - return rundownId - }, - async (playlistId, rundownId, parts, getPartInstances, checkTimings) => { - const outputLayerIds = Object.keys(showStyle.outputLayers) - const sourceLayerIds = Object.keys(showStyle.sourceLayers) - - // Take the only Part: - await doTakePart(context, playlistId, null, parts[0]._id, null) + ) - // Should look normal for now + testTransitionTimings( + 'Preroll', + setupRundownWithPreroll, + async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { await checkTimings({ - previousPart: null, + // old part is extended due to preroll + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 500` }, currentPieces: { - piece000: { - controlObj: { start: 500 }, // This one gave the preroll - childGroup: { preroll: 500, postroll: 0 }, - }, - piece001: { + // main piece + piece010: { controlObj: { start: 500 }, - childGroup: { preroll: 50, postroll: 0 }, + childGroup: { preroll: 500, postroll: 0 }, }, }, currentInfinitePieces: {}, previousOutTransition: undefined, }) + } + ) - const { currentPartInstance } = await getPartInstances() - expect(currentPartInstance).toBeTruthy() - - // Insert an adlib piece - await doStartAdlibPiece( - playlistId, - literal({ - _id: protectString('adlib1'), - rundownId: currentPartInstance!.partInstance.rundownId, - externalId: 'fake', - name: 'Adlibbed piece', - lifespan: PieceLifespan.WithinPart, - sourceLayerId: sourceLayerIds[0], - outputLayerId: outputLayerIds[0], - content: {}, - timelineObjectsString: EmptyPieceTimelineObjectsBlob, - _rank: 0, - }) - ) - - const adlibbedPieceId = 'randomId9007' - - // The adlib should be starting at 'now' + testTransitionTimings( + 'Basic inTransition with contentDelay', + setupRundownWithInTransitionContentDelay, + async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { await checkTimings({ - previousPart: null, + // old part is extended due to preroll and transition keepalive + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, currentPieces: { - piece000: { - controlObj: { - start: 500, // This one gave the preroll - end: `#piece_group_control_${ - currentPartInstance!.partInstance._id - }_${rundownId}_piece000_cap_now.start + 0`, - }, - childGroup: { - preroll: 500, - postroll: 0, - }, - }, - piece001: { - controlObj: { - start: 500, - }, - childGroup: { - preroll: 50, - postroll: 0, - }, + // pieces are delayed by the content delay + piece010: { + controlObj: { start: 500 }, + childGroup: { preroll: 0, postroll: 0 }, }, - [adlibbedPieceId]: { - // Our adlibbed piece - controlObj: { - start: 'now', - }, - childGroup: { - preroll: 0, - postroll: 0, - }, + // transition piece + piece011: { + controlObj: { start: 0 }, + childGroup: { preroll: 0, postroll: 0 }, }, }, currentInfinitePieces: {}, previousOutTransition: undefined, }) + } + ) - const pieceOffset = 12560 - - // Simulate the piece timing confirmation from playout-gateway - await doSimulatePiecePlaybackTimings(playlistId, pieceOffset, 2) // This pieceOffset includes the partPreroll - - // Now we have a concrete time + testTransitionTimings( + 'Basic inTransition with contentDelay + preroll', + setupRundownWithInTransitionContentDelayAndPreroll, + async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { await checkTimings({ - previousPart: null, + // old part is extended due to preroll and transition keepalive + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, currentPieces: { - piece000: { - controlObj: { - start: 500, // This one gave the preroll - end: pieceOffset, // This is expected to match the start of the adlib - }, - childGroup: { - preroll: 500, - postroll: 0, - }, - }, - piece001: { - controlObj: { - start: 500, - }, - childGroup: { - preroll: 50, - postroll: 0, - }, + // pieces are delayed by the content delay + piece010: { + controlObj: { start: 500 }, + childGroup: { preroll: 250, postroll: 0 }, }, - [adlibbedPieceId]: { - // Our adlibbed piece - controlObj: { - start: pieceOffset, - }, - childGroup: { - preroll: 0, - postroll: 0, - }, + // transition piece + piece011: { + controlObj: { start: 0 }, + childGroup: { preroll: 0, postroll: 0 }, }, }, currentInfinitePieces: {}, previousOutTransition: undefined, }) } - )) + ) - test('Current part with preroll and adlib preroll', async () => - runTimelineTimings( + testTransitionTimings( + 'inTransition with existing infinites', + setupRundownWithInTransitionExistingInfinite, async ( - context: MockJobContext, - playlistId: RundownPlaylistId, - rundownId: RundownId, - showStyle: ReadonlyDeep - ): Promise => { - const sourceLayerIds = Object.keys(showStyle.sourceLayers) - - await setupRundownBase( - context, - playlistId, - rundownId, - showStyle, - {}, - { - piece0: { prerollDuration: 500 }, - piece1: { prerollDuration: 50, sourceLayerId: sourceLayerIds[3] }, - } - ) - - return rundownId - }, - async (playlistId, _rundownId, parts, getPartInstances, checkTimings) => { - const outputLayerIds = Object.keys(showStyle.outputLayers) - const sourceLayerIds = Object.keys(showStyle.sourceLayers) - - // Take the only Part: - await doTakePart(context, playlistId, null, parts[0]._id, null) - - // Should look normal for now + _rundownId0, + _timeline, + currentPartInstance, + _previousPartInstance, + checkTimings, + previousTakeTime + ) => { await checkTimings({ - previousPart: null, + // old part is extended due to transition keepalive + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, currentPieces: { - piece000: { - // This one gave the preroll - controlObj: { - start: 500, - }, - childGroup: { - preroll: 500, - postroll: 0, - }, + // pieces are delayed by the content delay + piece010: { + controlObj: { start: 500 }, + childGroup: { preroll: 0, postroll: 0 }, }, - piece001: { - controlObj: { - start: 500, - }, - childGroup: { - preroll: 50, - postroll: 0, + // transition piece + piece011: { + controlObj: { start: 0 }, + childGroup: { preroll: 0, postroll: 0 }, + }, + }, + currentInfinitePieces: { + piece002: { + // Should still be based on the time of previousPart, offset for preroll + partGroup: { start: previousTakeTime - 500 }, + pieceGroup: { + controlObj: { start: 500 }, + childGroup: { preroll: 0, postroll: 0 }, }, }, }, - currentInfinitePieces: {}, previousOutTransition: undefined, }) + } + ) - const { currentPartInstance } = await getPartInstances() - expect(currentPartInstance).toBeTruthy() - - // Insert an adlib piece - await doStartAdlibPiece( - playlistId, - literal({ - _id: protectString('adlib1'), - rundownId: currentPartInstance!.partInstance.rundownId, - externalId: 'fake', - name: 'Adlibbed piece', - lifespan: PieceLifespan.WithinPart, - sourceLayerId: sourceLayerIds[0], - outputLayerId: outputLayerIds[0], - content: {}, - timelineObjectsString: EmptyPieceTimelineObjectsBlob, - _rank: 0, - prerollDuration: 340, - }) - ) - - const adlibbedPieceId = 'randomId9007' - - // The adlib should be starting at 'now' + testTransitionTimings( + 'inTransition with new infinite', + setupRundownWithInTransitionNewInfinite, + async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { await checkTimings({ - previousPart: null, + // old part is extended due to transition keepalive + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, currentPieces: { - piece000: { - controlObj: { - start: 500, // This one gave the preroll - end: `#piece_group_control_${ - currentPartInstance!.partInstance._id - }_${_rundownId}_piece000_cap_now.start + 0`, - }, - childGroup: { - preroll: 500, - postroll: 0, - }, + // pieces are delayed by the content delay + piece010: { + controlObj: { start: 500 }, + childGroup: { preroll: 0, postroll: 0 }, }, - piece001: { - controlObj: { - start: 500, - }, - childGroup: { - preroll: 50, - postroll: 0, - }, + // transition piece + piece011: { + controlObj: { start: 0 }, + childGroup: { preroll: 0, postroll: 0 }, }, - [adlibbedPieceId]: { - // Our adlibbed piece - controlObj: { - start: `#piece_group_control_${ - currentPartInstance!.partInstance._id - }_${adlibbedPieceId}_start_now + 340`, + }, + currentInfinitePieces: { + piece012: { + // Delay get applied to the pieceGroup inside the partGroup + partGroup: { start: `#${getPartGroupId(currentPartInstance)}.start` }, + pieceGroup: { + controlObj: { start: 500 }, + childGroup: { preroll: 0, postroll: 0 }, }, - childGroup: { - preroll: 340, - postroll: 0, + }, + }, + previousOutTransition: undefined, + }) + } + ) + + // eslint-disable-next-line jest/expect-expect + test('inTransition is disabled during hold', async () => + runTimelineTimings( + setupRundownWithInTransitionEnableHold, + async (playlistId, _rundownId, parts, getPartInstances, checkTimings) => { + // Take the first Part: + await doTakePart(context, playlistId, null, parts[0]._id, parts[1]._id) + + // activate hold mode + await handleActivateHold(context, { playlistId: playlistId }) + + await doTakePart(context, playlistId, parts[0]._id, parts[1]._id, null) + + const { currentPartInstance } = await getPartInstances() + await checkTimings({ + // old part ends immediately + previousPart: { end: `#${getPartGroupId(currentPartInstance!.partInstance)}.start + 0` }, + currentPieces: { + // pieces are not delayed + piece010: { + controlObj: { start: 0 }, + childGroup: { preroll: 0, postroll: 0 }, }, + // no in transition + piece011: null, + }, + currentInfinitePieces: {}, + previousOutTransition: undefined, + }) + } + )) + + testTransitionTimings( + 'inTransition disabled', + setupRundownWithInTransitionDisabled, + async (_rundownId0, _timeline, currentPartInstance, _previousPartInstance, checkTimings) => { + await checkTimings({ + // old part is not extended + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 0` }, + currentPieces: { + // pieces are not delayed + piece010: { + controlObj: { start: 0 }, + childGroup: { preroll: 0, postroll: 0 }, }, + // no transition piece + piece011: null, }, currentInfinitePieces: {}, previousOutTransition: undefined, }) + } + ) + }) - const pieceOffset = 12560 - // Simulate the piece timing confirmation from playout-gateway - await doSimulatePiecePlaybackTimings(playlistId, pieceOffset, 2) + describe('Out transitions', () => { + testTransitionTimings( + 'Basic outTransition', + setupRundownWithOutTransition, + async (_rundownId0, _timeline, currentPartInstance, previousPartInstance, checkTimings) => { + await checkTimings({ + // old part is extended by 1000ms due to transition keepalive + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, + currentPieces: { + // pieces are delayed by outTransition time + piece010: { + controlObj: { start: 1000 }, + childGroup: { preroll: 0, postroll: 0 }, + }, + }, + currentInfinitePieces: {}, + // outTransitionPiece is inserted + previousOutTransition: { + controlObj: { start: `#${getPartGroupId(previousPartInstance)}.end - 1000` }, + childGroup: { preroll: 0, postroll: 0 }, + }, + }) + } + ) - // Now we have a concrete time + testTransitionTimings( + 'outTransition + preroll', + setupRundownWithOutTransitionAndPreroll, + async (_rundownId0, _timeline, currentPartInstance, previousPartInstance, checkTimings) => { await checkTimings({ - previousPart: null, + // old part is extended by 1000ms due to transition keepalive + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, currentPieces: { - piece000: { - controlObj: { - start: 500, // This one gave the preroll - end: pieceOffset, - }, - childGroup: { - preroll: 500, - postroll: 0, - }, + // pieces are delayed by outTransition time + piece010: { + // 1000ms out transition, 250ms preroll + controlObj: { start: 1000 }, + childGroup: { preroll: 250, postroll: 0 }, }, - piece001: { - controlObj: { - start: 500, - }, - childGroup: { - preroll: 50, - postroll: 0, - }, + }, + currentInfinitePieces: {}, + // outTransitionPiece is inserted + previousOutTransition: { + controlObj: { start: `#${getPartGroupId(previousPartInstance)}.end - 1000` }, + childGroup: { preroll: 0, postroll: 0 }, + }, + }) + } + ) + + testTransitionTimings( + 'outTransition + preroll (2)', + setupRundownWithOutTransitionAndPreroll2, + async (_rundownId0, _timeline, currentPartInstance, previousPartInstance, checkTimings) => { + await checkTimings({ + // old part is extended by 1000ms due to transition keepalive + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, + currentPieces: { + // pieces are delayed by outTransition time + piece010: { + // 250ms out transition, 1000ms preroll. preroll takes precedence + controlObj: { start: 1000 }, + childGroup: { preroll: 1000, postroll: 0 }, }, - [adlibbedPieceId]: { - // Our adlibbed piece - controlObj: { - start: pieceOffset, - }, - childGroup: { - preroll: 340, - postroll: 0, - }, + }, + currentInfinitePieces: {}, + // outTransitionPiece is inserted + previousOutTransition: { + controlObj: { start: `#${getPartGroupId(previousPartInstance)}.end - 250` }, + childGroup: { preroll: 0, postroll: 0 }, + }, + }) + } + ) + + testTransitionTimings( + 'outTransition + inTransition', + setupRundownWithOutTransitionAndInTransition, + async (_rundownId0, _timeline, currentPartInstance, previousPartInstance, checkTimings) => { + await checkTimings({ + // old part is extended by 1000ms due to transition keepalive + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 600` }, // 600ms outtransiton & 250ms transition keepalive + currentPieces: { + // pieces are delayed by in transition preroll time + piece010: { + // inTransPieceTlObj + 300 contentDelay + controlObj: { start: 650 }, + childGroup: { preroll: 0, postroll: 0 }, + }, + // in transition is delayed by outTransition time + piece011: { + // 600 - 250 = 350 + controlObj: { start: 350, duration: 500 }, + childGroup: { preroll: 0, postroll: 0 }, }, }, currentInfinitePieces: {}, - previousOutTransition: undefined, + // outTransitionPiece is inserted + previousOutTransition: { + controlObj: { start: `#${getPartGroupId(previousPartInstance)}.end - 600` }, + childGroup: { preroll: 0, postroll: 0 }, + }, }) } - )) - }) + ) - describe('Infinite Pieces', () => { - test('Infinite Piece has stable timing across timeline regenerations with/without plannedStartedPlayback', async () => - runTimelineTimings( - async ( - context: MockJobContext, - playlistId: RundownPlaylistId, - rundownId: RundownId, - showStyle: ReadonlyDeep - ): Promise => { - const sourceLayerIds = Object.keys(showStyle.sourceLayers) - - await setupRundownBase( - context, - playlistId, - rundownId, - showStyle, - {}, - { - piece0: { prerollDuration: 500 }, - piece1: { - prerollDuration: 50, - sourceLayerId: sourceLayerIds[3], - lifespan: PieceLifespan.OutOnSegmentEnd, + // eslint-disable-next-line jest/expect-expect + test('outTransition is disabled during hold', async () => + runTimelineTimings( + setupRundownWithOutTransitionEnableHold, + async (playlistId, _rundownId, parts, getPartInstances, checkTimings) => { + // Take the first Part: + await doTakePart(context, playlistId, null, parts[0]._id, parts[1]._id) + + // activate hold mode + await handleActivateHold(context, { playlistId: playlistId }) + + await doTakePart(context, playlistId, parts[0]._id, parts[1]._id, null) + + const { currentPartInstance } = await getPartInstances() + await checkTimings({ + previousPart: { end: `#${getPartGroupId(currentPartInstance!.partInstance)}.start + 500` }, // note: this seems odd, but the pieces are delayed to compensate + currentPieces: { + piece010: { + controlObj: { start: 500 }, // note: Offset matches extension of previous partGroup + childGroup: { preroll: 0, postroll: 0 }, + }, }, - } + currentInfinitePieces: {}, + previousOutTransition: undefined, + }) + } + )) + }) + + describe('Adlib pieces', () => { + async function doStartAdlibPiece(playlistId: RundownPlaylistId, adlibSource: AdLibPiece) { + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => { + const currentPartInstance = playoutModel.currentPartInstance as PlayoutPartInstanceModel + expect(currentPartInstance).toBeTruthy() + + const rundown = playoutModel.getRundown( + currentPartInstance.partInstance.rundownId + ) as PlayoutRundownModel + expect(rundown).toBeTruthy() + + return innerStartOrQueueAdLibPiece( + context, + playoutModel, + rundown, + false, + currentPartInstance, + adlibSource ) + }) + } - return rundownId - }, - async (playlistId, rundownId, parts, getPartInstances, checkTimings) => { - // Take the only Part: - await doTakePart(context, playlistId, null, parts[0]._id, null) + async function doSimulatePiecePlaybackTimings( + playlistId: RundownPlaylistId, + time: Time, + objectCount: number + ) { + const timelineComplete = (await context.directCollections.Timelines.findOne( + context.studioId + )) as TimelineComplete + expect(timelineComplete).toBeTruthy() + + const rawTimelineObjs = deserializeTimelineBlob(timelineComplete.timelineBlob) + const nowObjs = rawTimelineObjs.filter( + (obj) => !Array.isArray(obj.enable) && obj.enable.start === 'now' + ) + expect(nowObjs).toHaveLength(objectCount) - const { currentPartInstance } = await getPartInstances() - expect(currentPartInstance).toBeTruthy() - if (!currentPartInstance) throw new Error('currentPartInstance must be defined') + const results = nowObjs.map((obj) => ({ + id: obj.id, + time: time, + })) + // console.log('Sending trigger for:', results) - // Should look normal for now - await checkTimings({ - previousPart: null, - currentPieces: { - piece000: { - // This one gave the preroll - controlObj: { - start: 500, + await handleTimelineTriggerTime(context, { results }) + + await doUpdateTimeline(context, playlistId) + } + + test('Current part with preroll', async () => + runTimelineTimings( + async ( + context: MockJobContext, + playlistId: RundownPlaylistId, + rundownId: RundownId, + showStyle: ReadonlyDeep + ): Promise => { + const sourceLayerIds = Object.keys(showStyle.sourceLayers) + + await setupRundownBase( + context, + playlistId, + rundownId, + showStyle, + {}, + { + piece0: { prerollDuration: 500 }, + piece1: { prerollDuration: 50, sourceLayerId: sourceLayerIds[3] }, + } + ) + + return rundownId + }, + async (playlistId, rundownId, parts, getPartInstances, checkTimings) => { + const outputLayerIds = Object.keys(showStyle.outputLayers) + const sourceLayerIds = Object.keys(showStyle.sourceLayers) + + // Take the only Part: + await doTakePart(context, playlistId, null, parts[0]._id, null) + + // Should look normal for now + await checkTimings({ + previousPart: null, + currentPieces: { + piece000: { + controlObj: { start: 500 }, // This one gave the preroll + childGroup: { preroll: 500, postroll: 0 }, }, - childGroup: { - preroll: 500, - postroll: 0, + piece001: { + controlObj: { start: 500 }, + childGroup: { preroll: 50, postroll: 0 }, }, }, - }, - currentInfinitePieces: { - piece001: { - pieceGroup: { + currentInfinitePieces: {}, + previousOutTransition: undefined, + }) + + const { currentPartInstance } = await getPartInstances() + expect(currentPartInstance).toBeTruthy() + + // Insert an adlib piece + await doStartAdlibPiece( + playlistId, + literal({ + _id: protectString('adlib1'), + rundownId: currentPartInstance!.partInstance.rundownId, + externalId: 'fake', + name: 'Adlibbed piece', + lifespan: PieceLifespan.WithinPart, + sourceLayerId: sourceLayerIds[0], + outputLayerId: outputLayerIds[0], + content: {}, + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + _rank: 0, + }) + ) + + const adlibbedPieceId = 'randomId9007' + + // The adlib should be starting at 'now' + await checkTimings({ + previousPart: null, + currentPieces: { + piece000: { + controlObj: { + start: 500, // This one gave the preroll + end: `#piece_group_control_${ + currentPartInstance!.partInstance._id + }_${rundownId}_piece000_cap_now.start + 0`, + }, + childGroup: { + preroll: 500, + postroll: 0, + }, + }, + piece001: { + controlObj: { + start: 500, + }, childGroup: { preroll: 50, postroll: 0, }, + }, + [adlibbedPieceId]: { + // Our adlibbed piece + controlObj: { + start: 'now', + }, + childGroup: { + preroll: 0, + postroll: 0, + }, + }, + }, + currentInfinitePieces: {}, + previousOutTransition: undefined, + }) + + const pieceOffset = 12560 + + // Simulate the piece timing confirmation from playout-gateway + await doSimulatePiecePlaybackTimings(playlistId, pieceOffset, 2) // This pieceOffset includes the partPreroll + + // Now we have a concrete time + await checkTimings({ + previousPart: null, + currentPieces: { + piece000: { + controlObj: { + start: 500, // This one gave the preroll + end: pieceOffset, // This is expected to match the start of the adlib + }, + childGroup: { + preroll: 500, + postroll: 0, + }, + }, + piece001: { controlObj: { start: 500, }, + childGroup: { + preroll: 50, + postroll: 0, + }, }, - partGroup: { - start: `#part_group_${currentPartInstance.partInstance._id}.start`, + [adlibbedPieceId]: { + // Our adlibbed piece + controlObj: { + start: pieceOffset, + }, + childGroup: { + preroll: 0, + postroll: 0, + }, }, }, - }, - previousOutTransition: undefined, - }) + currentInfinitePieces: {}, + previousOutTransition: undefined, + }) + } + )) - const currentPieceInstances = currentPartInstance.pieceInstances - const pieceInstance0 = currentPieceInstances.find( - (instance) => instance.pieceInstance.piece._id === protectString(`${rundownId}_piece000`) - ) - if (!pieceInstance0) throw new Error('pieceInstance0 must be defined') - const pieceInstance1 = currentPieceInstances.find( - (instance) => instance.pieceInstance.piece._id === protectString(`${rundownId}_piece001`) - ) - if (!pieceInstance1) throw new Error('pieceInstance1 must be defined') - - const currentTime = 12300 - await doOnPlayoutPlaybackChanged(context, playlistId, { - baseTime: currentTime, - partId: currentPartInstance.partInstance._id, - includePart: true, - pieceOffsets: { - [unprotectString(pieceInstance0.pieceInstance._id)]: 500, - [unprotectString(pieceInstance1.pieceInstance._id)]: 500, - }, - }) + test('Current part with preroll and adlib preroll', async () => + runTimelineTimings( + async ( + context: MockJobContext, + playlistId: RundownPlaylistId, + rundownId: RundownId, + showStyle: ReadonlyDeep + ): Promise => { + const sourceLayerIds = Object.keys(showStyle.sourceLayers) + + await setupRundownBase( + context, + playlistId, + rundownId, + showStyle, + {}, + { + piece0: { prerollDuration: 500 }, + piece1: { prerollDuration: 50, sourceLayerId: sourceLayerIds[3] }, + } + ) + + return rundownId + }, + async (playlistId, _rundownId, parts, getPartInstances, checkTimings) => { + const outputLayerIds = Object.keys(showStyle.outputLayers) + const sourceLayerIds = Object.keys(showStyle.sourceLayers) + + // Take the only Part: + await doTakePart(context, playlistId, null, parts[0]._id, null) + + // Should look normal for now + await checkTimings({ + previousPart: null, + currentPieces: { + piece000: { + // This one gave the preroll + controlObj: { + start: 500, + }, + childGroup: { + preroll: 500, + postroll: 0, + }, + }, + piece001: { + controlObj: { + start: 500, + }, + childGroup: { + preroll: 50, + postroll: 0, + }, + }, + }, + currentInfinitePieces: {}, + previousOutTransition: undefined, + }) - await doUpdateTimeline(context, playlistId) + const { currentPartInstance } = await getPartInstances() + expect(currentPartInstance).toBeTruthy() - await checkTimings({ - previousPart: null, - currentPieces: { - piece000: { - controlObj: { - start: 500, + // Insert an adlib piece + await doStartAdlibPiece( + playlistId, + literal({ + _id: protectString('adlib1'), + rundownId: currentPartInstance!.partInstance.rundownId, + externalId: 'fake', + name: 'Adlibbed piece', + lifespan: PieceLifespan.WithinPart, + sourceLayerId: sourceLayerIds[0], + outputLayerId: outputLayerIds[0], + content: {}, + timelineObjectsString: EmptyPieceTimelineObjectsBlob, + _rank: 0, + prerollDuration: 340, + }) + ) + + const adlibbedPieceId = 'randomId9007' + + // The adlib should be starting at 'now' + await checkTimings({ + previousPart: null, + currentPieces: { + piece000: { + controlObj: { + start: 500, // This one gave the preroll + end: `#piece_group_control_${ + currentPartInstance!.partInstance._id + }_${_rundownId}_piece000_cap_now.start + 0`, + }, + childGroup: { + preroll: 500, + postroll: 0, + }, }, - childGroup: { - preroll: 500, - postroll: 0, + piece001: { + controlObj: { + start: 500, + }, + childGroup: { + preroll: 50, + postroll: 0, + }, + }, + [adlibbedPieceId]: { + // Our adlibbed piece + controlObj: { + start: `#piece_group_control_${ + currentPartInstance!.partInstance._id + }_${adlibbedPieceId}_start_now + 340`, + }, + childGroup: { + preroll: 340, + postroll: 0, + }, }, }, - }, - currentInfinitePieces: { - piece001: { - pieceGroup: { + currentInfinitePieces: {}, + previousOutTransition: undefined, + }) + + const pieceOffset = 12560 + // Simulate the piece timing confirmation from playout-gateway + await doSimulatePiecePlaybackTimings(playlistId, pieceOffset, 2) + + // Now we have a concrete time + await checkTimings({ + previousPart: null, + currentPieces: { + piece000: { + controlObj: { + start: 500, // This one gave the preroll + end: pieceOffset, + }, + childGroup: { + preroll: 500, + postroll: 0, + }, + }, + piece001: { + controlObj: { + start: 500, + }, childGroup: { preroll: 50, postroll: 0, }, + }, + [adlibbedPieceId]: { + // Our adlibbed piece + controlObj: { + start: pieceOffset, + }, + childGroup: { + preroll: 340, + postroll: 0, + }, + }, + }, + currentInfinitePieces: {}, + previousOutTransition: undefined, + }) + } + )) + }) + + describe('Infinite Pieces', () => { + test('Infinite Piece has stable timing across timeline regenerations with/without plannedStartedPlayback', async () => + runTimelineTimings( + async ( + context: MockJobContext, + playlistId: RundownPlaylistId, + rundownId: RundownId, + showStyle: ReadonlyDeep + ): Promise => { + const sourceLayerIds = Object.keys(showStyle.sourceLayers) + + await setupRundownBase( + context, + playlistId, + rundownId, + showStyle, + {}, + { + piece0: { prerollDuration: 500 }, + piece1: { + prerollDuration: 50, + sourceLayerId: sourceLayerIds[3], + lifespan: PieceLifespan.OutOnSegmentEnd, + }, + } + ) + + return rundownId + }, + async (playlistId, rundownId, parts, getPartInstances, checkTimings) => { + // Take the only Part: + await doTakePart(context, playlistId, null, parts[0]._id, null) + + const { currentPartInstance } = await getPartInstances() + expect(currentPartInstance).toBeTruthy() + if (!currentPartInstance) throw new Error('currentPartInstance must be defined') + + // Should look normal for now + await checkTimings({ + previousPart: null, + currentPieces: { + piece000: { + // This one gave the preroll + controlObj: { + start: 500, + }, + childGroup: { + preroll: 500, + postroll: 0, + }, + }, + }, + currentInfinitePieces: { + piece001: { + pieceGroup: { + childGroup: { + preroll: 50, + postroll: 0, + }, + controlObj: { + start: 500, + }, + }, + partGroup: { + start: `#part_group_${currentPartInstance.partInstance._id}.start`, + }, + }, + }, + previousOutTransition: undefined, + }) + + const currentPieceInstances = currentPartInstance.pieceInstances + const pieceInstance0 = currentPieceInstances.find( + (instance) => instance.pieceInstance.piece._id === protectString(`${rundownId}_piece000`) + ) + if (!pieceInstance0) throw new Error('pieceInstance0 must be defined') + const pieceInstance1 = currentPieceInstances.find( + (instance) => instance.pieceInstance.piece._id === protectString(`${rundownId}_piece001`) + ) + if (!pieceInstance1) throw new Error('pieceInstance1 must be defined') + + const currentTime = 12300 + await doOnPlayoutPlaybackChanged(context, playlistId, { + baseTime: currentTime, + partId: currentPartInstance.partInstance._id, + includePart: true, + pieceOffsets: { + [unprotectString(pieceInstance0.pieceInstance._id)]: 500, + [unprotectString(pieceInstance1.pieceInstance._id)]: 500, + }, + }) + + await doUpdateTimeline(context, playlistId) + + await checkTimings({ + previousPart: null, + currentPieces: { + piece000: { controlObj: { start: 500, }, + childGroup: { + preroll: 500, + postroll: 0, + }, }, - partGroup: { - start: currentTime, // same as the partGroup, note that this counteracts the offset in onPlayoutPlaybackChanged + }, + currentInfinitePieces: { + piece001: { + pieceGroup: { + childGroup: { + preroll: 50, + postroll: 0, + }, + controlObj: { + start: 500, + }, + }, + partGroup: { + start: currentTime, // same as the partGroup, note that this counteracts the offset in onPlayoutPlaybackChanged + }, + }, + }, + previousOutTransition: undefined, + }) + } + )) + }) + }) + describe('Multi-gateway mode', () => { + let context: MockJobContext + let showStyle: ReadonlyDeep + + const playoutLatency = 5 + const latencySafetyMargin = 10 + + beforeEach(async () => { + restartRandomId() + + context = setupDefaultJobEnvironment(undefined, { + forceMultiGatewayMode: true, + multiGatewayNowSafeLatency: latencySafetyMargin, + }) + + useFakeCurrentTime(10000) + + showStyle = await setupMockShowStyleCompound(context) + + // Ignore calls to queueEventJob, they are expected + context.queueEventJob = async () => Promise.resolve() + }) + afterEach(() => { + useRealCurrentTime() + }) + + /** + * Perform a test to check how a transition is formed on the timeline. + * This simulates two takes then allows for analysis of the state. + * @param name Name of the test + * @param customRundownFactory Factory to produce the rundown to play + * @param checkFcn Function used to check the resulting timeline + * @param timeout Override the timeout of the test + */ + function testTransitionTimings( + name: string, + customRundownFactory: ( + context: MockJobContext, + playlistId: RundownPlaylistId, + rundownId: RundownId, + showStyle: ReadonlyDeep + ) => Promise, + checkFcn: ( + rundownId: RundownId, + timeline: null, + currentPartInstance: DBPartInstance, + previousPartInstance: DBPartInstance, + checkTimings: (timings: PartTimelineTimings) => Promise, + previousTakeTime: number + ) => Promise, + timeout?: number + ) { + // eslint-disable-next-line jest/expect-expect + test( + // eslint-disable-next-line jest/valid-title + name, + async () => + runTimelineTimings( + customRundownFactory, + async (playlistId, rundownId, parts, _getPartInstances, checkTimings) => { + // Take the first Part: + const { currentPartInstance: currentPartInstance0 } = await doTakePart( + context, + playlistId, + null, + parts[0]._id, + parts[1]._id + ) + + const afterFirstTakeTime = getCurrentTime() + + // Report the first part as having started playback + await doAutoPlayoutPlaybackChangedForPart( + context, + playlistId, + currentPartInstance0!._id, + getCurrentTime() + ) + + // Take the second Part: + const { currentPartInstance, previousPartInstance } = await doTakePart( + context, + playlistId, + parts[0]._id, + parts[1]._id, + null + ) + + // Report the second part as having started playback + await doAutoPlayoutPlaybackChangedForPart( + context, + playlistId, + currentPartInstance!._id, + getCurrentTime() + ) + + // Run the result check + await checkFcn( + rundownId, + null, + currentPartInstance!, + previousPartInstance!, + checkTimings, + afterFirstTakeTime + playoutLatency + latencySafetyMargin + ) + } + ), + timeout + ) + } + + /** + * Perform a test to check how a timeline is formed + * This simulates two takes then allows for analysis of the state. + * @param customRundownFactory Factory to produce the rundown to play + * @param fcn Function to perform some playout operations and check the results + */ + async function runTimelineTimings( + customRundownFactory: ( + context: MockJobContext, + playlistId: RundownPlaylistId, + rundownId: RundownId, + showStyle: ReadonlyDeep + ) => Promise, + fcn: ( + playlistId: RundownPlaylistId, + rundownId: RundownId, + parts: DBPart[], + getPartInstances: () => Promise, + checkTimings: (timings: PartTimelineTimings) => Promise + ) => Promise + ) { + await setupMockPeripheralDevice( + context, + PeripheralDeviceCategory.PLAYOUT, + PeripheralDeviceType.PLAYOUT, + PERIPHERAL_SUBTYPE_PROCESS, + { + latencies: [playoutLatency], + } + ) + + const rundownId0: RundownId = getRandomId() + const playlistId0 = await context.mockCollections.RundownPlaylists.insertOne( + defaultRundownPlaylist(protectString('playlist_' + rundownId0), context.studioId) + ) + + const rundownId = await customRundownFactory(context, playlistId0, rundownId0, showStyle) + expect(rundownId0).toBe(rundownId) + + const rundown = (await context.directCollections.Rundowns.findOne(rundownId0)) as Rundown + expect(rundown).toBeTruthy() + + { + const playlist = (await context.directCollections.RundownPlaylists.findOne( + playlistId0 + )) as DBRundownPlaylist + expect(playlist).toBeTruthy() + + // Ensure this is defined to something, for the jest matcher + playlist.activationId = playlist.activationId ?? undefined + + expect(playlist).toMatchObject({ + activationId: undefined, + rehearsal: false, + }) + } + + const parts = await getSortedPartsForRundown(context, rundown._id) + + // Prepare and activate in rehersal: + await doActivatePlaylist(context, playlistId0, parts[0]._id) + + const getPartInstances = async (): Promise => { + const playlist = (await context.directCollections.RundownPlaylists.findOne( + playlistId0 + )) as DBRundownPlaylist + expect(playlist).toBeTruthy() + const res = await getSelectedPartInstances(context, playlist) + + async function wrapPartInstance( + partInstance: DBPartInstance | null + ): Promise { + if (!partInstance) return undefined + + const pieceInstances = await context.directCollections.PieceInstances.findFetch({ + partInstanceId: partInstance?._id, + }) + return new PlayoutPartInstanceModelImpl( + partInstance, + pieceInstances, + false, + mock() + ) + } + + return { + currentPartInstance: await wrapPartInstance(res.currentPartInstance), + nextPartInstance: await wrapPartInstance(res.nextPartInstance), + previousPartInstance: await wrapPartInstance(res.previousPartInstance), + } + } + + const checkTimings = async (timings: PartTimelineTimings) => { + // Check the calculated timings + const timeline = await context.directCollections.Timelines.findOne(context.studio._id) + expect(timeline).toBeTruthy() + + const { currentPartInstance, previousPartInstance } = await getPartInstances() + return checkTimingsRaw(rundownId0, timeline, currentPartInstance!, previousPartInstance, timings) + } + + // Run the required steps + await fcn(playlistId0, rundownId0, parts, getPartInstances, checkTimings) + + // Deactivate rundown: + await doDeactivatePlaylist(context, playlistId0) + + const timelinesEnd = await context.directCollections.Timelines.findFetch() + expect(fixSnapshot(timelinesEnd)).toMatchSnapshot() + } + + describe('In transitions', () => { + testTransitionTimings( + 'inTransition with existing infinites', + setupRundownWithInTransitionExistingInfinite, + async ( + _rundownId0, + _timeline, + currentPartInstance, + _previousPartInstance, + checkTimings, + firstPartTakeTime + ) => { + await checkTimings({ + // old part is extended due to transition keepalive + previousPart: { end: `#${getPartGroupId(currentPartInstance)}.start + 1000` }, + currentPieces: { + // pieces are delayed by the content delay + piece010: { + controlObj: { start: 500 }, + childGroup: { preroll: 0, postroll: 0 }, + }, + // transition piece + piece011: { + controlObj: { start: 0 }, + childGroup: { preroll: 0, postroll: 0 }, + }, + }, + currentInfinitePieces: { + piece002: { + // Should still be based on the time of previousPart + partGroup: { start: firstPartTakeTime }, + pieceGroup: { + controlObj: { start: 500 }, + childGroup: { preroll: 0, postroll: 0 }, }, }, }, previousOutTransition: undefined, }) } - )) + ) + }) + + describe('Infinite Pieces', () => { + test('Infinite Piece has stable timing across timeline regenerations after onPlayoutPlaybackChanged', async () => + runTimelineTimings( + async ( + context: MockJobContext, + playlistId: RundownPlaylistId, + rundownId: RundownId, + showStyle: ReadonlyDeep + ): Promise => { + const sourceLayerIds = Object.keys(showStyle.sourceLayers) + + await setupRundownBase( + context, + playlistId, + rundownId, + showStyle, + {}, + { + piece0: { prerollDuration: 500 }, + piece1: { + prerollDuration: 50, + sourceLayerId: sourceLayerIds[3], + lifespan: PieceLifespan.OutOnSegmentEnd, + }, + } + ) + + return rundownId + }, + async (playlistId, rundownId, parts, getPartInstances, checkTimings) => { + useFakeCurrentTime(10000) + + // Take the only Part: + await doTakePart(context, playlistId, null, parts[0]._id, null) // this moves 1500ms forward in fake time before doing a take + const afterTakeTime = 11500 // getCurrentTime() + const plannedStartedPlayback = afterTakeTime + playoutLatency + latencySafetyMargin // 11515 = 11500 + 5 + 10 + + const { currentPartInstance } = await getPartInstances() + expect(currentPartInstance).toBeTruthy() + if (!currentPartInstance) throw new Error('currentPartInstance must be defined') + + const expectedTimeline = { + previousPart: null, + currentPieces: { + piece000: { + // This one gave the preroll + controlObj: { + start: 500, + }, + childGroup: { + preroll: 500, + postroll: 0, + }, + }, + }, + currentInfinitePieces: { + piece001: { + pieceGroup: { + childGroup: { + preroll: 50, + postroll: 0, + }, + controlObj: { + start: 500, + }, + }, + partGroup: { + start: plannedStartedPlayback, + }, + }, + }, + previousOutTransition: undefined, + } as const + + // Should look normal for now + await checkTimings(expectedTimeline) + + await doUpdateTimeline(context, playlistId) + + // Should look normal for now + await checkTimings(expectedTimeline) + + const currentPieceInstances = currentPartInstance.pieceInstances + const pieceInstance0 = currentPieceInstances.find( + (instance) => instance.pieceInstance.piece._id === protectString(`${rundownId}_piece000`) + ) + if (!pieceInstance0) throw new Error('pieceInstance0 must be defined') + const pieceInstance1 = currentPieceInstances.find( + (instance) => instance.pieceInstance.piece._id === protectString(`${rundownId}_piece001`) + ) + if (!pieceInstance1) throw new Error('pieceInstance1 must be defined') + + // actual playback starts a bit later than planned + const actualStartedPlayback = plannedStartedPlayback + 3 + + // reception of the onPlayoutPlaybackChanged event also takes some time + adjustFakeTime(200) + + await doOnPlayoutPlaybackChanged(context, playlistId, { + baseTime: actualStartedPlayback, + partId: currentPartInstance.partInstance._id, + includePart: true, + pieceOffsets: { + [unprotectString(pieceInstance0.pieceInstance._id)]: 500, + [unprotectString(pieceInstance1.pieceInstance._id)]: 500, + }, + }) + + await doUpdateTimeline(context, playlistId) + + await checkTimings(expectedTimeline) + } + )) + }) }) }) diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index fa85af74050..66217e8a42a 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -76,21 +76,7 @@ export async function executeAdlibActionAndSaveModel( throw UserError.create(UserErrorMessage.ActionsNotSupported) } - const [adLibAction, baselineAdLibAction, bucketAdLibAction] = await Promise.all([ - context.directCollections.AdLibActions.findOne(data.actionDocId as AdLibActionId, { - projection: { _id: 1, privateData: 1 }, - }), - context.directCollections.RundownBaselineAdLibActions.findOne( - data.actionDocId as RundownBaselineAdLibActionId, - { - projection: { _id: 1, privateData: 1 }, - } - ), - context.directCollections.BucketAdLibActions.findOne(data.actionDocId as BucketAdLibActionId, { - projection: { _id: 1, privateData: 1 }, - }), - ]) - const adLibActionDoc = adLibAction ?? baselineAdLibAction ?? bucketAdLibAction + const adLibActionDoc = await findActionDoc(context, data) if (adLibActionDoc && adLibActionDoc.invalid) throw UserError.from( @@ -202,6 +188,26 @@ export interface ExecuteActionParameters { triggerMode: string | undefined } +async function findActionDoc(context: JobContext, data: ExecuteActionProps) { + if (data.actionDocId === null) return undefined + + const [adLibAction, baselineAdLibAction, bucketAdLibAction] = await Promise.all([ + context.directCollections.AdLibActions.findOne(data.actionDocId as AdLibActionId, { + projection: { _id: 1, privateData: 1, publicData: 1 }, + }), + context.directCollections.RundownBaselineAdLibActions.findOne( + data.actionDocId as RundownBaselineAdLibActionId, + { + projection: { _id: 1, privateData: 1, publicData: 1 }, + } + ), + context.directCollections.BucketAdLibActions.findOne(data.actionDocId as BucketAdLibActionId, { + projection: { _id: 1, privateData: 1, publicData: 1 }, + }), + ]) + return adLibAction ?? baselineAdLibAction ?? bucketAdLibAction +} + export async function executeActionInner( context: JobContext, playoutModel: PlayoutModel, @@ -241,7 +247,10 @@ export async function executeActionInner( ) try { - const blueprintPersistentState = new PersistentPlayoutStateStore(playoutModel.playlist.previousPersistentState) + const blueprintPersistentState = new PersistentPlayoutStateStore( + playoutModel.playlist.privatePlayoutPersistentState, + playoutModel.playlist.publicPlayoutPersistentState + ) await blueprint.blueprint.executeAction( actionContext, @@ -254,9 +263,7 @@ export async function executeActionInner( actionParameters.actionOptions ?? {} ) - if (blueprintPersistentState.hasChanges) { - playoutModel.setBlueprintPersistentState(blueprintPersistentState.getAll()) - } + blueprintPersistentState.saveToModel(playoutModel) } catch (err) { logger.error(`Error in showStyleBlueprint.executeAction: ${stringifyError(err)}`) throw UserError.fromUnknown(err) diff --git a/packages/job-worker/src/playout/infinites.ts b/packages/job-worker/src/playout/infinites.ts index 0f149931bab..ce938f43bc5 100644 --- a/packages/job-worker/src/playout/infinites.ts +++ b/packages/job-worker/src/playout/infinites.ts @@ -158,13 +158,13 @@ export async function fetchPiecesThatMayBeActiveForPart( ): Promise[]> { const span = context.startSpan('fetchPiecesThatMayBeActiveForPart') - const piecePromises: Array> | Array>> = [] + const piecePromises: Array | Array>>> = [] // Find all the pieces starting in the part const thisPiecesQuery = buildPiecesStartingInThisPartQuery(part) piecePromises.push( unsavedIngestModel?.rundownId === part.rundownId - ? unsavedIngestModel.getAllPieces().filter((p) => mongoWhere(p, thisPiecesQuery)) + ? Promise.resolve(unsavedIngestModel.getAllPieces().filter((p) => mongoWhere(p, thisPiecesQuery))) : context.directCollections.Pieces.findFetch(thisPiecesQuery) ) @@ -181,7 +181,9 @@ export async function fetchPiecesThatMayBeActiveForPart( [] // other rundowns don't exist in the ingestModel ) if (thisRundownPieceQuery) { - piecePromises.push(unsavedIngestModel.getAllPieces().filter((p) => mongoWhere(p, thisRundownPieceQuery))) + piecePromises.push( + Promise.resolve(unsavedIngestModel.getAllPieces().filter((p) => mongoWhere(p, thisRundownPieceQuery))) + ) } // Find pieces for the previous rundowns diff --git a/packages/job-worker/src/playout/lookahead/__tests__/__snapshots__/lookahead.test.ts.snap b/packages/job-worker/src/playout/lookahead/__tests__/__snapshots__/lookahead.test.ts.snap index 2c5ef924bf6..3d74422f49a 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/__snapshots__/lookahead.test.ts.snap +++ b/packages/job-worker/src/playout/lookahead/__tests__/__snapshots__/lookahead.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`Lookahead got some objects 1`] = ` [ diff --git a/packages/job-worker/src/playout/lookahead/__tests__/findForLayer.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer.test.ts deleted file mode 100644 index 643d95b66e8..00000000000 --- a/packages/job-worker/src/playout/lookahead/__tests__/findForLayer.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { findLookaheadForLayer } from '../findForLayer.js' -import { PartAndPieces, PartInstanceAndPieceInstances } from '../util.js' -import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' -import { getRandomString } from '@sofie-automation/corelib/dist/lib' -import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { setupDefaultJobEnvironment } from '../../../__mocks__/context.js' - -jest.mock('../findObjects') -import { findLookaheadObjectsForPart } from '../findObjects.js' -import { ReadonlyDeep } from 'type-fest' -type TfindLookaheadObjectsForPart = jest.MockedFunction -const findLookaheadObjectsForPartMock = findLookaheadObjectsForPart as TfindLookaheadObjectsForPart -findLookaheadObjectsForPartMock.mockImplementation(() => []) // Default mock - -describe('findLookaheadForLayer', () => { - const context = setupDefaultJobEnvironment() - - test('no data', () => { - const res = findLookaheadForLayer(context, null, [], undefined, [], 'abc', 1, 1) - expect(res.timed).toHaveLength(0) - expect(res.future).toHaveLength(0) - }) - - function expectInstancesToMatch( - index: number, - layer: string, - partInstanceInfo: PartInstanceAndPieceInstances, - previousPart: PartInstanceAndPieceInstances | undefined - ): void { - expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( - index, - context, - null, - layer, - previousPart?.part.part, - { - part: partInstanceInfo.part.part, - usesInTransition: false, - pieces: partInstanceInfo.allPieces, - }, - partInstanceInfo.part._id - ) - } - - function createFakePiece(id: string): PieceInstance { - return { - _id: id, - piece: { - enable: { - start: 0, - }, - }, - } as any - } - - test('partInstances', () => { - const layer = getRandomString() - - const partInstancesInfo: PartInstanceAndPieceInstances[] = [ - { - part: { _id: '1', part: '1p' }, - allPieces: [createFakePiece('1'), createFakePiece('2'), createFakePiece('3')], - onTimeline: true, - nowInPart: 2000, - calculatedTimings: { inTransitionStart: null }, - }, - { - part: { _id: '2', part: '2p' }, - allPieces: [createFakePiece('4'), createFakePiece('5'), createFakePiece('6')], - onTimeline: true, - nowInPart: 1000, - calculatedTimings: { inTransitionStart: null }, - }, - { - part: { _id: '3', part: '3p' }, - allPieces: [createFakePiece('7'), createFakePiece('8'), createFakePiece('9')], - onTimeline: false, - nowInPart: 0, - calculatedTimings: { inTransitionStart: null }, - }, - ] as any - - findLookaheadObjectsForPartMock - .mockReset() - .mockReturnValue([]) - .mockReturnValueOnce(['t0', 't1'] as any) - .mockReturnValueOnce(['t2', 't3'] as any) - .mockReturnValueOnce(['t4', 't5'] as any) - - // Run it - const res = findLookaheadForLayer(context, null, partInstancesInfo, undefined, [], layer, 1, 1) - expect(res.timed).toEqual(['t0', 't1', 't2', 't3']) - expect(res.future).toEqual(['t4', 't5']) - - // Check the mock was called correctly - expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(3) - expectInstancesToMatch(1, layer, partInstancesInfo[0], undefined) - expectInstancesToMatch(2, layer, partInstancesInfo[1], partInstancesInfo[0]) - expectInstancesToMatch(3, layer, partInstancesInfo[2], partInstancesInfo[1]) - - // Check a previous part gets propogated - const previousPartInfo: PartInstanceAndPieceInstances = { - part: { _id: '5', part: '5p' }, - pieces: [createFakePiece('10'), createFakePiece('11'), createFakePiece('12')], - onTimeline: true, - } as any - findLookaheadObjectsForPartMock.mockReset().mockReturnValue([]) - findLookaheadForLayer(context, null, partInstancesInfo, previousPartInfo, [], layer, 1, 1) - expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(3) - expectInstancesToMatch(1, layer, partInstancesInfo[0], previousPartInfo) - - // Max search distance of 0 should ignore any not on the timeline - findLookaheadObjectsForPartMock - .mockReset() - .mockReturnValue([]) - .mockReturnValueOnce(['t0', 't1'] as any) - .mockReturnValueOnce(['t2', 't3'] as any) - .mockReturnValueOnce(['t4', 't5'] as any) - - const res2 = findLookaheadForLayer(context, null, partInstancesInfo, undefined, [], layer, 1, 0) - expect(res2.timed).toEqual(['t0', 't1', 't2', 't3']) - expect(res2.future).toHaveLength(0) - expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2) - expectInstancesToMatch(1, layer, partInstancesInfo[0], undefined) - expectInstancesToMatch(2, layer, partInstancesInfo[1], partInstancesInfo[0]) - }) - - function expectPartToMatch( - index: number, - layer: string, - partInfo: PartAndPieces, - previousPart: ReadonlyDeep | undefined - ): void { - expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( - index, - context, - null, - layer, - previousPart, - partInfo, - null - ) - } - - test('parts', () => { - const layer = getRandomString() - - const orderedParts: PartAndPieces[] = [ - { _id: 'p1' }, - { _id: 'p2', invalid: true }, - { _id: 'p3' }, - { _id: 'p4' }, - { _id: 'p5' }, - ].map((p) => ({ - part: p as any, - usesInTransition: true, - pieces: [{ _id: p._id + '_p1' } as any], - })) - - findLookaheadObjectsForPartMock - .mockReset() - .mockReturnValue([]) - .mockReturnValueOnce(['t0', 't1'] as any) - .mockReturnValueOnce(['t2', 't3'] as any) - .mockReturnValueOnce(['t4', 't5'] as any) - .mockReturnValueOnce(['t6', 't7'] as any) - .mockReturnValueOnce(['t8', 't9'] as any) - - // Cant search far enough - const res = findLookaheadForLayer(context, null, [], undefined, orderedParts, layer, 1, 1) - expect(res.timed).toHaveLength(0) - expect(res.future).toHaveLength(0) - expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(0) - - // Find the target of 1 - const res2 = findLookaheadForLayer(context, null, [], undefined, orderedParts, layer, 1, 4) - expect(res2.timed).toHaveLength(0) - expect(res2.future).toEqual(['t0', 't1']) - expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(1) - expectPartToMatch(1, layer, orderedParts[0], undefined) - - // Find the target of 0 - findLookaheadObjectsForPartMock.mockReset().mockReturnValue([]) - const res3 = findLookaheadForLayer(context, null, [], undefined, orderedParts, layer, 0, 4) - expect(res3.timed).toHaveLength(0) - expect(res3.future).toHaveLength(0) - expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(0) - - // Search max distance - findLookaheadObjectsForPartMock - .mockReset() - .mockReturnValue([]) - .mockReturnValueOnce(['t0', 't1'] as any) - .mockReturnValueOnce(['t2', 't3'] as any) - .mockReturnValueOnce(['t4', 't5'] as any) - .mockReturnValueOnce(['t6', 't7'] as any) - .mockReturnValueOnce(['t8', 't9'] as any) - - const res4 = findLookaheadForLayer(context, null, [], undefined, orderedParts, layer, 100, 5) - expect(res4.timed).toHaveLength(0) - expect(res4.future).toEqual(['t0', 't1', 't2', 't3', 't4', 't5']) - expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(3) - expectPartToMatch(1, layer, orderedParts[0], undefined) - expectPartToMatch(2, layer, orderedParts[2], orderedParts[0].part) - expectPartToMatch(3, layer, orderedParts[3], orderedParts[2].part) - }) -}) diff --git a/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/basicBehavior.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/basicBehavior.test.ts new file mode 100644 index 00000000000..7a47a4bca87 --- /dev/null +++ b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/basicBehavior.test.ts @@ -0,0 +1,43 @@ +jest.mock('../../findObjects') +import { context, TfindLookaheadObjectsForPart } from './helpers/mockSetup.js' +import { findLookaheadForLayer } from '../../findForLayer.js' +import { expectInstancesToMatch } from '../utils.js' +import { findForLayerTestConstants } from './constants.js' +import { findLookaheadObjectsForPart } from '../../findObjects.js' + +const findLookaheadObjectsForPartMockBase = findLookaheadObjectsForPart as TfindLookaheadObjectsForPart +const findLookaheadObjectsForPartMock = findLookaheadObjectsForPartMockBase.mockImplementation(() => []) // Default mock + +beforeEach(() => { + findLookaheadObjectsForPartMock.mockReset() +}) + +const current = findForLayerTestConstants.current +const nextFuture = findForLayerTestConstants.nextFuture +const layer = findForLayerTestConstants.layer +const onAirPlayoutState = findForLayerTestConstants.playoutState.onAir + +describe('findLookaheadForLayer – basic behavior', () => { + test('no parts', () => { + const res = findLookaheadForLayer(context, {}, [], 'abc', 1, 1, onAirPlayoutState) + + expect(res.timed).toHaveLength(0) + expect(res.future).toHaveLength(0) + }) + test('if the previous part is unset', () => { + findLookaheadObjectsForPartMock.mockReturnValue([]) + + findLookaheadForLayer( + context, + { previous: undefined, current, next: nextFuture }, + [], + layer, + 1, + 1, + onAirPlayoutState + ) + + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2) + expectInstancesToMatch(findLookaheadObjectsForPartMock, 1, layer, current, undefined, onAirPlayoutState) + }) +}) diff --git a/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/constants.ts b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/constants.ts new file mode 100644 index 00000000000..e7dce409b77 --- /dev/null +++ b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/constants.ts @@ -0,0 +1,57 @@ +import { getRandomString } from '@sofie-automation/corelib/dist/lib' +import { PartInstanceAndPieceInstances, PartAndPieces } from '../../util.js' +import { createFakePiece } from '../utils.js' +import { TimelinePlayoutState } from '../../../timeline/lib.js' + +const layer: string = getRandomString() + +export const findForLayerTestConstants = { + playoutState: { + onAir: { isInHold: false, isRehearsal: false } as TimelinePlayoutState, + onAirIncludeNotInHold: { + isInHold: false, + isRehearsal: false, + includeWhenNotInHoldObjects: true, + } as TimelinePlayoutState, + inHoldOnAir: { isInHold: true, isRehearsal: false } as TimelinePlayoutState, + rehearsal: { isInHold: false, isRehearsal: true } as TimelinePlayoutState, + inHoldRehearsal: { isInHold: true, isRehearsal: true } as TimelinePlayoutState, + rehearsalIncludeNotInHold: { + isInHold: false, + isRehearsal: true, + includeWhenNotInHoldObjects: true, + } as TimelinePlayoutState, + }, + previous: { + part: { _id: 'pPrev', part: 'prev' }, + allPieces: [createFakePiece('1'), createFakePiece('2'), createFakePiece('3')], + onTimeline: true, + nowInPart: 2000, + } as any as PartInstanceAndPieceInstances, + current: { + part: { _id: 'pCur', part: 'cur' }, + allPieces: [createFakePiece('4'), createFakePiece('5'), createFakePiece('6')], + onTimeline: true, + nowInPart: 1000, + } as any as PartInstanceAndPieceInstances, + nextTimed: { + part: { _id: 'pNextTimed', part: 'nextT' }, + allPieces: [createFakePiece('7'), createFakePiece('8'), createFakePiece('9')], + onTimeline: true, + } as any as PartInstanceAndPieceInstances, + nextFuture: { + part: { _id: 'pNextFuture', part: 'nextF' }, + allPieces: [createFakePiece('10'), createFakePiece('11'), createFakePiece('12')], + onTimeline: false, + } as any as PartInstanceAndPieceInstances, + + orderedParts: [{ _id: 'p1' }, { _id: 'p2', invalid: true }, { _id: 'p3' }, { _id: 'p4' }, { _id: 'p5' }].map( + (p) => ({ + part: p as any, + usesInTransition: true, + pieces: [{ _id: p._id + '_p1' } as any], + }) + ) as PartAndPieces[], + + layer, +} diff --git a/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/helpers/mockSetup.ts b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/helpers/mockSetup.ts new file mode 100644 index 00000000000..40b1d4e423b --- /dev/null +++ b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/helpers/mockSetup.ts @@ -0,0 +1,6 @@ +import { setupDefaultJobEnvironment } from '../../../../../__mocks__/context.js' +import { findLookaheadObjectsForPart } from '../../../../../playout/lookahead/findObjects.js' + +export type TfindLookaheadObjectsForPart = jest.MockedFunction + +export const context = setupDefaultJobEnvironment() diff --git a/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/orderedParts.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/orderedParts.test.ts new file mode 100644 index 00000000000..5353697dc8c --- /dev/null +++ b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/orderedParts.test.ts @@ -0,0 +1,113 @@ +import { findLookaheadForLayer } from '../../findForLayer.js' +import { setupDefaultJobEnvironment } from '../../../../__mocks__/context.js' + +jest.mock('../../findObjects') +import { findForLayerTestConstants } from './constants.js' +import { expectPartToMatch } from '../utils.js' +import { findLookaheadObjectsForPart } from '../../findObjects.js' +import { TfindLookaheadObjectsForPart } from './helpers/mockSetup.js' + +const findLookaheadObjectsForPartMockBase = findLookaheadObjectsForPart as TfindLookaheadObjectsForPart +const findLookaheadObjectsForPartMock = findLookaheadObjectsForPartMockBase.mockImplementation(() => []) // Default mock + +beforeEach(() => { + findLookaheadObjectsForPartMock.mockReset() +}) + +const orderedParts = findForLayerTestConstants.orderedParts +const layer = findForLayerTestConstants.layer +const onAirPlayoutState = findForLayerTestConstants.playoutState.onAir + +// All future parts get modified playoutState (isInHold forced to false, includeWhenNotInHoldObjects added) +// This behavior is unrelated to these tests, but it is expected and also verified in playoutStatePropagation.test.ts. +const onAirIncludeNotInHoldPlayoutState = findForLayerTestConstants.playoutState.onAirIncludeNotInHold + +describe('findLookaheadForLayer - orderedParts', () => { + beforeEach(() => { + findLookaheadObjectsForPartMock.mockReset() + }) + + const context = setupDefaultJobEnvironment() + + test('finds lookahead for target index 1', () => { + findLookaheadObjectsForPartMock + .mockReturnValue([]) + .mockReturnValueOnce(['t0', 't1'] as any) + .mockReturnValueOnce(['t2', 't3'] as any) + .mockReturnValueOnce(['t4', 't5'] as any) + .mockReturnValueOnce(['t6', 't7'] as any) + .mockReturnValueOnce(['t8', 't9'] as any) + + const res2 = findLookaheadForLayer(context, {}, orderedParts, layer, 1, 4, onAirPlayoutState, null) + + expect(res2.timed).toHaveLength(0) + expect(res2.future).toEqual(['t0', 't1']) + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(1) + + expectPartToMatch( + findLookaheadObjectsForPartMock, + 1, + layer, + orderedParts[0], + undefined, + null, + onAirIncludeNotInHoldPlayoutState + ) + }) + + test('returns nothing when target index is 0', () => { + findLookaheadObjectsForPartMock.mockReturnValue([]) + + const res3 = findLookaheadForLayer(context, {}, orderedParts, layer, 0, 4, onAirPlayoutState, null) + + expect(res3.timed).toHaveLength(0) + expect(res3.future).toHaveLength(0) + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(0) + }) + + test('searches across maximum search distance', () => { + findLookaheadObjectsForPartMock + .mockReturnValue([]) // default value + .mockReturnValueOnce(['t0', 't1'] as any) // 1st part + .mockReturnValueOnce(['t2', 't3'] as any) // 2nd part + .mockReturnValueOnce(['t4', 't5'] as any) // 3rd part + .mockReturnValueOnce(['t6', 't7'] as any) // 4th part + .mockReturnValueOnce(['t8', 't9'] as any) // 5th part - we shouldn't see objects from this one due to the maximum search distance + + const res4 = findLookaheadForLayer(context, {}, orderedParts, layer, 100, 5, onAirPlayoutState, null) + + expect(res4.timed).toHaveLength(0) + expect(res4.future).toEqual(['t0', 't1', 't2', 't3', 't4', 't5', 't6', 't7']) + + // Called for parts: [0], [2], [3] + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(4) + + expectPartToMatch( + findLookaheadObjectsForPartMock, + 1, + layer, + orderedParts[0], + undefined, + null, + onAirIncludeNotInHoldPlayoutState + ) + expectPartToMatch( + findLookaheadObjectsForPartMock, + 2, + layer, + orderedParts[2], + orderedParts[0].part, + null, + onAirIncludeNotInHoldPlayoutState + ) + expectPartToMatch( + findLookaheadObjectsForPartMock, + 3, + layer, + orderedParts[3], + orderedParts[2].part, + null, + onAirIncludeNotInHoldPlayoutState + ) + }) +}) diff --git a/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/playoutStatePropagation.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/playoutStatePropagation.test.ts new file mode 100644 index 00000000000..2de1a7c9cd9 --- /dev/null +++ b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/playoutStatePropagation.test.ts @@ -0,0 +1,125 @@ +jest.mock('../../findObjects') +import { context, TfindLookaheadObjectsForPart } from './helpers/mockSetup.js' +import { findLookaheadForLayer, PartInstanceAndPieceInstancesInfos } from '../../findForLayer.js' +import { findForLayerTestConstants } from './constants.js' +import { findLookaheadObjectsForPart } from '../../findObjects.js' +import { PartAndPieces } from '../../util.js' + +const findLookaheadObjectsForPartMockBase = findLookaheadObjectsForPart as TfindLookaheadObjectsForPart +const findLookaheadObjectsForPartMock = findLookaheadObjectsForPartMockBase.mockImplementation(() => []) // Default mock + +beforeEach(() => { + findLookaheadObjectsForPartMock.mockReset().mockReturnValue([]) +}) + +const current = findForLayerTestConstants.current +const nextFuture = findForLayerTestConstants.nextFuture +const layer = findForLayerTestConstants.layer +const playoutState = findForLayerTestConstants.playoutState + +describe('playoutState propagates to findLookaheadObjectsForPart', () => { + test('onAir inHold propagation for partInstances (current and next)', () => { + const partInstancesInfo: PartInstanceAndPieceInstancesInfos = { + current, + next: nextFuture, + } + + findLookaheadForLayer(context, partInstancesInfo, [], layer, 1, 1, playoutState.inHoldOnAir) + + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2) + + // current + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + 1, + context, + partInstancesInfo.current?.part._id, + layer, + undefined, + expect.any(Object), + partInstancesInfo.current?.part._id, + playoutState.inHoldOnAir + ) + // next + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + 2, + context, + partInstancesInfo.current?.part._id, + layer, + partInstancesInfo.current?.part.part, + expect.any(Object), + partInstancesInfo.next?.part._id, + playoutState.inHoldOnAir + ) + }) + test('Rehearsal propagation for partInstances (current and next)', () => { + const partInstancesInfo: PartInstanceAndPieceInstancesInfos = { + current, + next: nextFuture, + } + + findLookaheadForLayer(context, partInstancesInfo, [], layer, 1, 1, playoutState.rehearsal) + + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2) + + // current + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + 1, + context, + partInstancesInfo.current?.part._id, + layer, + undefined, + expect.any(Object), + partInstancesInfo.current?.part._id, + { isInHold: false, isRehearsal: true } + ) + // next + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + 2, + context, + partInstancesInfo.current?.part._id, + layer, + partInstancesInfo.current?.part.part, + expect.any(Object), + partInstancesInfo.next?.part._id, + { isInHold: false, isRehearsal: true } + ) + }) + + test('Rehearsal inHold propagation future parts always get isInHold: false with includeWhenNotInHoldObjects: true', () => { + const maxSearchDistance = 5 + const rehearsalInHoldOrderedParts: PartAndPieces[] = [{ _id: 'p1' }, { _id: 'p2' }].map((p) => ({ + part: p as any, + usesInTransition: true, + pieces: [{ _id: p._id + '_p1' } as any], + })) + + findLookaheadForLayer( + context, + {}, + rehearsalInHoldOrderedParts, + layer, + 100, + maxSearchDistance, + playoutState.inHoldRehearsal + ) + + const expectedCalls = Math.min(maxSearchDistance, rehearsalInHoldOrderedParts.length) + + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(expectedCalls) + + // All futures that are considered (limited by the maxSearchDistance) + for (let i = 0; i < expectedCalls; i++) { + // All future parts get modified playoutState (isInHold forced to false, includeWhenNotInHoldObjects added) + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + i + 1, + context, + null, + layer, + (i === 0 ? undefined : rehearsalInHoldOrderedParts[i - 1])?.part, + rehearsalInHoldOrderedParts[i], + null, + playoutState.rehearsalIncludeNotInHold + ) + } + }) +}) diff --git a/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/searchDistance.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/searchDistance.test.ts new file mode 100644 index 00000000000..f8352d2317d --- /dev/null +++ b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/searchDistance.test.ts @@ -0,0 +1,60 @@ +jest.mock('../../findObjects') +import { context, TfindLookaheadObjectsForPart } from './helpers/mockSetup.js' +import { findLookaheadForLayer } from '../../findForLayer.js' +import { expectInstancesToMatch } from '../utils.js' +import { findForLayerTestConstants } from './constants.js' +import { findLookaheadObjectsForPart } from '../../findObjects.js' + +const findLookaheadObjectsForPartMockBase = findLookaheadObjectsForPart as TfindLookaheadObjectsForPart +const findLookaheadObjectsForPartMock = findLookaheadObjectsForPartMockBase.mockImplementation(() => []) // Default mock + +beforeEach(() => { + findLookaheadObjectsForPartMock.mockReset() +}) + +const previous = findForLayerTestConstants.previous +const current = findForLayerTestConstants.current +const nextFuture = findForLayerTestConstants.nextFuture +const orderedParts = findForLayerTestConstants.orderedParts +const layer = findForLayerTestConstants.layer +const onAirPlayoutState = findForLayerTestConstants.playoutState.onAir + +describe('findLookaheadForLayer – search distance', () => { + test('searchDistance = 0 ignores future parts', () => { + findLookaheadObjectsForPartMock.mockReturnValueOnce(['cur0', 'cur1'] as any) + + const res = findLookaheadForLayer( + context, + { previous, current, next: nextFuture }, + orderedParts, + layer, + 1, + 0, + onAirPlayoutState, + null + ) + + expect(res.timed).toEqual(['cur0', 'cur1']) + expect(res.future).toHaveLength(0) + + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2) + expectInstancesToMatch(findLookaheadObjectsForPartMock, 1, layer, current, previous, onAirPlayoutState) + expectInstancesToMatch(findLookaheadObjectsForPartMock, 2, layer, nextFuture, current, onAirPlayoutState) + }) + + test('returns nothing when maxSearchDistance is too small', () => { + findLookaheadObjectsForPartMock + .mockReturnValue([]) + .mockReturnValueOnce(['t0', 't1'] as any) + .mockReturnValueOnce(['t2', 't3'] as any) + .mockReturnValueOnce(['t4', 't5'] as any) + .mockReturnValueOnce(['t6', 't7'] as any) + .mockReturnValueOnce(['t8', 't9'] as any) + + const res = findLookaheadForLayer(context, {}, orderedParts, layer, 1, 1, onAirPlayoutState, null) + + expect(res.timed).toHaveLength(0) + expect(res.future).toHaveLength(0) + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(0) + }) +}) diff --git a/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/timing.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/timing.test.ts new file mode 100644 index 00000000000..77e40ffb877 --- /dev/null +++ b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer/timing.test.ts @@ -0,0 +1,70 @@ +jest.mock('../../findObjects') +import { context, TfindLookaheadObjectsForPart } from './helpers/mockSetup.js' +import { findLookaheadForLayer } from '../../findForLayer.js' +import { expectInstancesToMatch } from '../utils.js' +import { findForLayerTestConstants } from './constants.js' +import { findLookaheadObjectsForPart } from '../../findObjects.js' + +const findLookaheadObjectsForPartMockBase = findLookaheadObjectsForPart as TfindLookaheadObjectsForPart +const findLookaheadObjectsForPartMock = findLookaheadObjectsForPartMockBase.mockImplementation(() => []) // Default mock +const onAirPlayoutState = findForLayerTestConstants.playoutState.onAir + +beforeEach(() => { + findLookaheadObjectsForPartMock.mockReset() +}) + +const previous = findForLayerTestConstants.previous +const current = findForLayerTestConstants.current +const nextTimed = findForLayerTestConstants.nextTimed +const nextFuture = findForLayerTestConstants.nextFuture +const layer = findForLayerTestConstants.layer + +describe('findLookaheadForLayer – timing', () => { + test('current part with timed next part (all goes into timed)', () => { + findLookaheadObjectsForPartMock + .mockReturnValueOnce(['cur0', 'cur1'] as any) + .mockReturnValueOnce(['nT0', 'nT1'] as any) + + const res = findLookaheadForLayer( + context, + { previous, current, next: nextTimed }, + [], + layer, + 1, + 1, + onAirPlayoutState, + null + ) + + expect(res.timed).toEqual(['cur0', 'cur1', 'nT0', 'nT1']) // should have all pieces + expect(res.future).toHaveLength(0) // should be empty + + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2) + expectInstancesToMatch(findLookaheadObjectsForPartMock, 1, layer, current, previous, onAirPlayoutState) + expectInstancesToMatch(findLookaheadObjectsForPartMock, 2, layer, nextTimed, current, onAirPlayoutState) + }) + + test('current part with un-timed next part (next goes into future)', () => { + findLookaheadObjectsForPartMock + .mockReturnValueOnce(['cur0', 'cur1'] as any) + .mockReturnValueOnce(['nF0', 'nF1'] as any) + + const res = findLookaheadForLayer( + context, + { previous, current, next: nextFuture }, + [], + layer, + 1, + 1, + onAirPlayoutState, + null + ) + + expect(res.timed).toEqual(['cur0', 'cur1']) // Should only contain the current part's pieces + expect(res.future).toEqual(['nF0', 'nF1']) // Should only contain the future pieces + + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2) + expectInstancesToMatch(findLookaheadObjectsForPartMock, 1, layer, current, previous, onAirPlayoutState) + expectInstancesToMatch(findLookaheadObjectsForPartMock, 2, layer, nextFuture, current, onAirPlayoutState) + }) +}) diff --git a/packages/job-worker/src/playout/lookahead/__tests__/findObjects.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/findObjects.test.ts index 884ba4ff0ff..e884b4ce2e0 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/findObjects.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/findObjects.test.ts @@ -3,6 +3,8 @@ import { IBlueprintPieceType, OnGenerateTimelineObj, PieceLifespan, + TimelineObjHoldMode, + TimelineObjOnAirMode, TSR, } from '@sofie-automation/blueprints-integration' import { sortPieceInstancesByStart } from '../../pieces.js' @@ -19,6 +21,8 @@ import { serializePieceTimelineObjectsBlob, } from '@sofie-automation/corelib/dist/dataModel/Piece' +const DEFAULT_PLAYOUT_STATE = { isInHold: false, isRehearsal: false } + function stripObjectProperties( objs: Array>, keepContent?: boolean @@ -51,7 +55,8 @@ describe('findLookaheadObjectsForPart', () => { layerName, undefined, partInfo, - null + null, + DEFAULT_PLAYOUT_STATE ) expect(objects).toHaveLength(0) }) @@ -107,11 +112,27 @@ describe('findLookaheadObjectsForPart', () => { } // Empty layer - const objects = findLookaheadObjectsForPart(context, currentPartInstanceId, layer0, undefined, partInfo, null) + const objects = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + layer0, + undefined, + partInfo, + null, + DEFAULT_PLAYOUT_STATE + ) expect(objects).toHaveLength(0) // Layer has an object - const objects2 = findLookaheadObjectsForPart(context, currentPartInstanceId, layer1, undefined, partInfo, null) + const objects2 = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + layer1, + undefined, + partInfo, + null, + DEFAULT_PLAYOUT_STATE + ) expect(objects2).toHaveLength(1) }) @@ -146,7 +167,15 @@ describe('findLookaheadObjectsForPart', () => { } // Run for future part - const objects = findLookaheadObjectsForPart(context, currentPartInstanceId, layer0, undefined, partInfo, null) + const objects = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + layer0, + undefined, + partInfo, + null, + DEFAULT_PLAYOUT_STATE + ) expect(stripObjectProperties(objects)).toStrictEqual([ { id: 'obj0', @@ -164,7 +193,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, undefined, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects1)).toStrictEqual([ { @@ -177,7 +207,15 @@ describe('findLookaheadObjectsForPart', () => { ]) // Run for partInstance without the id - const objects2 = findLookaheadObjectsForPart(context, currentPartInstanceId, layer0, undefined, partInfo, null) + const objects2 = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + layer0, + undefined, + partInfo, + null, + DEFAULT_PLAYOUT_STATE + ) expect(stripObjectProperties(objects2)).toStrictEqual([ { id: 'obj0', @@ -195,7 +233,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, undefined, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(objects3).toStrictEqual(objects1) }) @@ -254,7 +293,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, undefined, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects, true)).toStrictEqual([ { @@ -277,7 +317,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, previousPart, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects1, true)).toStrictEqual(stripObjectProperties(objects, true)) @@ -297,7 +338,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, previousPart, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects2, true)).toStrictEqual([ { @@ -321,7 +363,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, undefined, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects3, true)).toStrictEqual(stripObjectProperties(objects1, true)) @@ -333,7 +376,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, blockedPreviousPart, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects4, true)).toStrictEqual(stripObjectProperties(objects1, true)) }) @@ -389,7 +433,15 @@ describe('findLookaheadObjectsForPart', () => { } // Run for future part - const objects = findLookaheadObjectsForPart(context, currentPartInstanceId, layer0, undefined, partInfo, null) + const objects = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + layer0, + undefined, + partInfo, + null, + DEFAULT_PLAYOUT_STATE + ) expect(stripObjectProperties(objects)).toStrictEqual([ { id: 'obj0', @@ -414,7 +466,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, undefined, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects1)).toStrictEqual([ { @@ -434,7 +487,15 @@ describe('findLookaheadObjectsForPart', () => { ]) // Run for partInstance without the id - const objects2 = findLookaheadObjectsForPart(context, currentPartInstanceId, layer0, undefined, partInfo, null) + const objects2 = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + layer0, + undefined, + partInfo, + null, + DEFAULT_PLAYOUT_STATE + ) expect(stripObjectProperties(objects2)).toStrictEqual([ { id: 'obj0', @@ -459,7 +520,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, undefined, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects3)).toStrictEqual([ { @@ -560,7 +622,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, undefined, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects, true)).toStrictEqual([ { @@ -593,7 +656,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, previousPart, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects1, true)).toStrictEqual(stripObjectProperties(objects, true)) @@ -623,7 +687,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, previousPart, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects2, true)).toStrictEqual([ { @@ -658,7 +723,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, undefined, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects3, true)).toStrictEqual(stripObjectProperties(objects1, true)) @@ -670,7 +736,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, blockedPreviousPart, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects4, true)).toStrictEqual(stripObjectProperties(objects1, true)) }) @@ -781,7 +848,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, previousPart, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects2, true)).toStrictEqual([ { @@ -815,7 +883,8 @@ describe('findLookaheadObjectsForPart', () => { layer0, undefined, partInfo, - partInstanceId + partInstanceId, + DEFAULT_PLAYOUT_STATE ) expect(stripObjectProperties(objects3, true)).toStrictEqual([ { @@ -840,4 +909,164 @@ describe('findLookaheadObjectsForPart', () => { }, ]) }) + + test('playoutState filters objects with holdMode', () => { + const currentPartInstanceId: PartInstanceId | null = null + const rundownId: RundownId = protectString('rundown0') + const partInstanceId = protectString('partInstance0') + + function createPartInfoWithHoldMode(holdMode: TimelineObjHoldMode, layer: string): any { + return { + part: definePart(rundownId), + usesInTransition: true, + pieces: literal([ + { + ...defaultPieceInstanceProps, + _id: protectString('piece_' + Math.random()), + rundownId: rundownId, + piece: { + ...defaultPieceInstanceProps.piece, + content: {}, + timelineObjectsString: serializePieceTimelineObjectsBlob([ + { + id: 'obj_' + holdMode, + enable: { start: 0 }, + layer: layer, + content: { deviceType: TSR.DeviceType.ABSTRACT } as any, + holdMode: holdMode, + priority: 0, + }, + ]), + }, + }, + ]), + } + } + + // Test EXCEPT holdMode: should be included when NOT in hold, filtered when in hold + const exceptNotInHold = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + 'layer_except', + undefined, + createPartInfoWithHoldMode(TimelineObjHoldMode.EXCEPT, 'layer_except'), + partInstanceId, + { isInHold: false, isRehearsal: false } + ) + expect(exceptNotInHold).toHaveLength(1) + + const exceptInHold = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + 'layer_except', + undefined, + createPartInfoWithHoldMode(TimelineObjHoldMode.EXCEPT, 'layer_except'), + partInstanceId, + { isInHold: true, isRehearsal: false } + ) + expect(exceptInHold).toHaveLength(0) + + // Test ONLY holdMode: should be filtered when NOT in hold, included when in hold + const onlyNotInHold = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + 'layer_only', + undefined, + createPartInfoWithHoldMode(TimelineObjHoldMode.ONLY, 'layer_only'), + partInstanceId, + { isInHold: false, isRehearsal: false } + ) + expect(onlyNotInHold).toHaveLength(0) + + const onlyInHold = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + 'layer_only', + undefined, + createPartInfoWithHoldMode(TimelineObjHoldMode.ONLY, 'layer_only'), + partInstanceId, + { isInHold: true, isRehearsal: false } + ) + expect(onlyInHold).toHaveLength(1) + }) + + test('playoutState filters objects with onAirMode', () => { + const currentPartInstanceId: PartInstanceId | null = null + const rundownId: RundownId = protectString('rundown0') + const partInstanceId = protectString('partInstance0') + + function createPartInfoWithOnAirMode(onAirMode: TimelineObjOnAirMode, layer: string): any { + return { + part: definePart(rundownId), + usesInTransition: true, + pieces: literal([ + { + ...defaultPieceInstanceProps, + _id: protectString('piece_' + Math.random()), + rundownId: rundownId, + piece: { + ...defaultPieceInstanceProps.piece, + content: {}, + timelineObjectsString: serializePieceTimelineObjectsBlob([ + { + id: 'obj_' + onAirMode, + enable: { start: 0 }, + layer: layer, + content: { deviceType: TSR.DeviceType.ABSTRACT } as any, + onAirMode: onAirMode, + priority: 0, + }, + ]), + }, + }, + ]), + } + } + + // Test ONAIR onAirMode: should be included when NOT in rehearsal, filtered when in rehearsal + const onAirNotRehearsal = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + 'layer_onair', + undefined, + createPartInfoWithOnAirMode(TimelineObjOnAirMode.ONAIR, 'layer_onair'), + partInstanceId, + { isInHold: false, isRehearsal: false } + ) + expect(onAirNotRehearsal).toHaveLength(1) + + const onAirInRehearsal = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + 'layer_onair', + undefined, + createPartInfoWithOnAirMode(TimelineObjOnAirMode.ONAIR, 'layer_onair'), + partInstanceId, + { isInHold: false, isRehearsal: true } + ) + expect(onAirInRehearsal).toHaveLength(0) + + // Test REHEARSAL onAirMode: should be filtered when NOT in rehearsal, included when in rehearsal + const rehearsalNotInRehearsal = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + 'layer_rehearsal', + undefined, + createPartInfoWithOnAirMode(TimelineObjOnAirMode.REHEARSAL, 'layer_rehearsal'), + partInstanceId, + { isInHold: false, isRehearsal: false } + ) + expect(rehearsalNotInRehearsal).toHaveLength(0) + + const rehearsalInRehearsal = findLookaheadObjectsForPart( + context, + currentPartInstanceId, + 'layer_rehearsal', + undefined, + createPartInfoWithOnAirMode(TimelineObjOnAirMode.REHEARSAL, 'layer_rehearsal'), + partInstanceId, + { isInHold: false, isRehearsal: true } + ) + expect(rehearsalInRehearsal).toHaveLength(1) + }) }) diff --git a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts index 2e633843d9c..a515e2e0b4f 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts @@ -11,21 +11,23 @@ import { LookaheadMode, PlaylistTimingType, TSR } from '@sofie-automation/bluepr import { setupDefaultJobEnvironment, MockJobContext } from '../../../__mocks__/context.js' import { runJobWithPlayoutModel } from '../../../playout/lock.js' import { defaultRundownPlaylist } from '../../../__mocks__/defaultCollectionObjects.js' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { DBRundownPlaylist, RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' jest.mock('../findForLayer') type TfindLookaheadForLayer = jest.MockedFunction -import { findLookaheadForLayer } from '../findForLayer.js' +import { findLookaheadForLayer, PartInstanceAndPieceInstancesInfos } from '../findForLayer.js' const findLookaheadForLayerMock = findLookaheadForLayer as TfindLookaheadForLayer jest.mock('../util') type TgetOrderedPartsAfterPlayhead = jest.MockedFunction -import { getOrderedPartsAfterPlayhead, PartAndPieces, PartInstanceAndPieceInstances } from '../util.js' +import { getOrderedPartsAfterPlayhead, PartAndPieces } from '../util.js' import { LookaheadTimelineObject } from '../findObjects.js' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' import { createPartCurrentTimes } from '@sofie-automation/corelib/dist/playout/processAndPrune' const getOrderedPartsAfterPlayheadMock = getOrderedPartsAfterPlayhead as TgetOrderedPartsAfterPlayhead +const DEFAULT_PLAYOUT_STATE = { isInHold: false, isRehearsal: false } + describe('Lookahead', () => { let context!: MockJobContext let playlistId: RundownPlaylistId @@ -125,9 +127,8 @@ describe('Lookahead', () => { async function expectLookaheadForLayerMock( playlistId0: RundownPlaylistId, - partInstances: PartInstanceAndPieceInstances[], - previous: PartInstanceAndPieceInstances | undefined, - orderedPartsFollowingPlayhead: PartAndPieces[] + partInstancesInfo: PartInstanceAndPieceInstancesInfos, + orderedPartInfos: Array ) { const playlist = (await context.mockCollections.RundownPlaylists.findOne(playlistId0)) as DBRundownPlaylist expect(playlist).toBeTruthy() @@ -136,24 +137,24 @@ describe('Lookahead', () => { expect(findLookaheadForLayerMock).toHaveBeenNthCalledWith( 1, context, - playlist.currentPartInfo?.partInstanceId ?? null, - partInstances, - previous, - orderedPartsFollowingPlayhead, + partInstancesInfo, + orderedPartInfos, 'PRELOAD', 1, - LOOKAHEAD_DEFAULT_SEARCH_DISTANCE + LOOKAHEAD_DEFAULT_SEARCH_DISTANCE, + DEFAULT_PLAYOUT_STATE, + undefined ) expect(findLookaheadForLayerMock).toHaveBeenNthCalledWith( 2, context, - playlist.currentPartInfo?.partInstanceId ?? null, - partInstances, - previous, - orderedPartsFollowingPlayhead, + partInstancesInfo, + orderedPartInfos, 'WHEN_CLEAR', 1, - LOOKAHEAD_DEFAULT_SEARCH_DISTANCE + LOOKAHEAD_DEFAULT_SEARCH_DISTANCE, + DEFAULT_PLAYOUT_STATE, + undefined ) findLookaheadForLayerMock.mockClear() } @@ -171,7 +172,7 @@ describe('Lookahead', () => { expect(getOrderedPartsAfterPlayheadMock).toHaveBeenCalledTimes(1) expect(getOrderedPartsAfterPlayheadMock).toHaveBeenCalledWith(context, expect.anything(), 10) // default distance - await expectLookaheadForLayerMock(playlistId, [], undefined, fakeParts) + await expectLookaheadForLayerMock(playlistId, {}, fakeParts) }) function fakeResultObj(id: string, pieceId: string, layer: string): LookaheadTimelineObject { @@ -190,22 +191,42 @@ describe('Lookahead', () => { getOrderedPartsAfterPlayheadMock.mockReturnValueOnce(fakeParts.map((p) => p.part)) findLookaheadForLayerMock - .mockImplementationOnce((_context, _id, _parts, _prev, _parts2, layer) => ({ - timed: [fakeResultObj('obj0', 'piece0', layer), fakeResultObj('obj1', 'piece1', layer)], - future: [ - fakeResultObj('obj2', 'piece0', layer), - fakeResultObj('obj3', 'piece0', layer), - fakeResultObj('obj4', 'piece0', layer), - ], - })) - .mockImplementationOnce((_context, _id, _parts, _prev, _parts2, layer) => ({ - timed: [fakeResultObj('obj5', 'piece1', layer), fakeResultObj('obj6', 'piece0', layer)], - future: [ - fakeResultObj('obj7', 'piece1', layer), - fakeResultObj('obj8', 'piece1', layer), - fakeResultObj('obj9', 'piece0', layer), - ], - })) + .mockImplementationOnce( + ( + _context, + _partInstancesInfo, + _orderedPartInfos, + layer, + _lookaheadTargetFutureObjects, + _lookaheadMaxSearchDistance, + _nextTimeOffset + ) => ({ + timed: [fakeResultObj('obj0', 'piece0', layer), fakeResultObj('obj1', 'piece1', layer)], + future: [ + fakeResultObj('obj2', 'piece0', layer), + fakeResultObj('obj3', 'piece0', layer), + fakeResultObj('obj4', 'piece0', layer), + ], + }) + ) + .mockImplementationOnce( + ( + _context, + _partInstancesInfo, + _orderedPartInfos, + layer, + _lookaheadTargetFutureObjects, + _lookaheadMaxSearchDistance, + _nextTimeOffset + ) => ({ + timed: [fakeResultObj('obj5', 'piece1', layer), fakeResultObj('obj6', 'piece0', layer)], + future: [ + fakeResultObj('obj7', 'piece1', layer), + fakeResultObj('obj8', 'piece1', layer), + fakeResultObj('obj9', 'piece0', layer), + ], + }) + ) const res = await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => getLookeaheadObjects(context, playoutModel, partInstancesInfo) @@ -214,7 +235,7 @@ describe('Lookahead', () => { expect(getOrderedPartsAfterPlayheadMock).toHaveBeenCalledTimes(1) expect(getOrderedPartsAfterPlayheadMock).toHaveBeenCalledWith(context, expect.anything(), 10) // default distance - await expectLookaheadForLayerMock(playlistId, [], undefined, fakeParts) + await expectLookaheadForLayerMock(playlistId, {}, fakeParts) }) test('Different max distances', async () => { @@ -290,7 +311,7 @@ describe('Lookahead', () => { await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) - await expectLookaheadForLayerMock(playlistId, [], expectedPrevious, fakeParts) + await expectLookaheadForLayerMock(playlistId, { previous: expectedPrevious }, fakeParts) // Add a current partInstancesInfo.current = { @@ -310,7 +331,11 @@ describe('Lookahead', () => { await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) - await expectLookaheadForLayerMock(playlistId, [expectedCurrent], expectedPrevious, fakeParts) + await expectLookaheadForLayerMock( + playlistId, + { current: expectedCurrent, previous: expectedPrevious }, + fakeParts + ) // Add a next partInstancesInfo.next = { @@ -330,7 +355,11 @@ describe('Lookahead', () => { await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) - await expectLookaheadForLayerMock(playlistId, [expectedCurrent, expectedNext], expectedPrevious, fakeParts) + await expectLookaheadForLayerMock( + playlistId, + { current: expectedCurrent, next: expectedNext, previous: expectedPrevious }, + fakeParts + ) // current has autonext ;(partInstancesInfo.current.partInstance.part as DBPart).autoNext = true @@ -338,7 +367,76 @@ describe('Lookahead', () => { await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => getLookeaheadObjects(context, playoutModel, partInstancesInfo) ) - await expectLookaheadForLayerMock(playlistId, [expectedCurrent, expectedNext], expectedPrevious, fakeParts) + await expectLookaheadForLayerMock( + playlistId, + { current: expectedCurrent, next: expectedNext, previous: expectedPrevious }, + fakeParts + ) + }) + + test('Playlist state influences playoutState parameter', async () => { + const partInstancesInfo: SelectedPartInstancesTimelineInfo = {} + const fakeParts = partIds.map((p) => ({ part: { _id: p } as any, usesInTransition: true, pieces: [] })) + getOrderedPartsAfterPlayheadMock.mockReturnValue(fakeParts.map((p) => p.part)) + + // Test with rehearsal mode + await context.mockCollections.RundownPlaylists.update(playlistId, { $set: { rehearsal: true } }) + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) + ) + + expect(findLookaheadForLayerMock).toHaveBeenCalledWith( + context, + {}, + fakeParts, + 'PRELOAD', + 1, + LOOKAHEAD_DEFAULT_SEARCH_DISTANCE, + { isInHold: false, isRehearsal: true }, + undefined + ) + + findLookaheadForLayerMock.mockClear() + + // Test with hold state + await context.mockCollections.RundownPlaylists.update(playlistId, { + $set: { rehearsal: false, holdState: RundownHoldState.ACTIVE }, + }) + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) + ) + + expect(findLookaheadForLayerMock).toHaveBeenCalledWith( + context, + {}, + fakeParts, + 'PRELOAD', + 1, + LOOKAHEAD_DEFAULT_SEARCH_DISTANCE, + { isInHold: true, isRehearsal: false }, + undefined + ) + + findLookaheadForLayerMock.mockClear() + + // Test with both rehearsal and hold + await context.mockCollections.RundownPlaylists.update(playlistId, { + $set: { rehearsal: true, holdState: RundownHoldState.ACTIVE }, + }) + await runJobWithPlayoutModel(context, { playlistId }, null, async (playoutModel) => + getLookeaheadObjects(context, playoutModel, partInstancesInfo) + ) + + expect(findLookaheadForLayerMock).toHaveBeenCalledWith( + context, + {}, + fakeParts, + 'PRELOAD', + 1, + LOOKAHEAD_DEFAULT_SEARCH_DISTANCE, + { isInHold: true, isRehearsal: true }, + undefined + ) }) // eslint-disable-next-line jest/no-commented-out-tests diff --git a/packages/job-worker/src/playout/lookahead/__tests__/lookaheadOffset/constants.ts b/packages/job-worker/src/playout/lookahead/__tests__/lookaheadOffset/constants.ts new file mode 100644 index 00000000000..c57f48221cc --- /dev/null +++ b/packages/job-worker/src/playout/lookahead/__tests__/lookaheadOffset/constants.ts @@ -0,0 +1,349 @@ +import { IBlueprintPieceType, TSR, LookaheadMode } from '@sofie-automation/blueprints-integration' +import { Piece, PieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { PlayoutModel } from '../../../model/PlayoutModel.js' +import { JobContext } from '../../../../jobs/index.js' + +export function makePiece({ + partId, + layer, + start = 0, + duration, + nameSuffix = '', + objsBeforeOffset = 0, + objsAfterOffset = 0, + objsWhile = false, +}: { + partId: string + layer: string + start?: number + duration?: number + nameSuffix?: string + objsBeforeOffset?: number + objsAfterOffset?: number + objsWhile?: boolean +}): Piece { + return literal>({ + _id: protectString(`piece_${partId}_${nameSuffix}_${layer}`), + startRundownId: protectString('r1'), + startPartId: protectString(partId), + enable: { start, duration }, + outputLayerId: layer, + pieceType: IBlueprintPieceType.Normal, + timelineObjectsString: generateFakeObectsString( + `piece_${partId}_${nameSuffix}_${layer}`, + layer, + objsBeforeOffset, + objsAfterOffset, + objsWhile + ), + }) as Piece +} +export function generateFakeObectsString( + pieceId: string, + layer: string, + beforeStart: number, + afterStart: number, + enableWhile: boolean = false +): PieceTimelineObjectsBlob { + return protectString( + JSON.stringify([ + // At piece start + { + id: `${pieceId}_objPieceStart_${layer}`, + layer, + enable: !enableWhile ? { start: 0 } : { while: 1 }, + content: { + deviceType: TSR.DeviceType.CASPARCG, + type: TSR.TimelineContentTypeCasparCg.MEDIA, + file: 'AMB', + }, + }, + //beforeOffsetObj except if it's piece starts later than the offset. + { + id: `${pieceId}_obj_beforeOffset_${layer}`, + layer, + enable: !enableWhile ? { start: beforeStart } : { while: beforeStart }, + content: { + deviceType: TSR.DeviceType.CASPARCG, + type: TSR.TimelineContentTypeCasparCg.MEDIA, + file: 'AMB', + }, + }, + //afterOffsetObj except if it's piece starts later than the offset. + { + id: `${pieceId}_obj_afterOffset_${layer}`, + layer, + enable: !enableWhile ? { start: afterStart } : { while: afterStart }, + content: { + deviceType: TSR.DeviceType.CASPARCG, + type: TSR.TimelineContentTypeCasparCg.MEDIA, + file: 'AMB', + }, + }, + ]) + ) +} + +export const partDuration = 3000 +export const lookaheadOffsetTestConstants = { + multiLayerPart: { + partTimes: { nowInPart: 0 }, + partInstance: { + _id: protectString('pLookahead_ml_instance'), + part: { + _id: protectString('pLookahead_ml'), + _rank: 0, + }, + playlistActivationId: 'pA1', + }, + pieces: [ + // piece1 — At Part Start - lookaheadOffset should equal nextTimeOffset + // We generate three objects, one at the piece's start, one 700 ms after the piece's start, one 1700 ms after the piece's start. + // We need to check if all offsets are calculated correctly. (1000, 300 and no offset) + makePiece({ + partId: 'pLookahead_ml', + layer: 'layer1', + duration: partDuration, + nameSuffix: 'partStart', + objsBeforeOffset: 700, + objsAfterOffset: 1700, + }), + + // piece2 — Before Offset — lookaheadOffset should equal nextTimeOffset - the piece's start - the timeline object's start. + // We generate three objects, one at the piece's start, one 200 ms after the piece's start, one 1200 ms after the piece's start. + // We need to check if all offsets are calculated correctly. (500, 300 and no offset) + makePiece({ + partId: 'pLookahead_ml', + layer: 'layer2', + start: 500, + duration: partDuration - 500, + nameSuffix: 'partBeforeOffset', + objsBeforeOffset: 200, + objsAfterOffset: 1200, + }), + + // piece3 — After Offset — no lookahead offset + // We generate three objects, one at the piece's start, one 200 ms after the piece's start, one 400 ms after the piece's start. + // We need to check if all offsets are calculated correctly. + // for reference no offset should be calculated for all of it's objects. + makePiece({ + partId: 'pLookahead_ml', + layer: 'layer3', + start: 1500, + duration: partDuration - 1500, + nameSuffix: 'partAfterOffset', + objsBeforeOffset: 200, + objsAfterOffset: 400, + }), + ], + calculatedTimings: undefined, + regenerateTimelineAt: undefined, + }, + multiLayerPartWhile: { + partTimes: { nowInPart: 0 }, + partInstance: { + _id: protectString('pLookahead_ml_while_instance'), + part: { + _id: protectString('pLookahead_ml_while'), + _rank: 0, + }, + playlistActivationId: 'pA1', + }, + pieces: [ + // piece1 — At Part Start - lookaheadOffset should equal nextTimeOffset + // We generate three objects, one at the piece's start, one 700 ms after the piece's start, one 1700 ms after the piece's start. + // We need to check if all offsets are calculated correctly. (1000, 300 and no offset) + makePiece({ + partId: 'pLookahead_ml_while', + layer: 'layer1', + duration: partDuration, + nameSuffix: 'partStart', + objsBeforeOffset: 700, + objsAfterOffset: 1700, + objsWhile: true, + }), + + // piece2 — Before Offset — lookaheadOffset should equal nextTimeOffset - the piece's start - the timeline object's start. + // We generate three objects, one at the piece's start, one 200 ms after the piece's start, one 1200 ms after the piece's start. + // We need to check if all offsets are calculated correctly. (500, 300 and no offset) + makePiece({ + partId: 'pLookahead_ml_while', + layer: 'layer2', + start: 500, + duration: partDuration - 500, + nameSuffix: 'partBeforeOffset', + objsBeforeOffset: 200, + objsAfterOffset: 1200, + objsWhile: true, + }), + + // piece3 — After Offset — no lookahead offset + // We generate three objects, one at the piece's start, one 200 ms after the piece's start, one 400 ms after the piece's start. + // We need to check if all offsets are calculated correctly. + // for reference no offset should be calculated for all of it's objects. + makePiece({ + partId: 'pLookahead_ml_while', + layer: 'layer3', + start: 1500, + duration: partDuration - 1500, + nameSuffix: 'partAfterOffset', + objsBeforeOffset: 200, + objsAfterOffset: 400, + objsWhile: true, + }), + ], + calculatedTimings: undefined, + regenerateTimelineAt: undefined, + }, + singleLayerPart: { + partTimes: { nowInPart: 0 }, + partInstance: { + _id: protectString('pLookahead_sl_instance'), + part: { + _id: protectString('pLookahead_sl'), + _rank: 0, + }, + playlistActivationId: 'pA1', + }, + pieces: [ + // piece1 — At Part Start - should be ignored + // We generate three objects, one at the piece's start, one 700 ms after the piece's start, one 1700 ms after the piece's start. + // If the piece is not ignored (which shouldn't happen, it would mean that the logic is wrong) + // for reference the calculated offset values should be 1000, 300 and no offset + makePiece({ + partId: 'pLookahead_sl', + layer: 'layer1', + duration: partDuration, + nameSuffix: 'partStart', + objsBeforeOffset: 700, + objsAfterOffset: 1700, + }), + + // piece2 — Before Offset — lookaheadOffset should equal nextTimeOffset - the piece's start - the timeline object's start. + /// We generate three objects, one at the piece's start, one 200 ms after the piece's start, one 1200 ms after the piece's start. + // We need to check if all offsets are calculated correctly. (500, 300 and no offset) + makePiece({ + partId: 'pLookahead_sl', + layer: 'layer1', + start: 500, + duration: partDuration - 500, + nameSuffix: 'partBeforeOffset', + objsBeforeOffset: 200, + objsAfterOffset: 1200, + }), + + // piece3 — After Offset — should be ignored + // We generate three objects, one at the piece's start, one 200 ms after the piece's start, one 400 ms after the piece's start. + // If the piece is not ignored (which shouldn't happen, it would mean that the logic is wrong) + // for reference no offset should be calculated for all of it's objects. + makePiece({ + partId: 'pLookahead_sl', + layer: 'layer1', + start: 1500, + duration: partDuration - 1500, + nameSuffix: 'partAfterOffset', + objsBeforeOffset: 200, + objsAfterOffset: 400, + }), + ], + calculatedTimings: undefined, + regenerateTimelineAt: undefined, + }, + singleLayerPartWhile: { + partTimes: { nowInPart: 0 }, + partInstance: { + _id: protectString('pLookahead_sl_while_instance'), + part: { + _id: protectString('pLookahead_sl_while'), + _rank: 0, + }, + playlistActivationId: 'pA1', + }, + pieces: [ + // piece1 — At Part Start - should be ignored + // We generate three objects, one at the piece's start, one 700 ms after the piece's start, one 1700 ms after the piece's start. + // If the piece is not ignored (which shouldn't happen, it would mean that the logic is wrong) + // for reference the calculated offset values should be 1000, 300 and no offset + makePiece({ + partId: 'pLookahead_sl_while', + layer: 'layer1', + duration: partDuration, + nameSuffix: 'partStart', + objsBeforeOffset: 700, + objsAfterOffset: 1700, + objsWhile: true, + }), + + // piece2 — Before Offset — lookaheadOffset should equal nextTimeOffset - the piece's start - the timeline object's start. + // We generate three objects, one at the piece's start, one 200 ms after the piece's start, one 1200 ms after the piece's start. + // We need to check if all offsets are calculated correctly. (500, 300 and no offset) + makePiece({ + partId: 'pLookahead_sl_while', + layer: 'layer1', + start: 500, + duration: partDuration - 500, + nameSuffix: 'partBeforeOffset', + objsBeforeOffset: 200, + objsAfterOffset: 1200, + objsWhile: true, + }), + + // piece3 — After Offset — should be ignored + // We generate three objects, one at the piece's start, one 200 ms after the piece's start, one 400 ms after the piece's start. + // If the piece is not ignored (which shouldn't happen, it would mean that the logic is wrong) + // for reference no offset should be calculated for all of it's objects. + makePiece({ + partId: 'pLookahead_sl_while', + layer: 'layer1', + start: 1500, + duration: partDuration - 1500, + nameSuffix: 'partAfterOffset', + objsBeforeOffset: 200, + objsAfterOffset: 400, + objsWhile: true, + }), + ], + calculatedTimings: undefined, + regenerateTimelineAt: undefined, + }, + nextTimeOffset: 1000, +} +export const baseContext = { + startSpan: jest.fn(() => ({ end: jest.fn() })), + studio: { + mappings: { + layer1: { + device: 'casparcg', + layer: 10, + lookahead: LookaheadMode.PRELOAD, + lookaheadDepth: 2, + }, + layer2: { + device: 'casparcg', + layer: 10, + lookahead: LookaheadMode.PRELOAD, + lookaheadDepth: 2, + }, + layer3: { + device: 'casparcg', + layer: 10, + lookahead: LookaheadMode.PRELOAD, + lookaheadDepth: 2, + }, + }, + }, + directCollections: { + Pieces: { + findFetch: jest.fn(), + }, + }, +} as unknown as JobContext + +export const basePlayoutModel = { + getRundownIds: () => [protectString('r1')], + playlist: { + nextTimeOffset: 0, + }, +} as unknown as PlayoutModel diff --git a/packages/job-worker/src/playout/lookahead/__tests__/lookaheadOffset/lookaheadOffset.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/lookaheadOffset/lookaheadOffset.test.ts new file mode 100644 index 00000000000..7c969d1d619 --- /dev/null +++ b/packages/job-worker/src/playout/lookahead/__tests__/lookaheadOffset/lookaheadOffset.test.ts @@ -0,0 +1,329 @@ +jest.mock('../../../../playout/lookahead/index.js', () => { + const actual = jest.requireActual('../../../../playout/lookahead/index.js') + return { + ...actual, + findLargestLookaheadDistance: jest.fn(() => 0), + getLookeaheadObjects: actual.getLookeaheadObjects, + } +}) +jest.mock('../../../../playout/lookahead/util.js') +import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { TSR } from '@sofie-automation/blueprints-integration' +import { JobContext } from '../../../../jobs/index.js' +import { findLargestLookaheadDistance, getLookeaheadObjects } from '../../index.js' +import { getOrderedPartsAfterPlayhead } from '../../util.js' +import { PlayoutModel } from '../../../model/PlayoutModel.js' +import { SelectedPartInstancesTimelineInfo } from '../../../timeline/generate.js' +import { wrapPieceToInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { baseContext, basePlayoutModel, makePiece, lookaheadOffsetTestConstants } from './constants.js' + +const findLargestLookaheadDistanceMock = jest.mocked(findLargestLookaheadDistance).mockImplementation(() => 0) +const getOrderedPartsAfterPlayheadMock = jest.mocked(getOrderedPartsAfterPlayhead).mockImplementation(() => []) + +describe('lookahead offset integration', () => { + let context: JobContext + let playoutModel: PlayoutModel + + beforeEach(() => { + jest.resetAllMocks() + + context = baseContext + playoutModel = basePlayoutModel + }) + + test('returns empty array when no lookahead mappings are defined', async () => { + getOrderedPartsAfterPlayheadMock.mockReturnValue([ + { + _id: protectString('p1'), + classesForNext: [], + } as any, + ]) + const findFetchMock = jest.fn().mockResolvedValue([makePiece({ partId: 'p1', layer: 'layer1' })]) + context = { + ...context, + studio: { + ...context.studio, + mappings: {}, + }, + directCollections: { + ...context.directCollections, + Pieces: { + ...context.directCollections.Pieces, + findFetch: findFetchMock, + }, + }, + } as JobContext + + const res = await getLookeaheadObjects(context, playoutModel, {} as SelectedPartInstancesTimelineInfo) + + expect(res).toEqual([]) + }) + test('respects lookaheadMaxSearchDistance', async () => { + getOrderedPartsAfterPlayheadMock.mockReturnValue([ + { _id: protectString('p1'), classesForNext: [] } as any, + { _id: protectString('p2'), classesForNext: [] } as any, + { _id: protectString('p3'), classesForNext: [] } as any, + { _id: protectString('p4'), classesForNext: [] } as any, + ]) + + const findFetchMock = jest + .fn() + .mockResolvedValue([ + makePiece({ partId: 'p1', layer: 'layer1' }), + makePiece({ partId: 'p2', layer: 'layer1' }), + makePiece({ partId: 'p3', layer: 'layer1' }), + makePiece({ partId: 'p4', layer: 'layer1' }), + ]) + + context = { + ...context, + studio: { + ...context.studio, + mappings: { + ...context.studio.mappings, + layer1: { + ...context.studio.mappings['layer1'], + lookaheadMaxSearchDistance: 3, + }, + }, + }, + directCollections: { + ...context.directCollections, + Pieces: { + ...context.directCollections.Pieces, + findFetch: findFetchMock, + }, + }, + } as JobContext + + const res = await getLookeaheadObjects(context, playoutModel, { + current: undefined, + next: undefined, + previous: undefined, + } as SelectedPartInstancesTimelineInfo) + + expect(res).toHaveLength(2) + const obj0 = res[0] + const obj1 = res[1] + + expect(obj0.layer).toBe('layer1_lookahead') + expect(obj0.objectType).toBe('rundown') + expect(obj0.pieceInstanceId).toContain('p1') + expect(obj0.partInstanceId).toContain('p1') + expect(obj0.content).toMatchObject({ + deviceType: TSR.DeviceType.CASPARCG, + type: TSR.TimelineContentTypeCasparCg.MEDIA, + file: 'AMB', + }) + expect(obj1.layer).toBe('layer1_lookahead') + expect(obj1.objectType).toBe('rundown') + expect(obj1.pieceInstanceId).toContain('p2') + expect(obj1.partInstanceId).toContain('p2') + expect(obj1.content).toMatchObject({ + deviceType: TSR.DeviceType.CASPARCG, + type: TSR.TimelineContentTypeCasparCg.MEDIA, + file: 'AMB', + }) + }) + test('applies nextTimeOffset to lookahead objects in future part', async () => { + playoutModel = { + ...playoutModel, + playlist: { + ...playoutModel.playlist, + nextTimeOffset: 5000, + }, + } as PlayoutModel + findLargestLookaheadDistanceMock.mockReturnValue(1) + getOrderedPartsAfterPlayheadMock.mockReturnValue([ + { _id: protectString('p1'), classesForNext: [] } as any, + { _id: protectString('p2'), classesForNext: [] } as any, + ]) + + context.directCollections.Pieces.findFetch = jest + .fn() + .mockResolvedValue([ + makePiece({ partId: 'p1', layer: 'layer1' }), + makePiece({ partId: 'p2', layer: 'layer1', start: 2000 }), + ]) + + const res = await getLookeaheadObjects(context, playoutModel, {} as SelectedPartInstancesTimelineInfo) + + expect(res).toHaveLength(2) + expect(res[0].lookaheadOffset).toBe(5000) + expect(res[1].lookaheadOffset).toBe(3000) + }) + test('applies nextTimeOffset to lookahead objects in nextPart with no offset on next part', async () => { + playoutModel = { + ...playoutModel, + playlist: { + ...playoutModel.playlist, + nextTimeOffset: 5000, + }, + } as PlayoutModel + getOrderedPartsAfterPlayheadMock.mockReturnValue([{ _id: protectString('p1'), classesForNext: [] } as any]) + + context.directCollections.Pieces.findFetch = jest + .fn() + .mockResolvedValue([ + makePiece({ partId: 'pNext', layer: 'layer1', start: 0 }), + makePiece({ partId: 'p1', layer: 'layer2', start: 0 }), + ]) + + const res = await getLookeaheadObjects(context, playoutModel, { + next: { + partTimes: { nowInPart: 0 }, + partInstance: { + _id: protectString('pNextInstance'), + part: { + _id: protectString('pNext'), + _rank: 0, + }, + playlistActivationId: 'pA1', + }, + pieceInstances: [ + wrapPieceToInstance( + makePiece({ partId: 'pNext', layer: 'layer1', start: 0 }) as any, + 'pA1' as any, + 'pNextInstance' as any + ), + ], + calculatedTimings: undefined, + regenerateTimelineAt: undefined, + }, + } as any) + + expect(res).toHaveLength(2) + expect(res[0].lookaheadOffset).toBe(5000) + expect(res[1].lookaheadOffset).toBe(undefined) + }) + test('Multi layer part produces lookahead objects for all layers with the correct offsets', async () => { + playoutModel = { + ...playoutModel, + playlist: { + ...playoutModel.playlist, + nextTimeOffset: 1000, + }, + } as PlayoutModel + getOrderedPartsAfterPlayheadMock.mockReturnValue([ + { ...lookaheadOffsetTestConstants.multiLayerPart, classesForNext: [] } as any, + ]) + + context.directCollections.Pieces.findFetch = jest + .fn() + .mockResolvedValue(lookaheadOffsetTestConstants.multiLayerPart.pieces) + + const res = await getLookeaheadObjects(context, playoutModel, { + next: { + ...lookaheadOffsetTestConstants.multiLayerPart, + pieceInstances: lookaheadOffsetTestConstants.multiLayerPart.pieces.map((piece) => + wrapPieceToInstance( + piece as any, + 'pA1' as any, + lookaheadOffsetTestConstants.multiLayerPart.partInstance._id + ) + ), + }, + } as any) + + expect(res).toHaveLength(3) + expect(res.map((o) => o.layer).sort()).toEqual([`layer1_lookahead`, 'layer2_lookahead', 'layer3_lookahead']) + expect(res.map((o) => o.lookaheadOffset).sort()).toEqual([1000, 500]) + }) + test('Multi layer part produces lookahead objects with while enable values for all layers with the correct offsets', async () => { + playoutModel = { + ...playoutModel, + playlist: { + ...playoutModel.playlist, + nextTimeOffset: 1000, + }, + } as PlayoutModel + getOrderedPartsAfterPlayheadMock.mockReturnValue([ + { ...lookaheadOffsetTestConstants.multiLayerPartWhile, classesForNext: [] } as any, + ]) + + context.directCollections.Pieces.findFetch = jest + .fn() + .mockResolvedValue(lookaheadOffsetTestConstants.multiLayerPartWhile.pieces) + + const res = await getLookeaheadObjects(context, playoutModel, { + next: { + ...lookaheadOffsetTestConstants.multiLayerPartWhile, + pieceInstances: lookaheadOffsetTestConstants.multiLayerPartWhile.pieces.map((piece) => + wrapPieceToInstance( + piece as any, + 'pA1' as any, + lookaheadOffsetTestConstants.multiLayerPartWhile.partInstance._id + ) + ), + }, + } as any) + + expect(res).toHaveLength(3) + expect(res.map((o) => o.layer).sort()).toEqual([`layer1_lookahead`, 'layer2_lookahead', 'layer3_lookahead']) + expect(res.map((o) => o.lookaheadOffset).sort()).toEqual([1000, 500]) + }) + test('Single layer part produces lookahead objects with the correct offsets', async () => { + playoutModel = { + ...playoutModel, + playlist: { + ...playoutModel.playlist, + nextTimeOffset: 1000, + }, + } as PlayoutModel + getOrderedPartsAfterPlayheadMock.mockReturnValue([ + { ...lookaheadOffsetTestConstants.singleLayerPart, classesForNext: [] } as any, + ]) + + context.directCollections.Pieces.findFetch = jest + .fn() + .mockResolvedValue(lookaheadOffsetTestConstants.singleLayerPart.pieces) + + const res = await getLookeaheadObjects(context, playoutModel, { + next: { + ...lookaheadOffsetTestConstants.singleLayerPart, + pieceInstances: lookaheadOffsetTestConstants.singleLayerPart.pieces.map((piece) => + wrapPieceToInstance( + piece as any, + 'pA1' as any, + lookaheadOffsetTestConstants.singleLayerPart.partInstance._id + ) + ), + }, + } as any) + expect(res).toHaveLength(2) + expect(res.map((o) => o.layer)).toEqual(['layer1_lookahead', 'layer1_lookahead']) + expect(res.map((o) => o.lookaheadOffset)).toEqual([500, undefined]) + }) + test('Single layer part produces lookahead objects with while enable values with the correct offsets', async () => { + playoutModel = { + ...playoutModel, + playlist: { + ...playoutModel.playlist, + nextTimeOffset: 1000, + }, + } as PlayoutModel + getOrderedPartsAfterPlayheadMock.mockReturnValue([ + { ...lookaheadOffsetTestConstants.singleLayerPartWhile, classesForNext: [] } as any, + ]) + + context.directCollections.Pieces.findFetch = jest + .fn() + .mockResolvedValue(lookaheadOffsetTestConstants.singleLayerPartWhile.pieces) + + const res = await getLookeaheadObjects(context, playoutModel, { + next: { + ...lookaheadOffsetTestConstants.singleLayerPartWhile, + pieceInstances: lookaheadOffsetTestConstants.singleLayerPartWhile.pieces.map((piece) => + wrapPieceToInstance( + piece as any, + 'pA1' as any, + lookaheadOffsetTestConstants.singleLayerPartWhile.partInstance._id + ) + ), + }, + } as any) + expect(res).toHaveLength(2) + expect(res.map((o) => o.layer)).toEqual(['layer1_lookahead', 'layer1_lookahead']) + expect(res.map((o) => o.lookaheadOffset)).toEqual([500, undefined]) + }) +}) diff --git a/packages/job-worker/src/playout/lookahead/__tests__/utils.ts b/packages/job-worker/src/playout/lookahead/__tests__/utils.ts new file mode 100644 index 00000000000..8d930f71dd4 --- /dev/null +++ b/packages/job-worker/src/playout/lookahead/__tests__/utils.ts @@ -0,0 +1,80 @@ +import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' +import { ReadonlyDeep } from 'type-fest' +import { PartInstanceAndPieceInstances, PartAndPieces } from '../util.js' +import { findForLayerTestConstants } from './findForLayer/constants.js' +import { context, TfindLookaheadObjectsForPart } from './findForLayer/helpers/mockSetup.js' +import { TimelinePlayoutState } from '../../timeline/lib.js' + +export function expectInstancesToMatch( + findLookaheadObjectsForPartMock: TfindLookaheadObjectsForPart, + index: number, + layer: string, + partInstanceInfo: PartInstanceAndPieceInstances, + previousPart: PartInstanceAndPieceInstances | undefined, + playoutState: TimelinePlayoutState +): void { + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + index, + context, + findForLayerTestConstants.current.part._id, + layer, + previousPart?.part.part, + { + part: partInstanceInfo.part.part, + usesInTransition: false, + pieces: partInstanceInfo.allPieces, + }, + partInstanceInfo?.part._id, + playoutState + ) +} + +export function createFakePiece(id: string, pieceProps?: Partial): PieceInstance { + return { + _id: id, + piece: { + ...(pieceProps ?? {}), + enable: { + start: 0, + ...(pieceProps ? pieceProps.enable : {}), + }, + }, + } as any +} + +export function expectPartToMatch( + findLookaheadObjectsForPartMock: TfindLookaheadObjectsForPart, + index: number, + layer: string, + partInfo: PartAndPieces, + previousPart: ReadonlyDeep | undefined, + currentPartInstanceId: PartInstanceId | null = null, + playoutState: TimelinePlayoutState, + nextTimeOffset?: number +): void { + if (nextTimeOffset) + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + index, + context, + currentPartInstanceId, + layer, + previousPart, + partInfo, + null, + playoutState, + nextTimeOffset + ) + else + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + index, + context, + currentPartInstanceId, + layer, + previousPart, + partInfo, + null, + playoutState + ) +} diff --git a/packages/job-worker/src/playout/lookahead/findForLayer.ts b/packages/job-worker/src/playout/lookahead/findForLayer.ts index e09297c0776..9d69cb67616 100644 --- a/packages/job-worker/src/playout/lookahead/findForLayer.ts +++ b/packages/job-worker/src/playout/lookahead/findForLayer.ts @@ -5,23 +5,31 @@ import { JobContext } from '../../jobs/index.js' import { sortPieceInstancesByStart } from '../pieces.js' import { findLookaheadObjectsForPart, LookaheadTimelineObject } from './findObjects.js' import { PartAndPieces, PartInstanceAndPieceInstances } from './util.js' +import { TimelinePlayoutState } from '../timeline/lib.js' export interface LookaheadResult { timed: Array future: Array } +export interface PartInstanceAndPieceInstancesInfos { + previous?: PartInstanceAndPieceInstances + current?: PartInstanceAndPieceInstances + next?: PartInstanceAndPieceInstances +} + export function findLookaheadForLayer( context: JobContext, - currentPartInstanceId: PartInstanceId | null, - partInstancesInfo: PartInstanceAndPieceInstances[], - previousPartInstanceInfo: PartInstanceAndPieceInstances | undefined, + partInstancesInfo: PartInstanceAndPieceInstancesInfos, orderedPartInfos: Array, layer: string, lookaheadTargetFutureObjects: number, - lookaheadMaxSearchDistance: number + lookaheadMaxSearchDistance: number, + playoutState: TimelinePlayoutState, + nextTimeOffset?: number | null ): LookaheadResult { const span = context.startSpan(`findLookaheadForlayer.${layer}`) + const currentPartId = partInstancesInfo.current?.part._id ?? null const res: LookaheadResult = { timed: [], future: [], @@ -29,54 +37,87 @@ export function findLookaheadForLayer( // Track the previous info for checking how the timeline will be built let previousPart: ReadonlyDeep | undefined - if (previousPartInstanceInfo) { - previousPart = previousPartInstanceInfo.part.part + if (partInstancesInfo.previous?.part.part) { + previousPart = partInstancesInfo.previous.part.part } // Generate timed/future objects for the partInstances - for (const partInstanceInfo of partInstancesInfo) { - if (!partInstanceInfo.onTimeline && lookaheadMaxSearchDistance <= 0) break + if (partInstancesInfo.current) { + const { objs: currentObjs, partInfo: currentPartInfo } = generatePartInstanceLookaheads( + context, + partInstancesInfo.current, + partInstancesInfo.current.part._id, + layer, + previousPart, + playoutState + ) - const partInfo: PartAndPieces = { - part: partInstanceInfo.part.part, - usesInTransition: partInstanceInfo.calculatedTimings.inTransitionStart !== null, - pieces: sortPieceInstancesByStart(partInstanceInfo.allPieces, partInstanceInfo.nowInPart), + if (partInstancesInfo.current.onTimeline) { + res.timed.push(...currentObjs) + } else { + res.future.push(...currentObjs) } + previousPart = currentPartInfo.part + } - const objs = findLookaheadObjectsForPart( + let lookaheadMaxSearchDistanceOffset = 0 + + // for Lookaheads in the next part we need to take the nextTimeOffset into account. + if (partInstancesInfo.next) { + const { objs: nextObjs, partInfo: nextPartInfo } = generatePartInstanceLookaheads( context, - currentPartInstanceId, + partInstancesInfo.next, + currentPartId, layer, previousPart, - partInfo, - partInstanceInfo.part._id + playoutState, + nextTimeOffset ) - if (partInstanceInfo.onTimeline) { - res.timed.push(...objs) - } else { - res.future.push(...objs) + if (partInstancesInfo.next?.onTimeline) { + res.timed.push(...nextObjs) + } else if (lookaheadMaxSearchDistance >= 1 && lookaheadTargetFutureObjects > 0) { + res.future.push(...nextObjs) } + previousPart = nextPartInfo.part - previousPart = partInfo.part + lookaheadMaxSearchDistanceOffset = 1 } if (lookaheadMaxSearchDistance > 1 && lookaheadTargetFutureObjects > 0) { - for (const partInfo of orderedPartInfos.slice(0, lookaheadMaxSearchDistance - 1)) { + for (const partInfo of orderedPartInfos.slice( + 0, + lookaheadMaxSearchDistance - lookaheadMaxSearchDistanceOffset + )) { // Stop if we have enough objects already if (res.future.length >= lookaheadTargetFutureObjects) { break } if (partInfo.pieces.length > 0 && isPartPlayable(partInfo.part)) { - const objs = findLookaheadObjectsForPart( - context, - currentPartInstanceId, - layer, - previousPart, - partInfo, - null - ) + const objs = + nextTimeOffset && !partInstancesInfo.next // apply the lookahead offset to the first future if an offset is set. + ? findLookaheadObjectsForPart( + context, + currentPartId, + layer, + previousPart, + partInfo, + null, + { + ...playoutState, + // This is beyond the next part, so will be back to not being in hold + isInHold: false, + includeWhenNotInHoldObjects: true, + }, + nextTimeOffset + ) + : findLookaheadObjectsForPart(context, currentPartId, layer, previousPart, partInfo, null, { + ...playoutState, + // This is beyond the next part, so will be back to not being in hold + isInHold: false, + includeWhenNotInHoldObjects: true, + }) res.future.push(...objs) previousPart = partInfo.part } @@ -86,3 +127,46 @@ export function findLookaheadForLayer( if (span) span.end() return res } +function generatePartInstanceLookaheads( + context: JobContext, + partInstanceInfo: PartInstanceAndPieceInstances, + currentPartInstanceId: PartInstanceId | null, + layer: string, + previousPart: ReadonlyDeep | undefined, + playoutState: TimelinePlayoutState, + nextTimeOffset?: number | null +): { objs: LookaheadTimelineObject[]; partInfo: PartAndPieces } { + const partInfo: PartAndPieces = { + part: partInstanceInfo.part.part, + usesInTransition: partInstanceInfo.calculatedTimings?.inTransitionStart ? true : false, + pieces: sortPieceInstancesByStart(partInstanceInfo.allPieces, partInstanceInfo.nowInPart), + } + if (nextTimeOffset) { + return { + objs: findLookaheadObjectsForPart( + context, + currentPartInstanceId, + layer, + previousPart, + partInfo, + partInstanceInfo.part._id, + playoutState, + nextTimeOffset + ), + partInfo, + } + } else { + return { + objs: findLookaheadObjectsForPart( + context, + currentPartInstanceId, + layer, + previousPart, + partInfo, + partInstanceInfo.part._id, + playoutState + ), + partInfo, + } + } +} diff --git a/packages/job-worker/src/playout/lookahead/findObjects.ts b/packages/job-worker/src/playout/lookahead/findObjects.ts index d96035a74f5..a0704ebf9ab 100644 --- a/packages/job-worker/src/playout/lookahead/findObjects.ts +++ b/packages/job-worker/src/playout/lookahead/findObjects.ts @@ -13,8 +13,10 @@ import { JobContext } from '../../jobs/index.js' import { PartAndPieces, PieceInstanceWithObjectMap } from './util.js' import { deserializePieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { ReadonlyDeep, SetRequired } from 'type-fest' +import { shouldIncludeObjectOnTimeline, TimelinePlayoutState } from '../timeline/lib.js' +import { computeLookaheadObject } from './lookaheadOffset.js' -function getBestPieceInstanceId(piece: ReadonlyDeep): string { +export function getBestPieceInstanceId(piece: ReadonlyDeep): string { if (!piece.isTemporary || piece.partInstanceId) { return unprotectString(piece._id) } @@ -51,12 +53,17 @@ function tryActivateKeyframesForObject( } } -function getObjectMapForPiece(piece: PieceInstanceWithObjectMap): NonNullable { +function getObjectMapForPiece( + playoutState: TimelinePlayoutState, + piece: PieceInstanceWithObjectMap +): NonNullable { if (!piece.objectMap) { piece.objectMap = new Map() const objects = deserializePieceTimelineObjectsBlob(piece.piece.timelineObjectsString) for (const obj of objects) { + if (!shouldIncludeObjectOnTimeline(playoutState, obj)) continue + // Note: This is assuming that there is only one use of a layer in each piece. if (typeof obj.layer === 'string' && !piece.objectMap.has(obj.layer)) { piece.objectMap.set(obj.layer, obj) @@ -90,9 +97,11 @@ export function findLookaheadObjectsForPart( _context: JobContext, currentPartInstanceId: PartInstanceId | null, layer: string, - previousPart: ReadonlyDeep | undefined, + partBefore: ReadonlyDeep | undefined, partInfo: PartAndPieces, - partInstanceId: PartInstanceId | null + partInstanceId: PartInstanceId | null, + playoutState: TimelinePlayoutState, + nextTimeOffset?: number ): Array { // Sanity check, if no part to search, then abort if (!partInfo || partInfo.pieces.length === 0) { @@ -103,18 +112,12 @@ export function findLookaheadObjectsForPart( for (const rawPiece of partInfo.pieces) { if (shouldIgnorePiece(partInfo, rawPiece)) continue - const obj = getObjectMapForPiece(rawPiece).get(layer) - if (obj) { - allObjs.push( - literal({ - metaData: undefined, - ...obj, - objectType: TimelineObjType.RUNDOWN, - pieceInstanceId: getBestPieceInstanceId(rawPiece), - infinitePieceInstanceId: rawPiece.infinite?.infiniteInstanceId, - partInstanceId: partInstanceId ?? protectString(unprotectString(partInfo.part._id)), - }) - ) + const obj = getObjectMapForPiece(playoutState, rawPiece).get(layer) + + // we only consider lookahead objects for lookahead and calculate the lookaheadOffset for each object. + const computedLookaheadObj = computeLookaheadObject(obj, rawPiece, partInfo, partInstanceId, nextTimeOffset) + if (computedLookaheadObj) { + allObjs.push(computedLookaheadObj) } } @@ -124,8 +127,8 @@ export function findLookaheadObjectsForPart( } let classesFromPreviousPart: readonly string[] = [] - if (previousPart && currentPartInstanceId && partInstanceId) { - classesFromPreviousPart = previousPart.classesForNext || [] + if (partBefore && currentPartInstanceId && partInstanceId) { + classesFromPreviousPart = partBefore.classesForNext || [] } const transitionPiece = partInfo.usesInTransition @@ -144,29 +147,28 @@ export function findLookaheadObjectsForPart( }, ] } else { - const hasTransitionObj = transitionPiece && getObjectMapForPiece(transitionPiece).get(layer) + const hasTransitionObj = transitionPiece && getObjectMapForPiece(playoutState, transitionPiece).get(layer) const res: Array = [] - partInfo.pieces.forEach((piece) => { - if (shouldIgnorePiece(partInfo, piece)) return + allObjs.map((obj) => { + const piece = partInfo.pieces.find((piece) => unprotectString(piece._id) === obj.pieceInstanceId) + if (!piece) return // If there is a transition and this piece is abs0, it is assumed to be the primary piece and so does not need lookahead if ( hasTransitionObj && + obj.pieceInstanceId && piece.piece.pieceType === IBlueprintPieceType.Normal && piece.piece.enable.start === 0 ) { return } - // Note: This is assuming that there is only one use of a layer in each piece. - const obj = getObjectMapForPiece(piece).get(layer) if (obj) { const patchedContent = tryActivateKeyframesForObject(obj, !!transitionPiece, classesFromPreviousPart) res.push( literal({ - metaData: undefined, ...obj, objectType: TimelineObjType.RUNDOWN, pieceInstanceId: getBestPieceInstanceId(piece), @@ -177,7 +179,6 @@ export function findLookaheadObjectsForPart( ) } }) - return res } } diff --git a/packages/job-worker/src/playout/lookahead/index.ts b/packages/job-worker/src/playout/lookahead/index.ts index 64ac5a23372..cfcb8b42801 100644 --- a/packages/job-worker/src/playout/lookahead/index.ts +++ b/packages/job-worker/src/playout/lookahead/index.ts @@ -1,5 +1,5 @@ import { getOrderedPartsAfterPlayhead, PartAndPieces, PartInstanceAndPieceInstances } from './util.js' -import { findLookaheadForLayer, LookaheadResult } from './findForLayer.js' +import { findLookaheadForLayer, LookaheadResult, PartInstanceAndPieceInstancesInfos } from './findForLayer.js' import { PlayoutModel } from '../model/PlayoutModel.js' import { sortPieceInstancesByStart } from '../pieces.js' import { MappingExt } from '@sofie-automation/corelib/dist/dataModel/Studio' @@ -21,9 +21,11 @@ import _ from 'underscore' import { LOOKAHEAD_DEFAULT_SEARCH_DISTANCE } from '@sofie-automation/shared-lib/dist/core/constants' import { prefixSingleObjectId } from '../lib.js' import { LookaheadTimelineObject } from './findObjects.js' -import { hasPieceInstanceDefinitelyEnded } from '../timeline/lib.js' +import { hasPieceInstanceDefinitelyEnded, TimelinePlayoutState } from '../timeline/lib.js' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { ReadonlyDeep } from 'type-fest' +import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { filterPieceInstancesForNextPartWithOffset } from './lookaheadOffset.js' const LOOKAHEAD_OBJ_PRIORITY = 0.1 @@ -35,7 +37,7 @@ function parseSearchDistance(rawVal: number | undefined): number { } } -function findLargestLookaheadDistance(mappings: Array<[string, MappingExt]>): number { +export function findLargestLookaheadDistance(mappings: Array<[string, ReadonlyDeep]>): number { const values = mappings.map(([_id, m]) => parseSearchDistance(m.lookaheadMaxSearchDistance)) return _.max(values) } @@ -79,7 +81,7 @@ export async function getLookeaheadObjects( ): Promise> { const span = context.startSpan('getLookeaheadObjects') const allMappings = context.studio.mappings - const mappingsToConsider = Object.entries(allMappings).filter( + const mappingsToConsider = Object.entries>(allMappings).filter( ([_id, map]) => map.lookahead !== LookaheadMode.NONE && map.lookahead !== undefined ) if (mappingsToConsider.length === 0) { @@ -104,8 +106,24 @@ export async function getLookeaheadObjects( }, }) - const partInstancesInfo: PartInstanceAndPieceInstances[] = _.compact([ - partInstancesInfo0.current + // Track the previous info for checking how the timeline will be built + let previousPartInfo: PartInstanceAndPieceInstances | undefined + if (partInstancesInfo0.previous) { + previousPartInfo = removeInfiniteContinuations( + { + part: partInstancesInfo0.previous.partInstance, + onTimeline: true, + nowInPart: partInstancesInfo0.previous.partTimes.nowInPart, + allPieces: getPrunedEndedPieceInstances(partInstancesInfo0.previous), + calculatedTimings: partInstancesInfo0.previous.calculatedTimings, + }, + false + ) + } + + const partInstancesInfo: PartInstanceAndPieceInstancesInfos = { + previous: previousPartInfo, + current: partInstancesInfo0.current ? removeInfiniteContinuations( { part: partInstancesInfo0.current.partInstance, @@ -117,33 +135,21 @@ export async function getLookeaheadObjects( true ) : undefined, - partInstancesInfo0.next + next: partInstancesInfo0.next ? removeInfiniteContinuations( { part: partInstancesInfo0.next.partInstance, onTimeline: !!partInstancesInfo0.current?.partInstance?.part?.autoNext, //TODO -QL nowInPart: partInstancesInfo0.next.partTimes.nowInPart, - allPieces: partInstancesInfo0.next.pieceInstances, + allPieces: filterPieceInstancesForNextPartWithOffset( + partInstancesInfo0.next.pieceInstances, + playoutModel.playlist.nextTimeOffset + ), calculatedTimings: partInstancesInfo0.next.calculatedTimings, }, false ) : undefined, - ]) - - // Track the previous info for checking how the timeline will be built - let previousPartInfo: PartInstanceAndPieceInstances | undefined - if (partInstancesInfo0.previous) { - previousPartInfo = removeInfiniteContinuations( - { - part: partInstancesInfo0.previous.partInstance, - onTimeline: true, - nowInPart: partInstancesInfo0.previous.partTimes.nowInPart, - allPieces: getPrunedEndedPieceInstances(partInstancesInfo0.previous), - calculatedTimings: partInstancesInfo0.previous.calculatedTimings, - }, - false - ) } // TODO: Do we need to use processAndPrunePieceInstanceTimings on these pieces? In theory yes, but that gets messy and expensive. @@ -177,6 +183,11 @@ export async function getLookeaheadObjects( } }) + const playoutState: TimelinePlayoutState = { + isRehearsal: !!playoutModel.playlist.rehearsal, + isInHold: playoutModel.playlist.holdState === RundownHoldState.ACTIVE, + } + const span2 = context.startSpan('getLookeaheadObjects.iterate') const timelineObjs: Array = [] const futurePartCount = orderedPartInfos.length + (partInstancesInfo0.next ? 1 : 0) @@ -187,16 +198,15 @@ export async function getLookeaheadObjects( parseSearchDistance(mapping.lookaheadMaxSearchDistance), futurePartCount ) - const lookaheadObjs = findLookaheadForLayer( context, - playoutModel.playlist.currentPartInfo?.partInstanceId ?? null, partInstancesInfo, - previousPartInfo, orderedPartInfos, layerId, lookaheadTargetObjects, - lookaheadMaxSearchDistance + lookaheadMaxSearchDistance, + playoutState, + playoutModel.playlist.nextTimeOffset ) timelineObjs.push(...processResult(lookaheadObjs, mapping.lookahead)) diff --git a/packages/job-worker/src/playout/lookahead/lookaheadOffset.ts b/packages/job-worker/src/playout/lookahead/lookaheadOffset.ts new file mode 100644 index 00000000000..ac67ca9b313 --- /dev/null +++ b/packages/job-worker/src/playout/lookahead/lookaheadOffset.ts @@ -0,0 +1,208 @@ +import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { TimelineObjType } from '@sofie-automation/corelib/dist/dataModel/Timeline' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' +import { getBestPieceInstanceId, LookaheadTimelineObject } from './findObjects.js' +import { PartAndPieces, PieceInstanceWithObjectMap } from './util.js' +import { TimelineEnable } from 'superfly-timeline' +import { TimelineObjectCoreExt } from '@sofie-automation/blueprints-integration' +import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' + +/** + * Computes a full {@link LookaheadTimelineObject} for a given piece/object pair, + * including the correct `lookaheadOffset` based on explicit numeric `start` or `while` expressions. + * + * This function: + * - Ignores objects whose `enable` is an array (unsupported for lookahead) + * - Extracts a usable numeric start reference from both the object and its parent piece + * - Supports lookahead semantics where `enable.while >= 1` acts like an implicit start value + * - Returns `undefined` when lookahead cannot be computed safely + * + * @param obj - The timeline object associated with the piece and layer. If `undefined`, + * no lookahead object is created. + * @param rawPiece - The piece instance containing the object map and its own enable + * expression, which determines the base start time for lookahead. + * @param partInfo - Metadata about the part the piece belongs to, required for + * associating the lookahead object with the correct `partInstanceId`. + * @param partInstanceId - The currently active or next part instance ID. If `null`, + * the function falls back to the part ID from `partInfo`. + * @param nextTimeOffset - An optional offset of the in point of the next part + * used to calculate the lookahead offset. If omitted, no + * lookahead offset is generated. + * + * @returns A fully constructed {@link LookaheadTimelineObject} ready to be pushed + * into the lookahead timeline, or `undefined` when no valid lookahead + * calculation is possible. + */ +export function computeLookaheadObject( + obj: TimelineObjectCoreExt | undefined, + rawPiece: PieceInstanceWithObjectMap, + partInfo: PartAndPieces, + partInstanceId: PartInstanceId | null, + nextTimeOffset?: number +): LookaheadTimelineObject | undefined { + if (!obj) return undefined + + const enable = obj.enable + + if (Array.isArray(enable)) return undefined + + const objStart = getStartValueFromEnable(enable) + const pieceStart = getStartValueFromEnable(rawPiece.piece.enable) + + // We make sure to only consider objects for lookahead that have an explicit numeric start/while value. (while = 1 and 0 is considered boolean) + if (pieceStart === undefined) return undefined + + let lookaheadOffset: number | undefined + // Only calculate lookaheadOffset if needed + if (nextTimeOffset) { + lookaheadOffset = computeLookaheadOffset(nextTimeOffset, pieceStart, objStart) + } + + return literal({ + metaData: undefined, + ...obj, + objectType: TimelineObjType.RUNDOWN, + pieceInstanceId: getBestPieceInstanceId(rawPiece), + infinitePieceInstanceId: rawPiece.infinite?.infiniteInstanceId, + partInstanceId: partInstanceId ?? protectString(unprotectString(partInfo.part._id)), + ...(lookaheadOffset !== undefined ? { lookaheadOffset } : {}), + }) +} + +/** + * Computes a lookahead offset for an object based on the piece's start time + * and the object's start time, relative to the next part's start time. + * + * @param nextTimeOffset - The upcoming part's start time (or similar time anchor). + * If undefined, no lookahead offset is produced. + * @param pieceStart - The start time of the piece this object belongs to. + * @param objStart - The explicit start time of the object (relative to the piece's start time). + * + * @returns A positive lookahead offset, or `undefined` if lookahead cannot be + * determined or would be non-positive. + */ +function computeLookaheadOffset( + nextTimeOffset: number | undefined, + pieceStart: number, + objStart?: number +): number | undefined { + if (nextTimeOffset === undefined || objStart === undefined) return undefined + + const offset = nextTimeOffset - pieceStart - objStart + return offset > 0 ? offset : undefined +} + +/** + * Extracts a numeric start reference from a {@link TimelineEnable} object + * + * The function handles two mutually exclusive cases: + * + * **1. `start` mode (`{ start: number }`)** + * - If `enable.start` is a numeric value, it is returned as `start`. + * - If `enable.start` is the string `"now"`, it is treated as `0`. + * + * **2. `while` mode (`{ while: number }`)** + * - If `enable.while` is numeric and greater than 1, it's value is returned as is. + * - If `enable.while` is numeric and equal to 1 it's treated as `0`. + * + * If no usable numeric `start` or `while` expression exists, the function returns `undefined`. + * + * @param enable - The timeline object's enable expression to extract start info from. + * @returns the relative start value of the object or undefined if there is no explicit value. + */ +function getStartValueFromEnable(enable: TimelineEnable): number | undefined { + // Case: start is a number + if (typeof enable.start === 'number') { + return enable.start + } + + // Case: start is "now" + if (enable.start === 'now') { + return 0 + } + + // Case: while is numeric + if (typeof enable.while === 'number') { + // while > 1 we treat it as a start value + if (enable.while > 1) { + return enable.while + } + // while === 1 we treat it as a `0` start value + else if (enable.while === 1) { + return 0 + } + } + + // No usable numeric expressions + return undefined +} + +/** + * Filters piece instances for the "next" part when a `nextTimeOffset` is defined. + * + * This function ensures that we only take into account each layer's relevant piece before + * or at the `nextTimeOffset`, while also preserving all pieces starting after the offset. + * + * This is needed to ignore pieces that start before the offset, but then are replaced by another piece at the offset. + * Without ignoring them the lookahead logic would treat the next part as if it was queued from it's start. + * + * **Filtering rules:** + * - If `nextTimeOffset` is not set (0, null, undefined), the original list is returned. + * - Pieces are grouped based on their `outputLayerId`. + * - For each layer: + * - We only keep pieces with the **latest start time** where `start/while <= nextTimeOffset` + * - All pieces *after* `nextTimeOffset` are kept for future lookaheads. + * + * The result is a flattened list of the selected pieces across all layers. + * + * @param {PieceInstanceWithTimings[]} pieces + * The list of piece instances to filter. + * + * @param {number | null | undefined} nextTimeOffset + * The time offset (in part time) that defines relevance. + * Pieces are compared based on their enable.start value. + * + * @returns {PieceInstanceWithTimings[]} + * A filtered list of pieces containing only the relevant pieces per layer. + */ +export function filterPieceInstancesForNextPartWithOffset( + pieces: PieceInstanceWithTimings[], + nextTimeOffset: number | null | undefined +): PieceInstanceWithTimings[] { + if (!nextTimeOffset) return pieces + // Group pieces by layer + const layers = new Map() + for (const p of pieces) { + const layer = p.piece.outputLayerId || '__noLayer__' + if (!layers.has(layer)) layers.set(layer, []) + layers.get(layer)?.push(p) + } + + const result: PieceInstanceWithTimings[] = [] + + for (const layerPieces of layers.values()) { + const beforeOrAt: PieceInstanceWithTimings[] = [] + const after: PieceInstanceWithTimings[] = [] + + for (const piece of layerPieces) { + const pieceStart = getStartValueFromEnable(piece.piece.enable) + + if (pieceStart !== undefined) { + if (pieceStart <= nextTimeOffset) beforeOrAt.push(piece) + else after.push(piece) + } + } + + // Pick the relevant piece before/at nextTimeOffset + if (beforeOrAt.length > 0) { + const best = beforeOrAt.reduce((a, b) => (a.piece.enable.start > b.piece.enable.start ? a : b)) + result.push(best) + } + + // Keep all pieces after nextTimeOffset for future lookaheads. + result.push(...after) + } + + return result +} diff --git a/packages/job-worker/src/playout/lookahead/util.ts b/packages/job-worker/src/playout/lookahead/util.ts index 72bb201dc62..99d692d2592 100644 --- a/packages/job-worker/src/playout/lookahead/util.ts +++ b/packages/job-worker/src/playout/lookahead/util.ts @@ -34,11 +34,16 @@ export function isPieceInstance(piece: Piece | PieceInstance | PieceInstancePiec /** * Excludes the previous, current and next part + * @param context Job context + * @param playoutModel The playout model + * @param partCount Maximum number of parts to return + * @param ignoreQuickLoop If true, ignores quickLoop markers and returns parts in linear order. Defaults to false for backwards compatibility. */ export function getOrderedPartsAfterPlayhead( context: JobContext, playoutModel: PlayoutModel, - partCount: number + partCount: number, + ignoreQuickLoop: boolean = false ): ReadonlyDeep[] { if (partCount <= 0) { return [] @@ -66,7 +71,7 @@ export function getOrderedPartsAfterPlayhead( null, orderedSegments, orderedParts, - { ignoreUnplayable: true, ignoreQuickLoop: false } + { ignoreUnplayable: true, ignoreQuickLoop } ) if (!nextNextPart) { // We don't know where to begin searching, so we can't do anything diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index 0dff06ff919..f84a098b281 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -17,6 +17,7 @@ import { DBRundownPlaylist, QuickLoopMarker, RundownHoldState, + RundownTTimer, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { ReadonlyDeep } from 'type-fest' import { StudioPlayoutModelBase, StudioPlayoutModelBaseReadonly } from '../../studio/model/StudioPlayoutModel.js' @@ -328,10 +329,16 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa ): void /** - * Store the blueprint persistent state + * Store the blueprint private persistent state * @param persistentState Blueprint owned state */ - setBlueprintPersistentState(persistentState: unknown | undefined): void + setBlueprintPrivatePersistentState(persistentState: unknown | undefined): void + + /** + * Store the blueprint public persistent state + * @param persistentState Blueprint owned state + */ + setBlueprintPublicPersistentState(persistentState: unknown | undefined): void /** * Set a PartInstance as the nexted PartInstance @@ -374,6 +381,12 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa */ setQuickLoopMarker(type: 'start' | 'end', marker: QuickLoopMarker | null): void + /** + * Update a T-timer + * @param timer Timer properties + */ + updateTTimer(timer: RundownTTimer): void + calculatePartTimings( fromPartInstance: PlayoutPartInstanceModel | null, toPartInstance: PlayoutPartInstanceModel, @@ -387,6 +400,12 @@ export interface PlayoutModel extends PlayoutModelReadonly, StudioPlayoutModelBa */ getNowInPlayout(): Time + /** + * Mark the playlist as needing a timeline update. + * The timeline will be generated and published when model is ready to be saved. + */ + markTimelineNeedsUpdate(): void + /** Lifecycle */ /** diff --git a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts index c42de1e38a0..c811d2fdcd9 100644 --- a/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts +++ b/packages/job-worker/src/playout/model/implementation/LoadPlayoutModel.ts @@ -123,7 +123,7 @@ async function loadInitData( const [peripheralDevices, reloadedPlaylist, rundowns] = await Promise.all([ context.directCollections.PeripheralDevices.findFetch({ 'studioAndConfigId.studioId': tmpPlaylist.studioId }), reloadPlaylist - ? await context.directCollections.RundownPlaylists.findOne(tmpPlaylist._id) + ? context.directCollections.RundownPlaylists.findOne(tmpPlaylist._id) : clone(tmpPlaylist), existingRundowns ?? context.directCollections.Rundowns.findFetch({ playlistId: tmpPlaylist._id }), ]) diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 52253f1a2f5..3fe579c388a 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -16,6 +16,7 @@ import { DBRundownPlaylist, QuickLoopMarker, RundownHoldState, + RundownTTimer, SelectedPartInstance, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { ReadonlyDeep } from 'type-fest' @@ -71,6 +72,16 @@ import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout import { NotificationsModelHelper } from '../../../notifications/NotificationsModelHelper.js' import { getExpectedLatency } from '@sofie-automation/corelib/dist/studio/playout' import { ExpectedPackage } from '@sofie-automation/blueprints-integration' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { + getTimelineRundown, + flattenAndProcessTimelineObjects, + preserveOrReplaceNowTimesInObjects, + logAnyRemainingNowTimes, + getStudioTimeline, +} from '../../timeline/generate.js' +import { deNowifyMultiGatewayTimeline } from '../../timeline/multi-gateway.js' +import { validateTTimerIndex } from '../../tTimers.js' export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { public readonly playlistId: RundownPlaylistId @@ -315,6 +326,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou #playlistHasChanged = false #timelineHasChanged = false + #timelineNeedsRegeneration = false #pendingPartInstanceTimingEvents = new Set() @@ -661,7 +673,8 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou delete this.playlistImpl.startedPlayback delete this.playlistImpl.rundownsStartedPlayback delete this.playlistImpl.segmentsStartedPlayback - delete this.playlistImpl.previousPersistentState + delete this.playlistImpl.publicPlayoutPersistentState + delete this.playlistImpl.privatePlayoutPersistentState delete this.playlistImpl.trackedAbSessions delete this.playlistImpl.queuedSegmentId @@ -694,8 +707,17 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou } this.#deferredBeforeSaveFunctions.length = 0 // clear the array + // Generate timeline if needed + if (this.#timelineNeedsRegeneration) { + await this.#regenerateTimeline() + this.#timelineNeedsRegeneration = false + } + // Prioritise the timeline for publication reasons if (this.#timelineHasChanged && this.timelineImpl) { + // Do a fast-track for the timeline to be published faster: + this.context.hackPublishTimelineToFastTrack(this.timelineImpl) + await this.context.directCollections.Timelines.replace(this.timelineImpl) if (!process.env.JEST_WORKER_ID) { // Wait a little bit before saving the rest. @@ -764,8 +786,13 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#playlistHasChanged = true } - setBlueprintPersistentState(persistentState: unknown | undefined): void { - this.playlistImpl.previousPersistentState = persistentState + setBlueprintPrivatePersistentState(persistentState: unknown | undefined): void { + this.playlistImpl.privatePlayoutPersistentState = persistentState + + this.#playlistHasChanged = true + } + setBlueprintPublicPersistentState(persistentState: unknown | undefined): void { + this.playlistImpl.publicPlayoutPersistentState = persistentState this.#playlistHasChanged = true } @@ -780,7 +807,8 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou const storedPartInstance = this.allPartInstances.get(partInstance.partInstance._id) if (!storedPartInstance) throw new Error(`PartInstance being set as next was not constructed correctly`) // Make sure we were given the exact same object - if (storedPartInstance !== partInstance) throw new Error(`PartInstance being set as next is not current`) + if (storedPartInstance.partInstance._id !== partInstance.partInstance._id) + throw new Error(`PartInstance being set as next is not current`) } if (partInstance) { @@ -877,6 +905,13 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou this.#playlistHasChanged = true } + updateTTimer(timer: RundownTTimer): void { + validateTTimerIndex(timer.index) + + this.playlistImpl.tTimers[timer.index - 1] = timer + this.#playlistHasChanged = true + } + #lastMonotonicNowInPlayout = getCurrentTime() getNowInPlayout(): number { const nowOffsetLatency = this.getNowOffsetLatency() ?? 0 @@ -886,6 +921,10 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou return result } + markTimelineNeedsUpdate(): void { + this.#timelineNeedsRegeneration = true + } + /** Notifications */ async getAllNotifications( @@ -922,6 +961,40 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou /** BaseModel */ + async #regenerateTimeline(): Promise { + const span = this.context.startSpan('PlayoutModelImpl.regenerateTimeline') + logger.debug('Regenerating timeline...') + + try { + const { + versions, + objs: timelineObjs, + timingContext: timingInfo, + regenerateTimelineToken, + } = this.playlist.activationId + ? await getTimelineRundown(this.context, this) + : await getStudioTimeline(this.context, this) + + flattenAndProcessTimelineObjects(this.context, timelineObjs) + + preserveOrReplaceNowTimesInObjects(this, timelineObjs) + + if (this.isMultiGatewayMode) { + deNowifyMultiGatewayTimeline(this, timelineObjs, timingInfo) + + logAnyRemainingNowTimes(this.context, timelineObjs) + } + + const timelineHash = this.setTimeline(timelineObjs, versions, regenerateTimelineToken).timelineHash + logger.verbose(`Timeline regeneration done, hash: "${timelineHash}"`) + } catch (err) { + logger.error(`Error regenerating timeline: ${stringifyError(err)}`) + throw err + } finally { + if (span) span.end() + } + } + /** * Assert that no changes should have been made to the model, will throw an Error otherwise. This can be used in * place of `saveAllToDatabase()`, when the code controlling the model expects no changes to have been made and any diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts index 6c1f5a95885..b64511f17bf 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutPieceInstanceModelImpl.ts @@ -2,10 +2,12 @@ import { ExpectedPackageId, PieceInstanceInfiniteId, RundownId } from '@sofie-au import { ReadonlyDeep } from 'type-fest' import { PieceInstance, PieceInstancePiece } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { clone, getRandomId } from '@sofie-automation/corelib/dist/lib' -import { ExpectedPackage, Time } from '@sofie-automation/blueprints-integration' +import { ExpectedPackage, Time, PieceLifespan } from '@sofie-automation/blueprints-integration' import { PlayoutPieceInstanceModel } from '../PlayoutPieceInstanceModel.js' import _ from 'underscore' import { getExpectedPackageId } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' +import { setupPieceInstanceInfiniteProperties } from '../../pieces.js' +import { getCurrentTime } from '../../../lib/time.js' export class PlayoutPieceInstanceModelImpl implements PlayoutPieceInstanceModel { /** @@ -158,6 +160,14 @@ export class PlayoutPieceInstanceModelImpl implements PlayoutPieceInstanceModel }, true ) + if ( + props.lifespan !== undefined && + props.lifespan !== PieceLifespan.WithinPart && + !this.PieceInstanceImpl.infinite + ) { + setupPieceInstanceInfiniteProperties(this.PieceInstanceImpl) + this.PieceInstanceImpl.dynamicallyConvertedToInfinite = getCurrentTime() + } } } diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index 8739e289a33..2fb38067aed 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -33,6 +33,10 @@ import { import { NoteSeverity } from '@sofie-automation/blueprints-integration' import { convertNoteToNotification } from '../notifications/util.js' import { PersistentPlayoutStateStore } from '../blueprints/context/services/PersistantStateStore.js' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { PlayoutPartInstanceModelImpl } from './model/implementation/PlayoutPartInstanceModelImpl.js' +import { QuickLoopService } from './model/services/QuickLoopService.js' +import { recalculateTTimerProjections } from './tTimers.js' /** * Set or clear the nexted part, from a given PartInstance, or SelectNextPartResult @@ -96,6 +100,9 @@ export async function setNextPart( await cleanupOrphanedItems(context, playoutModel) + // Recalculate T-Timer projections based on the new next part + recalculateTTimerProjections(context, playoutModel) + if (span) span.end() } @@ -229,14 +236,15 @@ async function executeOnSetAsNextCallback( playoutModel.clearAllNotifications(NOTIFICATION_CATEGORY) try { - const blueprintPersistentState = new PersistentPlayoutStateStore(playoutModel.playlist.previousPersistentState) + const blueprintPersistentState = new PersistentPlayoutStateStore( + playoutModel.playlist.privatePlayoutPersistentState, + playoutModel.playlist.publicPlayoutPersistentState + ) await blueprint.blueprint.onSetAsNext(onSetAsNextContext, blueprintPersistentState) await applyOnSetAsNextSideEffects(context, playoutModel, onSetAsNextContext) - if (blueprintPersistentState.hasChanges) { - playoutModel.setBlueprintPersistentState(blueprintPersistentState.getAll()) - } + blueprintPersistentState.saveToModel(playoutModel) for (const note of onSetAsNextContext.notes) { // Update the notifications. Even though these are related to a partInstance, they will be cleared on the next take @@ -525,6 +533,10 @@ export async function queueNextSegment( } else { playoutModel.setQueuedSegment(null) } + + // Recalculate timer projections as the queued segment affects what comes after next + recalculateTTimerProjections(context, playoutModel) + span?.end() return { queuedSegmentId: queuedSegment?.segment?._id ?? null } } @@ -572,14 +584,14 @@ function findFirstPlayablePartOrThrow(segment: PlayoutSegmentModel): ReadonlyDee * Set the nexted part, from a given DBPart * @param context Context for the running job * @param playoutModel The playout model of the playlist - * @param nextPart The Part to set as next + * @param nextPartOrInstance The Part to set as next * @param setManually Whether this was manually chosen by the user * @param nextTimeOffset The offset into the Part to start playback */ export async function setNextPartFromPart( context: JobContext, playoutModel: PlayoutModel, - nextPart: ReadonlyDeep, + nextPartOrInstance: ReadonlyDeep | ReadonlyDeep, setManually: boolean, nextTimeOffset?: number ): Promise { @@ -589,9 +601,37 @@ export async function setNextPartFromPart( throw UserError.create(UserErrorMessage.DuringHold) } - const consumesQueuedSegmentId = doesPartConsumeQueuedSegmentId(playoutModel, nextPart) + let consumesQueuedSegmentId: boolean | undefined + + if (!('part' in nextPartOrInstance)) { + consumesQueuedSegmentId = doesPartConsumeQueuedSegmentId(playoutModel, nextPartOrInstance) - await setNextPart(context, playoutModel, { part: nextPart, consumesQueuedSegmentId }, setManually, nextTimeOffset) + await setNextPart( + context, + playoutModel, + { + part: nextPartOrInstance, + consumesQueuedSegmentId, + }, + setManually, + nextTimeOffset + ) + } else { + await setNextPart( + context, + playoutModel, + new PlayoutPartInstanceModelImpl( + nextPartOrInstance as DBPartInstance, + await context.directCollections.PieceInstances.findFetch({ + partInstanceId: nextPartOrInstance._id, + }), + false, + new QuickLoopService(context, playoutModel) + ), + setManually, + nextTimeOffset + ) + } } function doesPartConsumeQueuedSegmentId(playoutModel: PlayoutModel, nextPart: ReadonlyDeep) { diff --git a/packages/job-worker/src/playout/setNextJobs.ts b/packages/job-worker/src/playout/setNextJobs.ts index a9ffc2c1f88..f9ce6988de8 100644 --- a/packages/job-worker/src/playout/setNextJobs.ts +++ b/packages/job-worker/src/playout/setNextJobs.ts @@ -1,5 +1,5 @@ import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' +import { DBPart, isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' import { @@ -16,6 +16,7 @@ import { selectNewPartWithOffsets } from './moveNextPart.js' import { updateTimeline } from './timeline/generate.js' import { PlayoutSegmentModel } from './model/PlayoutSegmentModel.js' import { ReadonlyDeep } from 'type-fest' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' /** * Set the next Part to a specified id @@ -26,19 +27,55 @@ export async function handleSetNextPart(context: JobContext, data: SetNextPartPr data, async (playoutModel) => { const playlist = playoutModel.playlist - if (!playlist.activationId) throw UserError.create(UserErrorMessage.InactiveRundown, undefined, 412) if (playlist.holdState && playlist.holdState !== RundownHoldState.COMPLETE) { throw UserError.create(UserErrorMessage.DuringHold, undefined, 412) } }, async (playoutModel) => { - // Ensure the part is playable and found - const nextPart = playoutModel.findPart(data.nextPartId) - if (!nextPart) throw UserError.create(UserErrorMessage.PartNotFound, undefined, 404) - if (!isPartPlayable(nextPart)) throw UserError.create(UserErrorMessage.PartNotPlayable, undefined, 412) + const playlist = playoutModel.playlist + + let nextPartOrInstance: ReadonlyDeep | DBPartInstance | undefined + let nextPartId: PartId | undefined + + if (data.nextPartInstanceId) { + // Fetch the part instance + const nextPartInstance = await context.directCollections.PartInstances.findOne({ + _id: data.nextPartInstanceId, + }) + if (!nextPartInstance) throw UserError.create(UserErrorMessage.PartNotFound, undefined, 404) + + // Determine if we need the part itself or can use the instance (We can't reuse the currently playing instance) + if ( + !playlist.nextPartInfo?.partInstanceId || + !playlist.currentPartInfo?.partInstanceId || + playlist.currentPartInfo?.partInstanceId === data.nextPartInstanceId + ) { + nextPartId = nextPartInstance.part._id + } else { + nextPartOrInstance = nextPartInstance + } + } else if (data.nextPartId) { + nextPartId = data.nextPartId + } - await setNextPartFromPart(context, playoutModel, nextPart, data.setManually ?? false, data.nextTimeOffset) + // If we have a nextPartId, resolve the actual part + if (nextPartId) { + const nextPart = playoutModel.findPart(nextPartId) + if (!nextPart) throw UserError.create(UserErrorMessage.PartNotFound, undefined, 404) + if (!isPartPlayable(nextPart)) throw UserError.create(UserErrorMessage.PartNotPlayable, undefined, 412) + nextPartOrInstance = nextPart + } + + if (nextPartOrInstance) { + await setNextPartFromPart( + context, + playoutModel, + nextPartOrInstance, + data.setManually ?? false, + data.nextTimeOffset + ) + } await updateTimeline(context, playoutModel) } diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts new file mode 100644 index 00000000000..dc6d9524a05 --- /dev/null +++ b/packages/job-worker/src/playout/tTimers.ts @@ -0,0 +1,349 @@ +import type { + RundownTTimerIndex, + RundownTTimerMode, + RundownTTimer, + TimerState, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { getCurrentTime } from '../lib/index.js' +import type { ReadonlyDeep } from 'type-fest' +import * as chrono from 'chrono-node' +import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { JobContext } from '../jobs/index.js' +import { PlayoutModel } from './model/PlayoutModel.js' +import { getOrderedPartsAfterPlayhead } from './lookahead/util.js' + +export function validateTTimerIndex(index: number): asserts index is RundownTTimerIndex { + if (![1, 2, 3].includes(index)) throw new Error(`T-timer index out of range: ${index}`) +} + +/** + * Returns an updated T-timer in the paused state (if supported) + * @param timer Timer to update + * @returns If the timer supports pausing, the timer in paused state, otherwise null + */ +export function pauseTTimer(timer: ReadonlyDeep): ReadonlyDeep | null { + if (!timer.mode || !timer.state) return null + if (timer.mode.type === 'countdown' || timer.mode.type === 'freeRun') { + if (timer.state.paused) { + // Already paused + return timer + } + return { + ...timer, + state: { paused: true, duration: timer.state.zeroTime - getCurrentTime() }, + } + } else { + return null + } +} + +/** + * Returns an updated T-timer in the resumed state (if supported) + * @param timer Timer to update + * @returns If the timer supports pausing, the timer in resumed state, otherwise null + */ +export function resumeTTimer(timer: ReadonlyDeep): ReadonlyDeep | null { + if (!timer.mode || !timer.state) return null + if (timer.mode.type === 'countdown' || timer.mode.type === 'freeRun') { + if (!timer.state.paused) { + // Already running + return timer + } + + return { + ...timer, + state: { paused: false, zeroTime: timer.state.duration + getCurrentTime() }, + } + } else { + return null + } +} + +/** + * Returns an updated T-timer, after restarting (if supported) + * @param timer Timer to update + * @returns If the timer supports restarting, the restarted timer, otherwise null + */ +export function restartTTimer(timer: ReadonlyDeep): ReadonlyDeep | null { + if (!timer.mode || !timer.state) return null + if (timer.mode.type === 'countdown') { + return { + ...timer, + state: timer.state.paused + ? { paused: true, duration: timer.mode.duration } + : { paused: false, zeroTime: getCurrentTime() + timer.mode.duration }, + } + } else if (timer.mode.type === 'timeOfDay') { + const nextTime = calculateNextTimeOfDayTarget(timer.mode.targetRaw) + // If we can't calculate the next time, or it's the same, we can't restart + if (nextTime === null || (timer.state.paused ? false : nextTime === timer.state.zeroTime)) return null + + return { + ...timer, + state: { paused: false, zeroTime: nextTime }, + } + } else { + return null + } +} + +/** + * Create a new countdown T-timer mode and initial state + * @param duration Duration in milliseconds + * @param options Options for the countdown + * @returns The created T-timer mode and state + */ +export function createCountdownTTimer( + duration: number, + options: { + stopAtZero: boolean + startPaused: boolean + } +): { mode: ReadonlyDeep; state: ReadonlyDeep } { + if (duration <= 0) throw new Error('Duration must be greater than zero') + + return { + mode: { + type: 'countdown', + duration, + stopAtZero: !!options.stopAtZero, + }, + state: options.startPaused + ? { paused: true, duration: duration } + : { paused: false, zeroTime: getCurrentTime() + duration }, + } +} + +export function createTimeOfDayTTimer( + targetTime: string | number, + options: { + stopAtZero: boolean + } +): { mode: ReadonlyDeep; state: ReadonlyDeep } { + const nextTime = calculateNextTimeOfDayTarget(targetTime) + if (nextTime === null) throw new Error('Unable to parse target time for timeOfDay T-timer') + + return { + mode: { + type: 'timeOfDay', + targetRaw: targetTime, + stopAtZero: !!options.stopAtZero, + }, + state: { paused: false, zeroTime: nextTime }, + } +} + +/** + * Create a new free-running T-timer mode and initial state + * @param options Options for the free-run + * @returns The created T-timer mode and state + */ +export function createFreeRunTTimer(options: { startPaused: boolean }): { + mode: ReadonlyDeep + state: ReadonlyDeep +} { + const now = getCurrentTime() + return { + mode: { + type: 'freeRun', + }, + state: options.startPaused ? { paused: true, duration: 0 } : { paused: false, zeroTime: now }, + } +} + +/** + * Calculate the next target time for a timeOfDay T-timer + * @param targetTime The target time, as a string or timestamp number + * @returns The next target timestamp in milliseconds, or null if it could not be calculated + */ +export function calculateNextTimeOfDayTarget(targetTime: string | number): number | null { + if (typeof targetTime === 'number') { + // This should be a unix timestamp + return targetTime + } + + // Verify we have a string worth parsing + if (typeof targetTime !== 'string' || !targetTime) return null + + const parsed = chrono.parseDate(targetTime, undefined, { + // Always look ahead for the next occurrence + forwardDate: true, + }) + return parsed ? parsed.getTime() : null +} + +/** + * Recalculate T-Timer projections based on timing anchors using segment budget timing. + * + * Uses a single-pass algorithm with two accumulators: + * - totalAccumulator: Accumulated time across completed segments + * - segmentAccumulator: Accumulated time within current segment + * + * At each segment boundary: + * - If segment has a budget → use segment budget duration + * - Otherwise → use accumulated part durations + * + * Handles starting mid-segment with budget by calculating remaining budget time. + * + * @param context Job context + * @param playoutModel The playout model containing the playlist and parts + */ +export function recalculateTTimerProjections(context: JobContext, playoutModel: PlayoutModel): void { + const span = context.startSpan('recalculateTTimerProjections') + + const playlist = playoutModel.playlist + + const tTimers = playlist.tTimers + + // Find which timers have anchors that need calculation + const timerAnchors = new Map() + for (const timer of tTimers) { + if (timer.anchorPartId) { + const existingTimers = timerAnchors.get(timer.anchorPartId) ?? [] + existingTimers.push(timer.index) + timerAnchors.set(timer.anchorPartId, existingTimers) + } + } + + // If no timers have anchors, nothing to do + if (timerAnchors.size === 0) { + if (span) span.end() + return undefined + } + + const currentPartInstance = playoutModel.currentPartInstance?.partInstance + + // Get ordered parts after playhead (excludes previous, current, and next) + // Use ignoreQuickLoop=true to count parts linearly without loop-back behavior + const playablePartsSlice = getOrderedPartsAfterPlayhead(context, playoutModel, Infinity, true) + + if (playablePartsSlice.length === 0 && !currentPartInstance) { + // No parts to iterate through, clear projections + for (const timer of tTimers) { + if (timer.anchorPartId) { + playoutModel.updateTTimer({ ...timer, projectedState: undefined }) + } + } + if (span) span.end() + return + } + + const now = getCurrentTime() + + // Initialize accumulators + let totalAccumulator = 0 + let segmentAccumulator = 0 + let isPushing = false + let currentSegmentId: SegmentId | undefined = undefined + + // Handle current part/segment + if (currentPartInstance) { + currentSegmentId = currentPartInstance.segmentId + const currentSegment = playoutModel.findSegment(currentPartInstance.segmentId) + const currentSegmentBudget = currentSegment?.segment.segmentTiming?.budgetDuration + + if (currentSegmentBudget === undefined) { + // Normal part duration timing + const currentPartDuration = + currentPartInstance.part.expectedDurationWithTransition ?? currentPartInstance.part.expectedDuration + if (currentPartDuration) { + const currentPartStartedPlayback = currentPartInstance.timings?.plannedStartedPlayback + const startedPlayback = + currentPartStartedPlayback && currentPartStartedPlayback <= now ? currentPartStartedPlayback : now + const playOffset = currentPartInstance.timings?.playOffset || 0 + const elapsed = now - startedPlayback - playOffset + const remaining = currentPartDuration - elapsed + + isPushing = remaining < 0 + totalAccumulator = Math.max(0, remaining) + } + } else { + // Segment budget timing - we're already inside a budgeted segment + const segmentStartedPlayback = + playlist.segmentsStartedPlayback?.[currentPartInstance.segmentId as unknown as string] + if (segmentStartedPlayback) { + const segmentElapsed = now - segmentStartedPlayback + const remaining = currentSegmentBudget - segmentElapsed + isPushing = remaining < 0 + totalAccumulator = Math.max(0, remaining) + } else { + totalAccumulator = currentSegmentBudget + } + } + } + + // Save remaining current part time for pauseTime calculation + const currentPartRemainingTime = totalAccumulator + + // Add the next part to the beginning of playablePartsSlice + // getOrderedPartsAfterPlayhead excludes both current and next, so we need to prepend next + // This allows the loop to handle it normally, including detecting if it's an anchor + const nextPartInstance = playoutModel.nextPartInstance?.partInstance + if (nextPartInstance) { + playablePartsSlice.unshift(nextPartInstance.part) + } + + // Single pass through parts + for (const part of playablePartsSlice) { + // Detect segment boundary + if (part.segmentId !== currentSegmentId) { + // Flush previous segment + if (currentSegmentId !== undefined) { + const lastSegment = playoutModel.findSegment(currentSegmentId) + const segmentBudget = lastSegment?.segment.segmentTiming?.budgetDuration + + // Use budget if it exists, otherwise use accumulated part durations + if (segmentBudget !== undefined) { + totalAccumulator += segmentBudget + } else { + totalAccumulator += segmentAccumulator + } + } + + // Reset for new segment + segmentAccumulator = 0 + currentSegmentId = part.segmentId + } + + // Check if this part is an anchor + const timersForThisPart = timerAnchors.get(part._id) + if (timersForThisPart) { + const anchorTime = totalAccumulator + segmentAccumulator + + for (const timerIndex of timersForThisPart) { + const timer = tTimers[timerIndex - 1] + + const projectedState: TimerState = isPushing + ? literal({ + paused: true, + duration: anchorTime, + pauseTime: null, // Already paused/pushing + }) + : literal({ + paused: false, + zeroTime: now + anchorTime, + pauseTime: now + currentPartRemainingTime, // When current part ends and pushing begins + }) + + playoutModel.updateTTimer({ ...timer, projectedState }) + } + + timerAnchors.delete(part._id) + } + + // Accumulate this part's duration + const partDuration = part.expectedDurationWithTransition ?? part.expectedDuration ?? 0 + segmentAccumulator += partDuration + } + + // Clear projections for unresolved anchors + for (const [, timerIndices] of timerAnchors.entries()) { + for (const timerIndex of timerIndices) { + const timer = tTimers[timerIndex - 1] + playoutModel.updateTTimer({ ...timer, projectedState: undefined }) + } + } + + if (span) span.end() +} diff --git a/packages/job-worker/src/playout/tTimersJobs.ts b/packages/job-worker/src/playout/tTimersJobs.ts new file mode 100644 index 00000000000..a639ea1db04 --- /dev/null +++ b/packages/job-worker/src/playout/tTimersJobs.ts @@ -0,0 +1,44 @@ +import { JobContext } from '../jobs/index.js' +import { recalculateTTimerProjections } from './tTimers.js' +import { runWithPlayoutModel, runWithPlaylistLock } from './lock.js' + +/** + * Handle RecalculateTTimerProjections job + * This is called after setNext, takes, and ingest changes to update T-Timer projections + * Since this job doesn't take a playlistId parameter, it finds the active playlist in the studio + */ +export async function handleRecalculateTTimerProjections(context: JobContext): Promise { + // Find active playlists in this studio (projection to just get IDs) + const activePlaylistIds = await context.directCollections.RundownPlaylists.findFetch( + { + studioId: context.studioId, + activationId: { $exists: true }, + }, + { + projection: { + _id: 1, + }, + } + ) + + if (activePlaylistIds.length === 0) { + // No active playlist, nothing to do + return + } + + // Process each active playlist (typically there's only one) + for (const playlistRef of activePlaylistIds) { + await runWithPlaylistLock(context, playlistRef._id, async (lock) => { + // Fetch the full playlist object + const playlist = await context.directCollections.RundownPlaylists.findOne(playlistRef._id) + if (!playlist) { + // Playlist was removed between query and lock + return + } + + await runWithPlayoutModel(context, playlist, lock, null, async (playoutModel) => { + recalculateTTimerProjections(context, playoutModel) + }) + }) + } +} diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index c9a2dd32b97..18c1f24ccdf 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -40,10 +40,12 @@ import { PlayoutRundownModel } from './model/PlayoutRundownModel.js' import { convertNoteToNotification } from '../notifications/util.js' import { PersistentPlayoutStateStore } from '../blueprints/context/services/PersistantStateStore.js' +import { TakeNextPartResult } from '@sofie-automation/corelib/dist/worker/studio' + /** * Take the currently Next:ed Part (start playing it) */ -export async function handleTakeNextPart(context: JobContext, data: TakeNextPartProps): Promise { +export async function handleTakeNextPart(context: JobContext, data: TakeNextPartProps): Promise { const now = getCurrentTime() return runJobWithPlayoutModel( @@ -77,17 +79,29 @@ export async function handleTakeNextPart(context: JobContext, data: TakeNextPart } } if (lastTakeTime && now - lastTakeTime < context.studio.settings.minimumTakeSpan) { + const nextTakeTime = lastTakeTime + context.studio.settings.minimumTakeSpan logger.debug( `Time since last take is shorter than ${context.studio.settings.minimumTakeSpan} for ${ playlist.currentPartInfo?.partInstanceId }: ${now - lastTakeTime}` ) - throw UserError.create(UserErrorMessage.TakeRateLimit, { - duration: context.studio.settings.minimumTakeSpan, - }) + throw UserError.create( + UserErrorMessage.TakeRateLimit, + { + duration: context.studio.settings.minimumTakeSpan, + nextAllowedTakeTime: nextTakeTime, + }, + 429 + ) } - return performTakeToNextedPart(context, playoutModel, now, undefined) + const nextTakeTime = now + context.studio.settings.minimumTakeSpan + + await performTakeToNextedPart(context, playoutModel, now, undefined) + + return { + nextTakeTime, + } } ) } @@ -159,7 +173,14 @@ export async function performTakeToNextedPart( logger.debug( `Take is blocked until ${currentPartInstance.partInstance.blockTakeUntil}. Which is in: ${remainingTime}` ) - throw UserError.create(UserErrorMessage.TakeBlockedDuration, { duration: remainingTime }) + throw UserError.create( + UserErrorMessage.TakeBlockedDuration, + { + duration: remainingTime, + nextAllowedTakeTime: currentPartInstance.partInstance.blockTakeUntil, + }, + 425 + ) } // If there was a transition from the previous Part, then ensure that has finished before another take is permitted @@ -171,11 +192,17 @@ export async function performTakeToNextedPart( start && now < start + currentPartInstance.partInstance.part.inTransition.blockTakeDuration ) { - throw UserError.create(UserErrorMessage.TakeDuringTransition) + throw UserError.create( + UserErrorMessage.TakeDuringTransition, + { + nextAllowedTakeTime: start + currentPartInstance.partInstance.part.inTransition.blockTakeDuration, + }, + 425 + ) } if (currentPartInstance.isTooCloseToAutonext(true)) { - throw UserError.create(UserErrorMessage.TakeCloseToAutonext) + throw UserError.create(UserErrorMessage.TakeCloseToAutonext, undefined, 425) } } @@ -343,7 +370,8 @@ async function executeOnTakeCallback( ) try { const blueprintPersistentState = new PersistentPlayoutStateStore( - playoutModel.playlist.previousPersistentState + playoutModel.playlist.privatePlayoutPersistentState, + playoutModel.playlist.publicPlayoutPersistentState ) await blueprint.blueprint.onTake(onSetAsNextContext, blueprintPersistentState) @@ -353,9 +381,7 @@ async function executeOnTakeCallback( partToQueueAfterTake = onSetAsNextContext.partToQueueAfterTake } - if (blueprintPersistentState.hasChanges) { - playoutModel.setBlueprintPersistentState(blueprintPersistentState.getAll()) - } + blueprintPersistentState.saveToModel(playoutModel) for (const note of onSetAsNextContext.notes) { // Update the notifications. Even though these are related to a partInstance, they will be cleared on the next take @@ -549,7 +575,8 @@ export function updatePartInstanceOnTake( takeRundown ) const blueprintPersistentState = new PersistentPlayoutStateStore( - playoutModel.playlist.previousPersistentState + playoutModel.playlist.privatePlayoutPersistentState, + playoutModel.playlist.publicPlayoutPersistentState ) previousPartEndState = blueprint.blueprint.getEndStateForPart( context2, @@ -558,9 +585,8 @@ export function updatePartInstanceOnTake( resolvedPieces.map(convertResolvedPieceInstanceToBlueprints), time ) - if (blueprintPersistentState.hasChanges) { - playoutModel.setBlueprintPersistentState(blueprintPersistentState.getAll()) - } + blueprintPersistentState.saveToModel(playoutModel) + if (span) span.end() logger.info(`Calculated end state in ${getCurrentTime() - time}ms`) } catch (err) { diff --git a/packages/job-worker/src/playout/timeline/__tests__/__snapshots__/rundown.test.ts.snap b/packages/job-worker/src/playout/timeline/__tests__/__snapshots__/rundown.test.ts.snap index 60c9724e57a..e1db089fb99 100644 --- a/packages/job-worker/src/playout/timeline/__tests__/__snapshots__/rundown.test.ts.snap +++ b/packages/job-worker/src/playout/timeline/__tests__/__snapshots__/rundown.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`buildTimelineObjsForRundown current and previous parts 1`] = ` { @@ -117,6 +117,7 @@ exports[`buildTimelineObjsForRundown current and previous parts 1`] = ` "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": undefined, "previousPartOverlap": 0, }, @@ -205,6 +206,7 @@ exports[`buildTimelineObjsForRundown current part with startedPlayback 1`] = ` "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": undefined, }, } @@ -288,7 +290,7 @@ exports[`buildTimelineObjsForRundown infinite pieces infinite continuing from pr }, ], "enable": { - "start": 123, + "start": "#part_group_part0.start", }, "id": "part_group_piece6b_infinite", "isPieceTimeline": true, @@ -362,6 +364,7 @@ exports[`buildTimelineObjsForRundown infinite pieces infinite continuing from pr "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": undefined, "previousPartOverlap": 0, }, @@ -409,7 +412,7 @@ exports[`buildTimelineObjsForRundown infinite pieces infinite continuing into ne }, ], "enable": { - "start": 123, + "start": "#part_group_part0.start", }, "id": "part_group_piece6_infinite", "isPieceTimeline": true, @@ -528,6 +531,7 @@ exports[`buildTimelineObjsForRundown infinite pieces infinite continuing into ne "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": { "children": 3, "content": { @@ -692,6 +696,7 @@ exports[`buildTimelineObjsForRundown infinite pieces infinite ending with previo "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": undefined, "previousPartOverlap": 0, }, @@ -850,6 +855,7 @@ exports[`buildTimelineObjsForRundown infinite pieces infinite ending with previo "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": undefined, "previousPartOverlap": 0, }, @@ -1007,6 +1013,7 @@ exports[`buildTimelineObjsForRundown infinite pieces infinite starting in curren "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": undefined, "previousPartOverlap": 0, }, @@ -1055,7 +1062,7 @@ exports[`buildTimelineObjsForRundown infinite pieces infinite stopping in curren ], "enable": { "end": "#part_group_part0.end + 0", - "start": 123, + "start": "#part_group_part0.start", }, "id": "part_group_piece6_infinite", "isPieceTimeline": true, @@ -1174,6 +1181,7 @@ exports[`buildTimelineObjsForRundown infinite pieces infinite stopping in curren "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": { "children": 3, "content": { @@ -1241,7 +1249,7 @@ exports[`buildTimelineObjsForRundown infinite pieces infinite stopping in curren ], "enable": { "end": "#part_group_part0.end + -100", - "start": 123, + "start": "#part_group_part0.start", }, "id": "part_group_piece6_infinite", "isPieceTimeline": true, @@ -1360,6 +1368,7 @@ exports[`buildTimelineObjsForRundown infinite pieces infinite stopping in curren "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": { "children": 3, "content": { @@ -1466,6 +1475,7 @@ exports[`buildTimelineObjsForRundown next part no autonext 1`] = ` "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": undefined, }, } @@ -1597,6 +1607,7 @@ exports[`buildTimelineObjsForRundown next part with autonext 1`] = ` "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": { "children": 3, "content": { @@ -1748,6 +1759,7 @@ exports[`buildTimelineObjsForRundown overlap and keepalive autonext into next pa "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": { "children": 3, "content": { @@ -1934,6 +1946,7 @@ exports[`buildTimelineObjsForRundown overlap and keepalive autonext into next pa "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": { "children": 3, "content": { @@ -2098,6 +2111,7 @@ exports[`buildTimelineObjsForRundown overlap and keepalive current and previous "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": undefined, "previousPartOverlap": 900, }, @@ -2256,6 +2270,7 @@ exports[`buildTimelineObjsForRundown overlap and keepalive current and previous "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": undefined, "previousPartOverlap": 900, }, @@ -2344,6 +2359,7 @@ exports[`buildTimelineObjsForRundown simple current part 1`] = ` "partInstanceId": "part0", "priority": 5, }, + "multiGatewayMode": true, "nextPartGroup": undefined, }, } diff --git a/packages/job-worker/src/playout/timeline/__tests__/lib.test.ts b/packages/job-worker/src/playout/timeline/__tests__/lib.test.ts new file mode 100644 index 00000000000..23ce4892da6 --- /dev/null +++ b/packages/job-worker/src/playout/timeline/__tests__/lib.test.ts @@ -0,0 +1,406 @@ +import { TimelineObjHoldMode, TimelineObjOnAirMode } from '@sofie-automation/blueprints-integration' +import { shouldIncludeObjectOnTimeline, TimelinePlayoutState } from '../lib.js' +import { literal } from '@sofie-automation/corelib/dist/lib' +import { TimelineObjectCoreExt } from '@sofie-automation/blueprints-integration' + +describe('shouldIncludeObjectOnTimeline', () => { + describe('holdMode filtering', () => { + it('should include object with NORMAL holdMode when not in hold', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.NORMAL, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should include object with NORMAL holdMode when in hold', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: true, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.NORMAL, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should include object with undefined holdMode when not in hold', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should include object with undefined holdMode when in hold', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: true, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should include object with EXCEPT holdMode when not in hold', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.EXCEPT, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should exclude object with EXCEPT holdMode when in hold', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: true, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.EXCEPT, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(false) + }) + + it('should include object with EXCEPT holdMode when in hold but includeWhenNotInHoldObjects is true', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: true, + includeWhenNotInHoldObjects: true, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.EXCEPT, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should exclude object with ONLY holdMode when not in hold', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.ONLY, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(false) + }) + + it('should include object with ONLY holdMode when in hold', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: true, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.ONLY, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + }) + + describe('onAirMode filtering', () => { + it('should include object with ALWAYS onAirMode when on air', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + onAirMode: TimelineObjOnAirMode.ALWAYS, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should include object with ALWAYS onAirMode when in rehearsal', () => { + const playoutState = literal({ + isRehearsal: true, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + onAirMode: TimelineObjOnAirMode.ALWAYS, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should include object with undefined onAirMode when on air', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should include object with undefined onAirMode when in rehearsal', () => { + const playoutState = literal({ + isRehearsal: true, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should include object with ONAIR onAirMode when on air', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + onAirMode: TimelineObjOnAirMode.ONAIR, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should exclude object with ONAIR onAirMode when in rehearsal', () => { + const playoutState = literal({ + isRehearsal: true, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + onAirMode: TimelineObjOnAirMode.ONAIR, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(false) + }) + + it('should exclude object with REHEARSAL onAirMode when on air', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + onAirMode: TimelineObjOnAirMode.REHEARSAL, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(false) + }) + + it('should include object with REHEARSAL onAirMode when in rehearsal', () => { + const playoutState = literal({ + isRehearsal: true, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + onAirMode: TimelineObjOnAirMode.REHEARSAL, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + }) + + describe('combined holdMode and onAirMode filtering', () => { + it('should exclude object with EXCEPT holdMode in hold, even if onAirMode would allow it', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: true, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.EXCEPT, + onAirMode: TimelineObjOnAirMode.ONAIR, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(false) + }) + + it('should exclude object with ONLY holdMode when not in hold, even if onAirMode would allow it', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.ONLY, + onAirMode: TimelineObjOnAirMode.ONAIR, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(false) + }) + + it('should exclude object with ONAIR onAirMode in rehearsal, even if holdMode would allow it', () => { + const playoutState = literal({ + isRehearsal: true, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.NORMAL, + onAirMode: TimelineObjOnAirMode.ONAIR, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(false) + }) + + it('should exclude object with REHEARSAL onAirMode when on air, even if holdMode would allow it', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.NORMAL, + onAirMode: TimelineObjOnAirMode.REHEARSAL, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(false) + }) + + it('should include object with ONLY holdMode and REHEARSAL onAirMode when in hold and in rehearsal', () => { + const playoutState = literal({ + isRehearsal: true, + isInHold: true, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.ONLY, + onAirMode: TimelineObjOnAirMode.REHEARSAL, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + + it('should include object with NORMAL holdMode and ONAIR onAirMode when not in hold and on air', () => { + const playoutState = literal({ + isRehearsal: false, + isInHold: false, + }) + const object = literal>({ + id: 'test', + enable: { start: 0 }, + layer: 'layer1', + content: { deviceType: 0 }, + priority: 0, + holdMode: TimelineObjHoldMode.NORMAL, + onAirMode: TimelineObjOnAirMode.ONAIR, + }) + + expect(shouldIncludeObjectOnTimeline(playoutState, object)).toBe(true) + }) + }) +}) diff --git a/packages/job-worker/src/playout/timeline/__tests__/multi-gateway.test.ts b/packages/job-worker/src/playout/timeline/__tests__/multi-gateway.test.ts new file mode 100644 index 00000000000..7d10d163f58 --- /dev/null +++ b/packages/job-worker/src/playout/timeline/__tests__/multi-gateway.test.ts @@ -0,0 +1,107 @@ +import { TimelineObjRundown, TimelineObjType } from '@sofie-automation/corelib/dist/dataModel/Timeline' +import { deNowifyInfinites } from '../multi-gateway.js' +import { TSR } from '@sofie-automation/blueprints-integration' +import { literal } from '@sofie-automation/corelib/dist/lib' + +describe('Multi-gateway', () => { + describe('deNowifyInfinites', () => { + test('preserves other enable properties when de-nowifying', () => { + const targetNowTime = 1000 + const obj1 = literal({ + id: 'obj1', + enable: { + start: 'now', + duration: 500, + }, + layer: 'layer1', + content: { + deviceType: TSR.DeviceType.ABSTRACT, + }, + objectType: TimelineObjType.RUNDOWN, + priority: 0, + }) + + const timelineObjsMap = { + [obj1.id]: obj1, + } + + deNowifyInfinites(targetNowTime, [obj1], timelineObjsMap) + + expect(obj1.enable).toEqual({ + start: targetNowTime, + duration: 500, + }) + }) + + test('preserves other enable properties when de-nowifying with parent group', () => { + const targetNowTime = 1500 + const parentObj = literal({ + id: 'parent', + enable: { + start: 500, + }, + layer: 'parentLayer', + content: { + deviceType: TSR.DeviceType.ABSTRACT, + }, + objectType: TimelineObjType.RUNDOWN, + priority: 0, + }) + + const obj1 = literal({ + id: 'obj1', + inGroup: 'parent', + enable: { + start: 'now', + duration: 200, + }, + layer: 'layer1', + content: { + deviceType: TSR.DeviceType.ABSTRACT, + }, + objectType: TimelineObjType.RUNDOWN, + priority: 0, + }) + + const timelineObjsMap = { + [parentObj.id]: parentObj, + [obj1.id]: obj1, + } + + deNowifyInfinites(targetNowTime, [obj1], timelineObjsMap) + + expect(obj1.enable).toEqual({ + start: targetNowTime - 500, // 1500 - 500 = 1000 + duration: 200, + }) + }) + + test('does nothing if start is not "now"', () => { + const targetNowTime = 1000 + const obj1 = literal({ + id: 'obj1', + enable: { + start: 500, + duration: 500, + }, + layer: 'layer1', + content: { + deviceType: TSR.DeviceType.ABSTRACT, + }, + objectType: TimelineObjType.RUNDOWN, + priority: 0, + }) + + const timelineObjsMap = { + [obj1.id]: obj1, + } + + deNowifyInfinites(targetNowTime, [obj1], timelineObjsMap) + + expect(obj1.enable).toEqual({ + start: 500, + duration: 500, + }) + }) + }) +}) diff --git a/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts b/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts index b45ef327502..15133e9d57b 100644 --- a/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts +++ b/packages/job-worker/src/playout/timeline/__tests__/rundown.test.ts @@ -173,7 +173,7 @@ describe('buildTimelineObjsForRundown', () => { const selectedPartInfos: SelectedPartInstancesTimelineInfo = {} const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).toHaveLength(1) expect(objs.timingContext).toBeUndefined() @@ -210,7 +210,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).toHaveLength(1) expect(objs.timingContext).toBeUndefined() @@ -230,7 +230,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -263,7 +263,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -295,7 +295,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -328,7 +328,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).toBeTruthy() @@ -369,7 +369,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -418,7 +418,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -471,7 +471,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -511,7 +511,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).toBeTruthy() @@ -574,7 +574,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -622,7 +622,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -650,7 +650,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -678,7 +678,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -705,7 +705,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -751,7 +751,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -798,7 +798,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() @@ -848,7 +848,7 @@ describe('buildTimelineObjsForRundown', () => { } const playlist = createMockPlaylist(selectedPartInfos) - const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos) + const objs = buildTimelineObjsForRundown(context, playlist, selectedPartInfos, true) expect(objs.timeline).not.toHaveLength(0) expect(objs.timingContext).not.toBeUndefined() diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 44acd584a40..aa95c879c62 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -1,4 +1,4 @@ -import { BlueprintId, RundownPlaylistId, TimelineHash } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { BlueprintId, RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { JobContext, JobStudio } from '../../jobs/index.js' import { ReadonlyDeep } from 'type-fest' import { BlueprintResultBaseline, OnGenerateTimelineObj, Time, TSR } from '@sofie-automation/blueprints-integration' @@ -37,7 +37,6 @@ import { deserializePieceTimelineObjectsBlob } from '@sofie-automation/corelib/d import { convertResolvedPieceInstanceToBlueprints } from '../../blueprints/context/lib.js' import { buildTimelineObjsForRundown, RundownTimelineTimingContext } from './rundown.js' import { SourceLayers } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { deNowifyMultiGatewayTimeline } from './multi-gateway.js' import { validateTimeline } from 'superfly-timeline' import { getPartTimingsOrDefaults, PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings' import { applyAbPlaybackForTimeline } from '../abPlayback/index.js' @@ -67,25 +66,19 @@ function generateTimelineVersions( } } -export async function updateStudioTimeline( +/** + * Generate timeline objects for a studio (when no playlist is active) + */ +export async function getStudioTimeline( context: JobContext, playoutModel: StudioPlayoutModel | PlayoutModel -): Promise { - const span = context.startSpan('updateStudioTimeline') - logger.debug('updateStudioTimeline running...') +): Promise<{ + objs: Array + versions: TimelineCompleteGenerationVersions + timingContext: undefined + regenerateTimelineToken: undefined +}> { const studio = context.studio - // Ensure there isn't a playlist active, as that should be using a different function call - if (isModelForStudio(playoutModel)) { - const activePlaylists = playoutModel.getActiveRundownPlaylists() - if (activePlaylists.length > 0) { - throw new Error(`Studio has an active playlist`) - } - } else { - if (playoutModel.playlist.activationId) { - throw new Error(`Studio has an active playlist`) - } - } - let baselineObjects: TimelineObjRundown[] = [] let studioBaseline: BlueprintResultBaseline | undefined @@ -119,60 +112,58 @@ export async function updateStudioTimeline( studioBlueprint?.blueprint?.blueprintVersion ?? '-' ) - flattenAndProcessTimelineObjects(context, baselineObjects) - - // Future: We should handle any 'now' objects that are at the root of this timeline - preserveOrReplaceNowTimesInObjects(playoutModel, baselineObjects) - - if (playoutModel.isMultiGatewayMode) { - logAnyRemainingNowTimes(context, baselineObjects) + if (studioBaseline) { + updateBaselineExpectedPackagesOnStudio(context, playoutModel, studioBaseline) } - const timelineHash = saveTimeline(context, playoutModel, baselineObjects, versions, undefined) + return { + objs: baselineObjects, + versions, + timingContext: undefined, + regenerateTimelineToken: undefined, + } +} - if (studioBaseline) { - updateBaselineExpectedPackagesOnStudio(context, playoutModel, studioBaseline) +export async function updateStudioTimeline( + context: JobContext, + playoutModel: StudioPlayoutModel | PlayoutModel +): Promise { + const span = context.startSpan('updateStudioTimeline') + logger.debug('updateStudioTimeline: marking studio as needing timeline update') + // Ensure there isn't a playlist active, as that should be using a different function call + if (isModelForStudio(playoutModel)) { + const activePlaylists = playoutModel.getActiveRundownPlaylists() + if (activePlaylists.length > 0) { + throw new Error(`Studio has an active playlist`) + } + } else { + if (playoutModel.playlist.activationId) { + throw new Error(`Studio has an active playlist`) + } } - logger.verbose(`updateStudioTimeline done, hash: "${timelineHash}"`) + playoutModel.markTimelineNeedsUpdate() + if (span) span.end() } export async function updateTimeline(context: JobContext, playoutModel: PlayoutModel): Promise { const span = context.startSpan('updateTimeline') - logger.debug('updateTimeline running...') + logger.debug('updateTimeline: marking playlist as needing timeline update') if (!playoutModel.playlist.activationId) { throw new Error(`RundownPlaylist ("${playoutModel.playlist._id}") is not active")`) } - const { - versions, - objs: timelineObjs, - timingContext: timingInfo, - regenerateTimelineToken, - } = await getTimelineRundown(context, playoutModel) - - flattenAndProcessTimelineObjects(context, timelineObjs) - - preserveOrReplaceNowTimesInObjects(playoutModel, timelineObjs) - - if (playoutModel.isMultiGatewayMode) { - deNowifyMultiGatewayTimeline(playoutModel, timelineObjs, timingInfo) - - logAnyRemainingNowTimes(context, timelineObjs) - } - - const timelineHash = saveTimeline(context, playoutModel, timelineObjs, versions, regenerateTimelineToken) - logger.verbose(`updateTimeline done, hash: "${timelineHash}"`) + playoutModel.markTimelineNeedsUpdate() if (span) span.end() } -function preserveOrReplaceNowTimesInObjects( +export function preserveOrReplaceNowTimesInObjects( studioPlayoutModel: StudioPlayoutModelBase, timelineObjs: Array -) { +): void { const timeline = studioPlayoutModel.timeline const oldTimelineObjsMap = normalizeArray( (timeline?.timelineBlob !== undefined && deserializeTimelineBlob(timeline.timelineBlob)) || [], @@ -202,7 +193,7 @@ function preserveOrReplaceNowTimesInObjects( }) } -function logAnyRemainingNowTimes(_context: JobContext, timelineObjs: Array): void { +export function logAnyRemainingNowTimes(_context: JobContext, timelineObjs: Array): void { const badTimelineObjs: any[] = [] for (const obj of timelineObjs) { @@ -229,22 +220,6 @@ function hasNow(obj: TimelineEnableExt | TimelineEnableExt[]) { return res } -/** Store the timelineobjects into the model, and perform any post-save actions */ -export function saveTimeline( - context: JobContext, - studioPlayoutModel: StudioPlayoutModelBase, - timelineObjs: TimelineObjGeneric[], - generationVersions: TimelineCompleteGenerationVersions, - regenerateTimelineToken: string | undefined -): TimelineHash { - const newTimeline = studioPlayoutModel.setTimeline(timelineObjs, generationVersions, regenerateTimelineToken) - - // Also do a fast-track for the timeline to be published faster: - context.hackPublishTimelineToFastTrack(newTimeline) - - return newTimeline.timelineHash -} - export interface SelectedPartInstancesTimelineInfo { previous?: SelectedPartInstanceTimelineInfo current?: SelectedPartInstanceTimelineInfo @@ -303,7 +278,7 @@ function getPartInstanceTimelineInfo( /** * Returns timeline objects related to rundowns in a studio */ -async function getTimelineRundown( +export async function getTimelineRundown( context: JobContext, playoutModel: PlayoutModel ): Promise<{ @@ -378,7 +353,12 @@ async function getTimelineRundown( logger.warn(`Missing Baseline objects for Rundown "${activeRundown.rundown._id}"`) } - const rundownTimelineResult = buildTimelineObjsForRundown(context, playoutModel.playlist, partInstancesInfo) + const rundownTimelineResult = buildTimelineObjsForRundown( + context, + playoutModel.playlist, + partInstancesInfo, + playoutModel.isMultiGatewayMode + ) timelineObjs = timelineObjs.concat(rundownTimelineResult.timeline) timelineObjs = timelineObjs.concat(await pLookaheadObjs) @@ -435,7 +415,8 @@ async function getTimelineRundown( if (blueprint.blueprint.onTimelineGenerate) { const blueprintPersistentState = new PersistentPlayoutStateStore( - playoutModel.playlist.previousPersistentState + playoutModel.playlist.privatePlayoutPersistentState, + playoutModel.playlist.publicPlayoutPersistentState ) const span = context.startSpan('blueprint.onTimelineGenerate') @@ -457,9 +438,7 @@ async function getTimelineRundown( }) }) - if (blueprintPersistentState.hasChanges) { - playoutModel.setBlueprintPersistentState(blueprintPersistentState.getAll()) - } + blueprintPersistentState.saveToModel(playoutModel) } playoutModel.setAbResolvingState( @@ -557,7 +536,7 @@ function createRegenerateTimelineObj( * @param context * @param timelineObjs Array of timeline objects */ -function flattenAndProcessTimelineObjects(context: JobContext, timelineObjs: Array): void { +export function flattenAndProcessTimelineObjects(context: JobContext, timelineObjs: Array): void { const span = context.startSpan('processTimelineObjects') // first, split out any grouped objects, to make the timeline shallow: diff --git a/packages/job-worker/src/playout/timeline/lib.ts b/packages/job-worker/src/playout/timeline/lib.ts index 4dbe0491a2e..243b4fc7570 100644 --- a/packages/job-worker/src/playout/timeline/lib.ts +++ b/packages/job-worker/src/playout/timeline/lib.ts @@ -1,7 +1,13 @@ -import { IBlueprintPieceType } from '@sofie-automation/blueprints-integration' +import { + IBlueprintPieceType, + TimelineObjectCoreExt, + TimelineObjHoldMode, + TimelineObjOnAirMode, +} from '@sofie-automation/blueprints-integration' import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { ReadonlyDeep } from 'type-fest' import { DEFINITELY_ENDED_FUTURE_DURATION } from '../infinites.js' +import { assertNever } from '@sofie-automation/corelib/dist/lib' /** * Check if a PieceInstance has 'definitely ended'. @@ -37,3 +43,61 @@ export function hasPieceInstanceDefinitelyEnded( return relativeEnd !== undefined && relativeEnd + DEFINITELY_ENDED_FUTURE_DURATION < nowInPart } + +export interface TimelinePlayoutState { + /** Whether the playout is currently in rehearsal mode */ + isRehearsal: boolean + /** If true, we're playing in a HOLD situation */ + isInHold: boolean + /** + * If true, objects with holdMode EXCEPT will be included on the timeline, even when in hold. + * This is used for infinite, when their pieces belong to both sides of the HOLD + */ + includeWhenNotInHoldObjects?: boolean +} + +/** + * Whether a timeline object should be included on the timeline + * This uses some specific properties on the object which define this behaviour + */ +export function shouldIncludeObjectOnTimeline( + playoutState: TimelinePlayoutState, + object: TimelineObjectCoreExt +): boolean { + // Some objects can be filtered out at times based on the holdMode of the object + switch (object.holdMode) { + case TimelineObjHoldMode.NORMAL: + case undefined: + break + case TimelineObjHoldMode.EXCEPT: + if (playoutState.isInHold && !playoutState.includeWhenNotInHoldObjects) { + return false + } + break + case TimelineObjHoldMode.ONLY: + if (!playoutState.isInHold) { + return false + } + break + default: + assertNever(object.holdMode) + } + + // Some objects should be filtered depending on the onair mode + switch (object.onAirMode) { + case TimelineObjOnAirMode.ALWAYS: + case undefined: + break + case TimelineObjOnAirMode.ONAIR: + if (playoutState.isRehearsal) return false + break + case TimelineObjOnAirMode.REHEARSAL: + if (!playoutState.isRehearsal) return false + + break + default: + assertNever(object.onAirMode) + } + + return true +} diff --git a/packages/job-worker/src/playout/timeline/multi-gateway.ts b/packages/job-worker/src/playout/timeline/multi-gateway.ts index 48cc504d55c..b2ee15f78b5 100644 --- a/packages/job-worker/src/playout/timeline/multi-gateway.ts +++ b/packages/job-worker/src/playout/timeline/multi-gateway.ts @@ -130,12 +130,12 @@ function updatePartInstancePlannedTimes( * regeneration, items will already use the timestamps persited by `updatePlannedTimingsForPieceInstances` and will not * be included in `infiniteObjs`. */ -function deNowifyInfinites( +export function deNowifyInfinites( targetNowTime: number, /** A list of objects that need to be updated */ infiniteObjs: TimelineObjRundown[], timelineObjsMap: Record -) { +): void { /** * Recursively look up the absolute starttime of a timeline object * taking into account its parent's times. @@ -163,7 +163,7 @@ function deNowifyInfinites( if (Array.isArray(obj.enable) || obj.enable.start !== 'now') continue if (!obj.inGroup) { - obj.enable = { start: targetNowTime } + obj.enable = { ...obj.enable, start: targetNowTime } continue } @@ -181,7 +181,7 @@ function deNowifyInfinites( continue } - obj.enable = { start: targetNowTime - parentStartTime } + obj.enable = { ...obj.enable, start: targetNowTime - parentStartTime } logger.silly( `deNowifyInfinites: Setting "${obj.id}" enable.start = ${JSON.stringify(obj.enable.start)}, ${targetNowTime} ${parentStartTime} parentObject: "${parentObject.id}"` ) diff --git a/packages/job-worker/src/playout/timeline/part.ts b/packages/job-worker/src/playout/timeline/part.ts index b697f7da27f..61c4e248748 100644 --- a/packages/job-worker/src/playout/timeline/part.ts +++ b/packages/job-worker/src/playout/timeline/part.ts @@ -19,6 +19,7 @@ import { getPieceEnableInsidePart, transformPieceGroupAndObjects } from './piece import { PlayoutChangedType } from '@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI' import { SelectedPartInstanceTimelineInfo } from './generate.js' import { PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings' +import { TimelinePlayoutState } from './lib.js' export function transformPartIntoTimeline( context: JobContext, @@ -28,7 +29,7 @@ export function transformPartIntoTimeline( parentGroup: TimelineObjGroupPart & OnGenerateTimelineObjExt, partInfo: SelectedPartInstanceTimelineInfo, nextPartTimings: PartCalculatedTimings | null, - isInHold: boolean + playoutState: TimelinePlayoutState ): Array { const span = context.startSpan('transformPartIntoTimeline') @@ -68,8 +69,7 @@ export function transformPartIntoTimeline( pieceEnable, pieceInstance.dynamicallyInserted ? 0 : partTimings.toPartDelay, pieceGroupFirstObjClasses, - isInHold, - false + playoutState ) ) } diff --git a/packages/job-worker/src/playout/timeline/piece.ts b/packages/job-worker/src/playout/timeline/piece.ts index 2d5bf40a02d..1e46f6bcd89 100644 --- a/packages/job-worker/src/playout/timeline/piece.ts +++ b/packages/job-worker/src/playout/timeline/piece.ts @@ -1,4 +1,4 @@ -import { TSR, TimelineObjectCoreExt, TimelineObjHoldMode } from '@sofie-automation/blueprints-integration' +import { TSR, TimelineObjectCoreExt } from '@sofie-automation/blueprints-integration' import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { deserializePieceTimelineObjectsBlob } from '@sofie-automation/corelib/dist/dataModel/Piece' import { @@ -7,14 +7,14 @@ import { TimelineObjType, TimelineObjGroupPart, } from '@sofie-automation/corelib/dist/dataModel/Timeline' -import { assertNever, clone } from '@sofie-automation/corelib/dist/lib' +import { clone } from '@sofie-automation/corelib/dist/lib' import { PieceInstanceWithTimings } from '@sofie-automation/corelib/dist/playout/processAndPrune' import { createPieceGroupAndCap } from './pieceGroup.js' import { PartCalculatedTimings } from '@sofie-automation/corelib/dist/playout/timings' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { ReadonlyDeep } from 'type-fest' import { prefixAllObjectIds } from '../lib.js' -import { hasPieceInstanceDefinitelyEnded } from './lib.js' +import { hasPieceInstanceDefinitelyEnded, shouldIncludeObjectOnTimeline, TimelinePlayoutState } from './lib.js' export function transformPieceGroupAndObjects( playlistId: RundownPlaylistId, @@ -25,9 +25,7 @@ export function transformPieceGroupAndObjects( /** If the start of the piece has been offset inside the partgroup */ pieceStartOffset: number, controlObjClasses: string[], - /** If true, we're playing in a HOLD situation */ - isInHold: boolean, - includeHoldExceptObjects: boolean + playoutState: TimelinePlayoutState ): Array { // If a piece has definitely finished playback, then we can prune its contents. But we can only do that check if the part has an absolute time, otherwise we are only guessing const hasDefinitelyEnded = @@ -50,24 +48,7 @@ export function transformPieceGroupAndObjects( const objects = deserializePieceTimelineObjectsBlob(pieceInstance.piece.timelineObjectsString) for (const o of objects) { - // Some objects can be filtered out at times based on the holdMode of the object - switch (o.holdMode) { - case TimelineObjHoldMode.NORMAL: - case undefined: - break - case TimelineObjHoldMode.EXCEPT: - if (isInHold && !includeHoldExceptObjects) { - continue - } - break - case TimelineObjHoldMode.ONLY: - if (!isInHold) { - continue - } - break - default: - assertNever(o.holdMode) - } + if (!shouldIncludeObjectOnTimeline(playoutState, o)) continue pieceObjects.push({ metaData: undefined, diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index a473ed066fc..d2848356d6c 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -40,6 +40,8 @@ export interface RundownTimelineTimingContext { nextPartGroup?: TimelineObjGroupPart nextPartOverlap?: number + + multiGatewayMode: boolean } export interface RundownTimelineResult { timeline: (TimelineObjRundown & OnGenerateTimelineObjExt)[] @@ -49,7 +51,8 @@ export interface RundownTimelineResult { export function buildTimelineObjsForRundown( context: JobContext, activePlaylist: ReadonlyDeep, - partInstancesInfo: SelectedPartInstancesTimelineInfo + partInstancesInfo: SelectedPartInstancesTimelineInfo, + multiGatewayMode: boolean ): RundownTimelineResult { const span = context.startSpan('buildTimelineObjsForRundown') const timelineObjs: Array = [] @@ -139,6 +142,7 @@ export function buildTimelineObjsForRundown( const timingContext: RundownTimelineTimingContext = { currentPartGroup, currentPartDuration: currentPartEnable.duration, + multiGatewayMode, } // Start generating objects @@ -189,7 +193,10 @@ export function buildTimelineObjsForRundown( currentPartGroup, partInstancesInfo.current, partInstancesInfo.next?.calculatedTimings ?? null, - activePlaylist.holdState === RundownHoldState.ACTIVE + { + isRehearsal: !!activePlaylist.rehearsal, + isInHold: activePlaylist.holdState === RundownHoldState.ACTIVE, + } ) ) @@ -317,8 +324,11 @@ function generateCurrentInfinitePieceObjects( pieceEnable, 0, groupClasses, - isInHold, - isOriginOfInfinite + { + isRehearsal: !!activePlaylist.rehearsal, + isInHold: isInHold, + includeWhenNotInHoldObjects: isOriginOfInfinite, + } ), ] } @@ -360,10 +370,10 @@ function calculateInfinitePieceEnable( pieceEnable.start = 0 // Future: should this consider the prerollDuration? - } else if (pieceInstance.plannedStartedPlayback !== undefined) { - // We have a absolute start time, so we should use that. - let infiniteGroupStart = pieceInstance.plannedStartedPlayback - nowInParent = currentTime - pieceInstance.plannedStartedPlayback + } else if (!timingContext.multiGatewayMode && pieceInstance.reportedStartedPlayback !== undefined) { + // We have a absolute start time, so we should use that, but only if not in multiGatewayMode + let infiniteGroupStart = pieceInstance.reportedStartedPlayback + nowInParent = currentTime - pieceInstance.reportedStartedPlayback // infiniteGroupStart had an actual timestamp inside and pieceEnable.start being a number // means that it expects an offset from it's parent @@ -489,7 +499,10 @@ function generatePreviousPartInstanceObjects( previousPartGroup, previousPartInfo, currentPartInstanceTimings, - activePlaylist.holdState === RundownHoldState.ACTIVE + { + isRehearsal: !!activePlaylist.rehearsal, + isInHold: activePlaylist.holdState === RundownHoldState.ACTIVE, + } ), ] } else { @@ -532,7 +545,10 @@ function generateNextPartInstanceObjects( nextPartGroup, nextPartInfo, null, - false + { + isRehearsal: !!activePlaylist.rehearsal, + isInHold: false, + } ), ] } diff --git a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts index 831b651d9e3..399f999e7ad 100644 --- a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts +++ b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts @@ -4,7 +4,6 @@ import { OnTimelineTriggerTimeProps } from '@sofie-automation/corelib/dist/worke import { logger } from '../../logging.js' import { JobContext } from '../../jobs/index.js' import { runJobWithPlaylistLock } from '../lock.js' -import { saveTimeline } from '../timeline/generate.js' import { applyToArray, normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance' import { runJobWithStudioPlayoutModel } from '../../studio/lock.js' @@ -69,7 +68,6 @@ export async function handleTimelineTriggerTime(context: JobContext, data: OnTim // Take ownership of the playlist in the db, so that we can mutate the timeline and piece instances const changes = timelineTriggerTimeInner( - context, studioCache, data.results, partInstanceMap, @@ -81,7 +79,7 @@ export async function handleTimelineTriggerTime(context: JobContext, data: OnTim }) } else { // No playlist is active. no extra lock needed - timelineTriggerTimeInner(context, studioCache, data.results, undefined, undefined, undefined) + timelineTriggerTimeInner(studioCache, data.results, undefined, undefined, undefined) } }) } @@ -123,7 +121,6 @@ interface PieceInstancesChanges { } function timelineTriggerTimeInner( - context: JobContext, studioPlayoutModel: StudioPlayoutModel, results: OnTimelineTriggerTimeProps['results'], partInstances: Map> | undefined, @@ -205,14 +202,11 @@ function timelineTriggerTimeInner( } } if (tlChanged) { - const timelineHash = saveTimeline( - context, - studioPlayoutModel, + const timelineHash = studioPlayoutModel.setTimeline( timelineObjs, - // Preserve some current values: timeline.generationVersions, timeline.regenerateTimelineToken - ) + ).timelineHash logger.verbose(`timelineTriggerTime: Updated Timeline, hash: "${timelineHash}"`) } diff --git a/packages/job-worker/src/playout/upgrade.ts b/packages/job-worker/src/playout/upgrade.ts index aa95875a72e..d6b97f8894c 100644 --- a/packages/job-worker/src/playout/upgrade.ts +++ b/packages/job-worker/src/playout/upgrade.ts @@ -188,6 +188,11 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data enableEvaluationForm: true, } + const packageContainerSettings = result.packageContainerSettings ?? { + previewContainerIds: [], + thumbnailContainerIds: [], + } + await context.directCollections.Studios.update(context.studioId, { $set: { 'settingsWithOverrides.defaults': studioSettings, @@ -198,6 +203,7 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data 'peripheralDeviceSettings.inputDevices.defaults': inputDevices, 'routeSetsWithOverrides.defaults': routeSets, 'routeSetExclusivityGroupsWithOverrides.defaults': routeSetExclusivityGroups, + 'packageContainerSettingsWithOverrides.defaults': packageContainerSettings, 'packageContainersWithOverrides.defaults': packageContainers, lastBlueprintConfig: { blueprintHash: blueprint.blueprintDoc.blueprintHash, diff --git a/packages/job-worker/src/rundownPlaylists.ts b/packages/job-worker/src/rundownPlaylists.ts index 86e637802a0..33faf33e29a 100644 --- a/packages/job-worker/src/rundownPlaylists.ts +++ b/packages/job-worker/src/rundownPlaylists.ts @@ -236,6 +236,11 @@ export function produceRundownPlaylistInfoFromRundown( nextPartInfo: null, previousPartInfo: null, rundownIdsInOrder: [], + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], ...clone(existingPlaylist), @@ -332,6 +337,11 @@ function defaultPlaylistForRundown( nextPartInfo: null, previousPartInfo: null, rundownIdsInOrder: [], + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], ...clone(existingPlaylist), diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModel.ts b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts index 33145c4e6e8..788efee63ee 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModel.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModel.ts @@ -79,4 +79,10 @@ export interface StudioPlayoutModel extends StudioPlayoutModelBase, BaseModel { * @returns Whether the change may affect timeline generation */ switchRouteSet(routeSetId: string, isActive: boolean | 'toggle'): boolean + + /** + * Mark the studio as needing a timeline update. + * The timeline will be generated and published when model is ready to be saved. + */ + markTimelineNeedsUpdate(): void } diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts index 55a8e97808a..4dba1885ef1 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts @@ -18,6 +18,13 @@ import { DatabasePersistedModel } from '../../modelBase.js' import { ExpectedPlayoutItemStudio } from '@sofie-automation/corelib/dist/dataModel/ExpectedPlayoutItem' import { StudioBaselineHelper } from './StudioBaselineHelper.js' import { ExpectedPackage } from '@sofie-automation/blueprints-integration' +import { + getStudioTimeline, + flattenAndProcessTimelineObjects, + preserveOrReplaceNowTimesInObjects, + logAnyRemainingNowTimes, +} from '../../playout/timeline/generate.js' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' /** * This is a model used for studio operations. @@ -33,6 +40,7 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { public readonly rundownPlaylists: ReadonlyDeep #timelineHasChanged = false + #timelineNeedsRegeneration = false #timeline: TimelineComplete | null public get timeline(): TimelineComplete | null { @@ -111,6 +119,10 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { return this.context.setRouteSetActive(routeSetId, isActive) } + markTimelineNeedsUpdate(): void { + this.#timelineNeedsRegeneration = true + } + /** * Discards all documents in this model, and marks it as unusable */ @@ -118,6 +130,30 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { this.#disposed = true } + async #regenerateStudioTimeline(): Promise { + const span = this.context.startSpan('StudioPlayoutModelImpl.regenerateStudioTimeline') + logger.debug('Regenerating studio timeline...') + + try { + const { versions, objs: timelineObjs } = await getStudioTimeline(this.context, this) + + flattenAndProcessTimelineObjects(this.context, timelineObjs) + preserveOrReplaceNowTimesInObjects(this, timelineObjs) + + if (this.isMultiGatewayMode) { + logAnyRemainingNowTimes(this.context, timelineObjs) + } + + const timelineHash = this.setTimeline(timelineObjs, versions, undefined).timelineHash + logger.verbose(`Studio timeline regeneration done, hash: "${timelineHash}"`) + } catch (err) { + logger.error(`Error regenerating studio timeline: ${stringifyError(err)}`) + throw err + } finally { + if (span) span.end() + } + } + async saveAllToDatabase(): Promise { if (this.#disposed) { throw new Error('Cannot save disposed PlayoutModel') @@ -125,8 +161,17 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { const span = this.context.startSpan('StudioPlayoutModelImpl.saveAllToDatabase') + // Generate timeline if needed + if (this.#timelineNeedsRegeneration) { + await this.#regenerateStudioTimeline() + this.#timelineNeedsRegeneration = false + } + // Prioritise the timeline for publication reasons if (this.#timelineHasChanged && this.#timeline) { + // Do a fast-track for the timeline to be published faster: + this.context.hackPublishTimelineToFastTrack(this.#timeline) + await this.context.directCollections.Timelines.replace(this.#timeline) } this.#timelineHasChanged = false diff --git a/packages/job-worker/src/workers/ingest/jobs.ts b/packages/job-worker/src/workers/ingest/jobs.ts index 2bc85736cac..a6a263087d4 100644 --- a/packages/job-worker/src/workers/ingest/jobs.ts +++ b/packages/job-worker/src/workers/ingest/jobs.ts @@ -39,7 +39,7 @@ import { handleBucketRemoveAdlibPiece, } from '../../ingest/bucket/bucketAdlibs.js' import { handleBucketItemImport, handleBucketItemRegenerate } from '../../ingest/bucket/import.js' -import { handleUserExecuteChangeOperation } from '../../ingest/userOperation.js' +import { handlePlayoutExecuteChangeOperation, handleUserExecuteChangeOperation } from '../../ingest/userOperation.js' import { wrapCustomIngestJob, wrapGenericIngestJob, @@ -86,6 +86,7 @@ export const ingestJobHandlers: IngestJobHandlers = { [IngestJobs.UserRemoveRundown]: handleUserRemoveRundown, [IngestJobs.UserUnsyncRundown]: handleUserUnsyncRundown, [IngestJobs.UserExecuteChangeOperation]: handleUserExecuteChangeOperation, + [IngestJobs.PlayoutExecuteChangeOperation]: handlePlayoutExecuteChangeOperation, [IngestJobs.BucketItemImport]: handleBucketItemImport, [IngestJobs.BucketItemRegenerate]: handleBucketItemRegenerate, diff --git a/packages/job-worker/src/workers/studio/child.ts b/packages/job-worker/src/workers/studio/child.ts index 57974fbb737..e01783a4ef7 100644 --- a/packages/job-worker/src/workers/studio/child.ts +++ b/packages/job-worker/src/workers/studio/child.ts @@ -1,5 +1,6 @@ import { studioJobHandlers } from './jobs.js' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' import { MongoClient } from 'mongodb' import { createMongoConnection, getMongoCollections, IDirectCollections } from '../../db/index.js' import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' @@ -75,6 +76,16 @@ export class StudioWorkerChild { } logger.info(`Studio thread for ${this.#studioId} initialised`) + + // Queue initial T-Timer recalculation to set up timers after startup + this.#queueJob( + getStudioQueueName(this.#studioId), + StudioJobs.RecalculateTTimerProjections, + undefined, + undefined + ).catch((err) => { + logger.error(`Failed to queue initial T-Timer recalculation: ${err}`) + }) } async lockChange(lockId: string, locked: boolean): Promise { if (!this.#staticData) throw new Error('Worker not initialised') diff --git a/packages/job-worker/src/workers/studio/jobs.ts b/packages/job-worker/src/workers/studio/jobs.ts index be5d81787da..89928fd3b9f 100644 --- a/packages/job-worker/src/workers/studio/jobs.ts +++ b/packages/job-worker/src/workers/studio/jobs.ts @@ -49,6 +49,7 @@ import { handleActivateAdlibTesting } from '../../playout/adlibTesting.js' import { handleExecuteBucketAdLibOrAction } from '../../playout/bucketAdlibJobs.js' import { handleSwitchRouteSet } from '../../studio/routeSet.js' import { handleCleanupOrphanedExpectedPackageReferences } from '../../playout/expectedPackages.js' +import { handleRecalculateTTimerProjections } from '../../playout/tTimersJobs.js' type ExecutableFunction = ( context: JobContext, @@ -87,6 +88,8 @@ export const studioJobHandlers: StudioJobHandlers = { [StudioJobs.OnPlayoutPlaybackChanged]: handleOnPlayoutPlaybackChanged, [StudioJobs.OnTimelineTriggerTime]: handleTimelineTriggerTime, + [StudioJobs.RecalculateTTimerProjections]: handleRecalculateTTimerProjections, + [StudioJobs.UpdateStudioBaseline]: handleUpdateStudioBaseline, [StudioJobs.CleanupEmptyPlaylists]: handleRemoveEmptyPlaylists, diff --git a/packages/lerna.json b/packages/lerna.json index 26ccbc1db55..2d2b10ddfd6 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "1.53.0-in-development", - "npmClient": "yarn", - "$schema": "node_modules/lerna/schemas/lerna-schema.json" -} + "version": "26.3.0-2", + "npmClient": "yarn", + "$schema": "node_modules/lerna/schemas/lerna-schema.json" +} \ No newline at end of file diff --git a/packages/live-status-gateway-api/api/components/piece/pieceStatus/abSessionAssignment-example.yaml b/packages/live-status-gateway-api/api/components/piece/pieceStatus/abSessionAssignment-example.yaml new file mode 100644 index 00000000000..cf30c017e28 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/piece/pieceStatus/abSessionAssignment-example.yaml @@ -0,0 +1,3 @@ +poolName: 'VTR' +sessionName: 'clip_intro' +playerId: 1 diff --git a/packages/live-status-gateway-api/api/components/piece/pieceStatus/abSessionAssignment.yaml b/packages/live-status-gateway-api/api/components/piece/pieceStatus/abSessionAssignment.yaml new file mode 100644 index 00000000000..420bc683807 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/piece/pieceStatus/abSessionAssignment.yaml @@ -0,0 +1,16 @@ +type: object +title: AbSessionAssignment +properties: + poolName: + description: The name of the AB Pool this session is for + type: string + sessionName: + description: Name of the session + type: string + playerId: + description: The assigned player ID + oneOf: + - type: string + - type: number +required: [poolName, sessionName, playerId] +additionalProperties: false diff --git a/packages/live-status-gateway-api/api/components/piece/pieceStatus/pieceStatus-example.yaml b/packages/live-status-gateway-api/api/components/piece/pieceStatus/pieceStatus-example.yaml index 8763f524cb6..e1220e5c4f9 100644 --- a/packages/live-status-gateway-api/api/components/piece/pieceStatus/pieceStatus-example.yaml +++ b/packages/live-status-gateway-api/api/components/piece/pieceStatus/pieceStatus-example.yaml @@ -5,3 +5,5 @@ outputLayer: 'PGM' tags: ['camera'] publicData: switcherSource: 1 +abSessions: + - $ref: './abSessionAssignment-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/piece/pieceStatus/pieceStatus.yaml b/packages/live-status-gateway-api/api/components/piece/pieceStatus/pieceStatus.yaml index 5846fa8ffd7..2fb2d5fb9be 100644 --- a/packages/live-status-gateway-api/api/components/piece/pieceStatus/pieceStatus.yaml +++ b/packages/live-status-gateway-api/api/components/piece/pieceStatus/pieceStatus.yaml @@ -22,6 +22,11 @@ $defs: type: string publicData: description: Optional arbitrary data + abSessions: + description: AB playback session assignments for this Piece + type: array + items: + $ref: './abSessionAssignment.yaml' required: [id, name, sourceLayer, outputLayer] additionalProperties: false examples: diff --git a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml index d09a8222ef3..05ef11767ac 100644 --- a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml +++ b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent-example.yaml @@ -15,3 +15,18 @@ timing: $ref: '../../timing/activePlaylistTiming/activePlaylistTiming-example.yaml' quickLoop: $ref: '../../quickLoop/activePlaylistQuickLoop/activePlaylistQuickLoop-example.yaml' +tTimers: + - index: 1 + label: 'On Air Timer' + configured: true + mode: + $ref: '../../tTimers/tTimerMode/tTimerModeCountdown-example.yaml' + - index: 2 + label: '' + configured: false + mode: null + - index: 3 + label: 'Studio Clock' + configured: true + mode: + $ref: '../../tTimers/tTimerMode/tTimerModeFreeRun-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml index c41fed04c05..594635eed11 100644 --- a/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml +++ b/packages/live-status-gateway-api/api/components/playlist/activePlaylistEvent/activePlaylistEvent.yaml @@ -40,11 +40,20 @@ $defs: - type: 'null' publicData: description: Optional arbitrary data + playoutState: + description: Blueprint-defined playout state, used to expose arbitrary information about playout timing: $ref: '../../timing/activePlaylistTiming/activePlaylistTiming.yaml#/$defs/activePlaylistTiming' quickLoop: $ref: '../../quickLoop/activePlaylistQuickLoop/activePlaylistQuickLoop.yaml#/$defs/activePlaylistQuickLoop' - required: [event, id, externalId, name, rundownIds, currentPart, currentSegment, nextPart, timing] + tTimers: + description: T-timers for the playlist. Always contains 3 elements (one for each timer slot). + type: array + items: + $ref: '../../tTimers/tTimerStatus/tTimerStatus.yaml#/$defs/tTimerStatus' + minItems: 3 + maxItems: 3 + required: [event, id, externalId, name, rundownIds, currentPart, currentSegment, nextPart, timing, tTimers] additionalProperties: false examples: - $ref: './activePlaylistEvent-example.yaml' diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml new file mode 100644 index 00000000000..aab940cecd5 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml @@ -0,0 +1,6 @@ +$defs: + tTimerIndex: + type: integer + title: TTimerIndex + description: Timer index (1-3). The playlist always has 3 T-timer slots. + enum: [1, 2, 3] diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml new file mode 100644 index 00000000000..76f7301261c --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml @@ -0,0 +1,77 @@ +$defs: + tTimerModeCountdown: + type: object + title: TTimerModeCountdown + description: Countdown timer mode - counts down from a duration + properties: + type: + type: string + const: countdown + paused: + description: Whether the timer is currently paused + type: boolean + zeroTime: + description: >- + Unix timestamp (ms) when the timer reaches/reached zero. + Present when paused is false. The client calculates remaining time as zeroTime - Date.now(). + type: number + pauseTime: + description: >- + Unix timestamp (ms) when the timer should automatically pause. + Typically set to when the current part ends and overrun begins. + When present and current time >= pauseTime, the timer should display as paused at (zeroTime - pauseTime). + type: number + remainingMs: + description: >- + Frozen remaining duration in milliseconds. + Present when paused is true. + type: number + durationMs: + description: Total countdown duration in milliseconds (the original configured duration) + type: number + stopAtZero: + description: Whether timer stops at zero or continues into negative values + type: boolean + required: [type, paused, durationMs, stopAtZero] + additionalProperties: false + examples: + - $ref: './tTimerModeCountdown-example.yaml' + + tTimerModeFreeRun: + type: object + title: TTimerModeFreeRun + description: Free-running timer mode - counts up from start time + properties: + type: + type: string + const: freeRun + paused: + description: Whether the timer is currently paused + type: boolean + zeroTime: + description: >- + Unix timestamp (ms) when the timer was at zero (i.e. when it was started). + Present when paused is false. The client calculates elapsed time as Date.now() - zeroTime. + type: number + pauseTime: + description: >- + Unix timestamp (ms) when the timer should automatically pause. + Typically set to when the current part ends and overrun begins. + When present and current time >= pauseTime, the timer should display as paused at (pauseTime - zeroTime). + type: number + elapsedMs: + description: >- + Frozen elapsed time in milliseconds. + Present when paused is true. + type: number + required: [type, paused] + additionalProperties: false + examples: + - $ref: './tTimerModeFreeRun-example.yaml' + + tTimerMode: + title: TTimerMode + description: The mode/state of a T-timer + oneOf: + - $ref: '#/$defs/tTimerModeCountdown' + - $ref: '#/$defs/tTimerModeFreeRun' diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml new file mode 100644 index 00000000000..bcc642bbe7f --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml @@ -0,0 +1,5 @@ +type: countdown +paused: false +zeroTime: 1706371920000 +durationMs: 120000 +stopAtZero: true diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml new file mode 100644 index 00000000000..1cad209ada8 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml @@ -0,0 +1,3 @@ +type: freeRun +paused: false +zeroTime: 1706371800000 diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml new file mode 100644 index 00000000000..94e3027369d --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml @@ -0,0 +1,13 @@ +index: 1 +label: 'Segment Timer' +configured: true +mode: + type: countdown + paused: false + zeroTime: 1706371920000 + durationMs: 120000 + stopAtZero: true +projected: + paused: false + zeroTime: 1706371920000 +anchorPartId: 'part_break_1' diff --git a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml new file mode 100644 index 00000000000..acd2c344a78 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml @@ -0,0 +1,56 @@ +$defs: + tTimerStatus: + type: object + title: TTimerStatus + description: Status of a single T-timer in the playlist + properties: + index: + $ref: '../tTimerIndex.yaml#/$defs/tTimerIndex' + label: + description: User-defined label for the timer + type: string + configured: + description: Whether the timer has been configured (mode is not null) + type: boolean + mode: + description: Timer mode and timing state. Null if not configured. + oneOf: + - type: 'null' + - $ref: '../tTimerMode/tTimerMode.yaml#/$defs/tTimerMode' + projected: + description: >- + Projected timing for when we expect to reach an anchor part. + Used to calculate over/under diff + oneOf: + - type: 'null' + - type: object + title: TTimerProjected + description: >- + Projected timing state for a T-timer + properties: + paused: + description: Whether the projected time is frozen + type: boolean + zeroTime: + description: >- + Unix timestamp in milliseconds of projected arrival at the anchor part + type: number + pauseTime: + description: >- + Unix timestamp (ms) when the projected timer should automatically pause. + When present and current time >= pauseTime, the projected duration should be calculated from pauseTime. + type: number + durationMs: + description: >- + Frozen remaining duration projection in milliseconds + type: number + required: [paused] + additionalProperties: false + anchorPartId: + description: >- + The Part ID that this timer is counting towards (the timing anchor) + type: string + required: [index, label, configured] + additionalProperties: false + examples: + - $ref: './tTimerStatus-example.yaml' diff --git a/packages/live-status-gateway-api/eslint.config.mjs b/packages/live-status-gateway-api/eslint.config.mjs deleted file mode 100644 index b9e5a88fd88..00000000000 --- a/packages/live-status-gateway-api/eslint.config.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' - -export default generateEslintConfig({}) diff --git a/packages/live-status-gateway-api/jest.config.js b/packages/live-status-gateway-api/jest.config.js index 2fe89196eea..84040cf3898 100644 --- a/packages/live-status-gateway-api/jest.config.js +++ b/packages/live-status-gateway-api/jest.config.js @@ -6,6 +6,11 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.json', + diagnostics: { + ignoreCodes: [ + 151002, // hybrid module kind (Node16/18/Next) + ], + }, }, ], }, diff --git a/packages/live-status-gateway-api/package.json b/packages/live-status-gateway-api/package.json index b5b08e44dc2..d77ed143443 100644 --- a/packages/live-status-gateway-api/package.json +++ b/packages/live-status-gateway-api/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/live-status-gateway-api", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -15,9 +15,8 @@ }, "homepage": "https://github.com/nrkno/sofie-core/blob/master/packages/live-status-gateway-api#readme", "scripts": { + "lint": "run -T lint live-status-gateway-api", "build:prepare": "run generate-schema-types", - "lint:raw": "run -T eslint", - "lint": "run lint:raw .", "unit": "run -T jest", "test": "run lint && run unit", "watch": "run -T jest --watch", @@ -42,25 +41,17 @@ "/LICENSE" ], "dependencies": { - "tslib": "^2.6.2" + "tslib": "^2.8.1" }, "devDependencies": { - "@apidevtools/json-schema-ref-parser": "^14.2.1", - "@asyncapi/generator": "^2.6.0", - "@asyncapi/html-template": "^3.2.0", - "@asyncapi/modelina": "^4.0.4", + "@apidevtools/json-schema-ref-parser": "^15.2.2", + "@asyncapi/generator": "^2.11.0", + "@asyncapi/html-template": "^3.5.4", + "@asyncapi/modelina": "^5.10.1", "@asyncapi/nodejs-ws-template": "^0.10.0", - "@asyncapi/parser": "^3.4.0", - "yaml": "^2.8.1" + "@asyncapi/parser": "^3.6.0", + "yaml": "^2.8.2" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "lint-staged": { - "*.{js,css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx}": [ - "yarn lint:raw" - ] - }, "packageManager": "yarn@4.12.0" } diff --git a/packages/live-status-gateway-api/src/generated/asyncapi.yaml b/packages/live-status-gateway-api/src/generated/asyncapi.yaml index b747e97a84c..133cc509fb9 100644 --- a/packages/live-status-gateway-api/src/generated/asyncapi.yaml +++ b/packages/live-status-gateway-api/src/generated/asyncapi.yaml @@ -401,7 +401,7 @@ channels: pieces: description: All pieces in this part type: array - items: &a30 + items: &a32 type: object title: PieceStatus properties: @@ -424,6 +424,29 @@ channels: type: string publicData: description: Optional arbitrary data + abSessions: + description: AB playback session assignments for this Piece + type: array + items: + type: object + title: AbSessionAssignment + properties: + poolName: + description: The name of the AB Pool this session is for + type: string + sessionName: + description: Name of the session + type: string + playerId: + description: The assigned player ID + oneOf: + - type: string + - type: number + required: + - poolName + - sessionName + - playerId + additionalProperties: false required: - id - name @@ -440,6 +463,10 @@ channels: - camera publicData: switcherSource: 1 + abSessions: + - poolName: VTR + sessionName: clip_intro + playerId: 1 publicData: description: Optional arbitrary data required: @@ -510,7 +537,7 @@ channels: - type: object title: CurrentSegment allOf: - - &a32 + - &a34 title: SegmentBase type: object properties: @@ -529,7 +556,7 @@ channels: title: CurrentSegmentTiming description: Timing information about the current segment allOf: - - &a33 + - &a35 type: object title: SegmentTiming properties: @@ -617,6 +644,9 @@ channels: - type: "null" publicData: description: Optional arbitrary data + playoutState: + description: Blueprint-defined playout state, used to expose arbitrary + information about playout timing: type: object title: ActivePlaylistTiming @@ -709,6 +739,172 @@ channels: running: true start: *a23 end: *a23 + tTimers: + description: T-timers for the playlist. Always contains 3 elements (one for each + timer slot). + type: array + items: + type: object + title: TTimerStatus + description: Status of a single T-timer in the playlist + properties: + index: + type: integer + title: TTimerIndex + description: Timer index (1-3). The playlist always has 3 T-timer slots. + enum: + - 1 + - 2 + - 3 + label: + description: User-defined label for the timer + type: string + configured: + description: Whether the timer has been configured (mode is not null) + type: boolean + mode: + description: Timer mode and timing state. Null if not configured. + oneOf: + - type: "null" + - title: TTimerMode + description: The mode/state of a T-timer + oneOf: + - type: object + title: TTimerModeCountdown + description: Countdown timer mode - counts down from a duration + properties: + type: + type: string + const: countdown + paused: + description: Whether the timer is currently paused + type: boolean + zeroTime: + description: Unix timestamp (ms) when the timer reaches/reached zero. Present + when paused is false. The client + calculates remaining time as zeroTime - + Date.now(). + type: number + pauseTime: + description: Unix timestamp (ms) when the timer should automatically pause. + Typically set to when the current part + ends and overrun begins. When present and + current time >= pauseTime, the timer + should display as paused at (zeroTime - + pauseTime). + type: number + remainingMs: + description: Frozen remaining duration in milliseconds. Present when paused is + true. + type: number + durationMs: + description: Total countdown duration in milliseconds (the original configured + duration) + type: number + stopAtZero: + description: Whether timer stops at zero or continues into negative values + type: boolean + required: + - type + - paused + - durationMs + - stopAtZero + additionalProperties: false + examples: + - &a29 + type: countdown + paused: false + zeroTime: 1706371920000 + durationMs: 120000 + stopAtZero: true + - type: object + title: TTimerModeFreeRun + description: Free-running timer mode - counts up from start time + properties: + type: + type: string + const: freeRun + paused: + description: Whether the timer is currently paused + type: boolean + zeroTime: + description: Unix timestamp (ms) when the timer was at zero (i.e. when it was + started). Present when paused is false. + The client calculates elapsed time as + Date.now() - zeroTime. + type: number + pauseTime: + description: Unix timestamp (ms) when the timer should automatically pause. + Typically set to when the current part + ends and overrun begins. When present and + current time >= pauseTime, the timer + should display as paused at (pauseTime - + zeroTime). + type: number + elapsedMs: + description: Frozen elapsed time in milliseconds. Present when paused is true. + type: number + required: + - type + - paused + additionalProperties: false + examples: + - &a30 + type: freeRun + paused: false + zeroTime: 1706371800000 + projected: + description: Projected timing for when we expect to reach an anchor part. Used + to calculate over/under diff + oneOf: + - type: "null" + - type: object + title: TTimerProjected + description: Projected timing state for a T-timer + properties: + paused: + description: Whether the projected time is frozen + type: boolean + zeroTime: + description: Unix timestamp in milliseconds of projected arrival at the anchor + part + type: number + pauseTime: + description: Unix timestamp (ms) when the projected timer should automatically + pause. When present and current time >= + pauseTime, the projected duration should be + calculated from pauseTime. + type: number + durationMs: + description: Frozen remaining duration projection in milliseconds + type: number + required: + - paused + additionalProperties: false + anchorPartId: + description: The Part ID that this timer is counting towards (the timing anchor) + type: string + required: + - index + - label + - configured + additionalProperties: false + examples: + - index: 1 + label: Segment Timer + configured: true + mode: + type: countdown + paused: false + zeroTime: 1706371920000 + durationMs: 120000 + stopAtZero: true + projected: + paused: false + zeroTime: 1706371920000 + anchorPartId: part_break_1 + minItems: 3 + maxItems: 3 required: - event - id @@ -719,9 +915,10 @@ channels: - currentSegment - nextPart - timing + - tTimers additionalProperties: false examples: - - &a29 + - &a31 event: activePlaylist id: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ externalId: 1ZIYVYL1aEkNEJbeGsmRXr5s8wtkyxfPRjNSTxZfcoEI @@ -735,8 +932,21 @@ channels: category: Evening News timing: *a27 quickLoop: *a28 + tTimers: + - index: 1 + label: On Air Timer + configured: true + mode: *a29 + - index: 2 + label: "" + configured: false + mode: null + - index: 3 + label: Studio Clock + configured: true + mode: *a30 examples: - - payload: *a29 + - payload: *a31 activePieces: description: Topic for active pieces updates subscribe: @@ -761,20 +971,20 @@ channels: activePieces: description: Pieces that are currently active (on air) type: array - items: *a30 + items: *a32 required: - event - rundownPlaylistId - activePieces additionalProperties: false examples: - - &a31 + - &a33 event: activePieces rundownPlaylistId: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ activePieces: - *a13 examples: - - payload: *a31 + - payload: *a33 segments: description: Topic for Segment updates subscribe: @@ -803,7 +1013,7 @@ channels: type: object title: Segment allOf: - - *a32 + - *a34 - type: object title: Segment properties: @@ -817,7 +1027,7 @@ channels: name: description: Name of the segment type: string - timing: *a33 + timing: *a35 publicData: description: Optional arbitrary data required: @@ -830,7 +1040,7 @@ channels: - name - timing examples: - - &a34 + - &a36 identifier: Segment 0 identifier rundownId: y9HauyWkcxQS3XaAOsW40BRLLsI_ name: Segment 0 @@ -846,13 +1056,13 @@ channels: - rundownPlaylistId - segments examples: - - &a35 + - &a37 event: segments rundownPlaylistId: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ segments: - - *a34 + - *a36 examples: - - payload: *a35 + - payload: *a37 adLibs: description: Topic for AdLibs updates subscribe: @@ -882,7 +1092,7 @@ channels: items: title: AdLibStatus allOf: - - &a40 + - &a42 title: AdLibBase type: object properties: @@ -917,7 +1127,7 @@ channels: - label additionalProperties: false examples: - - &a36 + - &a38 name: pvw label: Preview tags: @@ -937,15 +1147,15 @@ channels: - sourceLayer - actionType examples: - - &a41 + - &a43 id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: Music video clip sourceLayer: Video Clip - actionType: &a37 - - *a36 - tags: &a38 + actionType: &a39 + - *a38 + tags: &a40 - music_video - publicData: &a39 + publicData: &a41 fileName: MV000123.mxf optionsSchema: '{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Play Video @@ -977,15 +1187,15 @@ channels: - segmentId - partId examples: - - &a42 + - &a44 segmentId: HsD8_QwE1ZmR5vN3XcK_Ab7y partId: JkL3_OpR6WxT1bF8Vq2_Zy9u id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: Music video clip sourceLayer: Video Clip - actionType: *a37 - tags: *a38 - publicData: *a39 + actionType: *a39 + tags: *a40 + publicData: *a41 optionsSchema: '{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Play Video Clip","type":"object","properties":{"type":"adlib_action_video_clip","label":{"type":"string"},"clipId":{"type":"string"},"vo":{"type":"boolean"},"target":{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Object @@ -1009,9 +1219,9 @@ channels: items: title: GlobalAdLibStatus allOf: - - *a40 + - *a42 examples: - - *a41 + - *a43 required: - event - rundownPlaylistId @@ -1019,15 +1229,15 @@ channels: - globalAdLibs additionalProperties: false examples: - - &a43 + - &a45 event: adLibs rundownPlaylistId: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ adLibs: - - *a42 + - *a44 globalAdLibs: - - *a41 + - *a43 examples: - - payload: *a43 + - payload: *a45 packages: description: Packages topic for websocket subscriptions. Packages are assets that need to be prepared by Sofie Package Manager or third-party systems @@ -1135,7 +1345,7 @@ channels: - pieceOrAdLibId additionalProperties: false examples: - - &a44 + - &a46 packageName: MV000123.mxf status: ok rundownId: y9HauyWkcxQS3XaAOsW40BRLLsI_ @@ -1153,7 +1363,7 @@ channels: - event: packages rundownPlaylistId: y9HauyWkcxQS3XaAOsW40BRLLsI_ packages: - - *a44 + - *a46 buckets: description: Buckets schema for websocket subscriptions subscribe: @@ -1189,7 +1399,7 @@ channels: items: title: BucketAdLibStatus allOf: - - *a40 + - *a42 - type: object title: BucketAdLibStatus properties: @@ -1200,14 +1410,14 @@ channels: required: - externalId examples: - - &a45 + - &a47 externalId: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: Music video clip sourceLayer: Video Clip - actionType: *a37 - tags: *a38 - publicData: *a39 + actionType: *a39 + tags: *a40 + publicData: *a41 optionsSchema: '{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Play Video Clip","type":"object","properties":{"type":"adlib_action_video_clip","label":{"type":"string"},"clipId":{"type":"string"},"vo":{"type":"boolean"},"target":{"$schema":"https://json-schema.org/draft/2020-12/schema","title":"Object @@ -1231,22 +1441,22 @@ channels: - adLibs additionalProperties: false examples: - - &a46 + - &a48 id: C6K_yIMuGFUk8X_L9A9_jRT6aq4_ name: My Bucket adLibs: - - *a45 + - *a47 required: - event - buckets additionalProperties: false examples: - - &a47 + - &a49 event: buckets buckets: - - *a46 + - *a48 examples: - - payload: *a47 + - payload: *a49 notifications: description: Notifications topic for websocket subscriptions. subscribe: @@ -1310,7 +1520,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: &a48 + enum: &a50 - rundown - playlist - partInstance @@ -1322,7 +1532,7 @@ channels: type: string additionalProperties: false examples: - - &a49 + - &a51 type: rundown studioId: studio01 rundownId: rd123 @@ -1338,14 +1548,14 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 studioId: type: string playlistId: type: string additionalProperties: false examples: - - &a50 + - &a52 type: playlist studioId: studio01 playlistId: pl456 @@ -1362,7 +1572,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 studioId: type: string rundownId: @@ -1371,7 +1581,7 @@ channels: type: string additionalProperties: false examples: - - &a51 + - &a53 type: partInstance studioId: studio01 rundownId: rd123 @@ -1390,7 +1600,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 studioId: type: string rundownId: @@ -1401,7 +1611,7 @@ channels: type: string additionalProperties: false examples: - - &a52 + - &a54 type: pieceInstance studioId: studio01 rundownId: rd123 @@ -1417,17 +1627,17 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 additionalProperties: false examples: - - &a53 + - &a55 type: unknown examples: - - *a49 - - *a50 - *a51 - *a52 - *a53 + - *a54 + - *a55 created: type: integer format: int64 @@ -1438,11 +1648,11 @@ channels: description: Unix timestamp of last modification additionalProperties: false examples: - - &a54 + - &a56 _id: notif123 severity: error message: disk.space.low - relatedTo: *a52 + relatedTo: *a54 created: 1694784932 modified: 1694784950 required: @@ -1450,9 +1660,9 @@ channels: - activeNotifications additionalProperties: false examples: - - &a55 + - &a57 event: notifications activeNotifications: - - *a54 + - *a56 examples: - - payload: *a55 + - payload: *a57 diff --git a/packages/live-status-gateway-api/src/generated/schema.ts b/packages/live-status-gateway-api/src/generated/schema.ts index b8f0970662a..60f039b9935 100644 --- a/packages/live-status-gateway-api/src/generated/schema.ts +++ b/packages/live-status-gateway-api/src/generated/schema.ts @@ -178,6 +178,10 @@ interface ActivePlaylistEvent { * Optional arbitrary data */ publicData?: any + /** + * Blueprint-defined playout state, used to expose arbitrary information about playout + */ + playoutState?: any /** * Timing information about the active playlist */ @@ -186,6 +190,10 @@ interface ActivePlaylistEvent { * Information about the current quickLoop, if any */ quickLoop?: ActivePlaylistQuickLoop + /** + * T-timers for the playlist. Always contains 3 elements (one for each timer slot). + */ + tTimers: TTimerStatus[] } interface CurrentPartStatus { @@ -245,6 +253,25 @@ interface PieceStatus { * Optional arbitrary data */ publicData?: any + /** + * AB playback session assignments for this Piece + */ + abSessions?: AbSessionAssignment[] +} + +interface AbSessionAssignment { + /** + * The name of the AB Pool this session is for + */ + poolName: string + /** + * Name of the session + */ + sessionName: string + /** + * The assigned player ID + */ + playerId: string | number } /** @@ -454,6 +481,121 @@ enum QuickLoopMarkerType { PART = 'part', } +/** + * Status of a single T-timer in the playlist + */ +interface TTimerStatus { + /** + * Timer index (1-3). The playlist always has 3 T-timer slots. + */ + index: TTimerIndex + /** + * User-defined label for the timer + */ + label: string + /** + * Whether the timer has been configured (mode is not null) + */ + configured: boolean + /** + * Timer mode and timing state. Null if not configured. + */ + mode?: TTimerModeCountdown | TTimerModeFreeRun | null + /** + * Projected timing for when we expect to reach an anchor part. Used to calculate over/under diff + */ + projected?: TTimerProjected | null + /** + * The Part ID that this timer is counting towards (the timing anchor) + */ + anchorPartId?: string +} + +/** + * Timer index (1-3). The playlist always has 3 T-timer slots. + */ +enum TTimerIndex { + NUMBER_1 = 1, + NUMBER_2 = 2, + NUMBER_3 = 3, +} + +/** + * Countdown timer mode - counts down from a duration + */ +interface TTimerModeCountdown { + type: 'countdown' + /** + * Whether the timer is currently paused + */ + paused: boolean + /** + * Unix timestamp (ms) when the timer reaches/reached zero. Present when paused is false. The client calculates remaining time as zeroTime - Date.now(). + */ + zeroTime?: number + /** + * Unix timestamp (ms) when the timer should automatically pause. Typically set to when the current part ends and overrun begins. When present and current time >= pauseTime, the timer should display as paused at (zeroTime - pauseTime). + */ + pauseTime?: number + /** + * Frozen remaining duration in milliseconds. Present when paused is true. + */ + remainingMs?: number + /** + * Total countdown duration in milliseconds (the original configured duration) + */ + durationMs: number + /** + * Whether timer stops at zero or continues into negative values + */ + stopAtZero: boolean +} + +/** + * Free-running timer mode - counts up from start time + */ +interface TTimerModeFreeRun { + type: 'freeRun' + /** + * Whether the timer is currently paused + */ + paused: boolean + /** + * Unix timestamp (ms) when the timer was at zero (i.e. when it was started). Present when paused is false. The client calculates elapsed time as Date.now() - zeroTime. + */ + zeroTime?: number + /** + * Unix timestamp (ms) when the timer should automatically pause. Typically set to when the current part ends and overrun begins. When present and current time >= pauseTime, the timer should display as paused at (pauseTime - zeroTime). + */ + pauseTime?: number + /** + * Frozen elapsed time in milliseconds. Present when paused is true. + */ + elapsedMs?: number +} + +/** + * Projected timing state for a T-timer + */ +interface TTimerProjected { + /** + * Whether the projected time is frozen + */ + paused: boolean + /** + * Unix timestamp in milliseconds of projected arrival at the anchor part + */ + zeroTime?: number + /** + * Unix timestamp (ms) when the projected timer should automatically pause. When present and current time >= pauseTime, the projected duration should be calculated from pauseTime. + */ + pauseTime?: number + /** + * Frozen remaining duration projection in milliseconds + */ + durationMs?: number +} + interface ActivePiecesEvent { event: 'activePieces' /** @@ -912,6 +1054,7 @@ export { ActivePlaylistEvent, CurrentPartStatus, PieceStatus, + AbSessionAssignment, CurrentPartTiming, CurrentSegment, CurrentSegmentTiming, @@ -924,6 +1067,11 @@ export { ActivePlaylistQuickLoop, QuickLoopMarker, QuickLoopMarkerType, + TTimerStatus, + TTimerIndex, + TTimerModeCountdown, + TTimerModeFreeRun, + TTimerProjected, ActivePiecesEvent, SegmentsEvent, Segment, diff --git a/packages/live-status-gateway/eslint.config.mjs b/packages/live-status-gateway/eslint.config.mjs deleted file mode 100644 index 379a687a6b0..00000000000 --- a/packages/live-status-gateway/eslint.config.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' -import pluginYaml from 'eslint-plugin-yml' - -const extendedRules = await generateEslintConfig({}) -extendedRules.push(...pluginYaml.configs['flat/recommended'], { - files: ['**/*.yaml'], - - rules: { - 'yml/quotes': ['error', { prefer: 'single' }], - 'yml/spaced-comment': ['error'], - 'spaced-comment': ['off'], - }, -}) - -export default extendedRules diff --git a/packages/live-status-gateway/jest.config.js b/packages/live-status-gateway/jest.config.js index 1b0e384f84f..897900a3ef0 100644 --- a/packages/live-status-gateway/jest.config.js +++ b/packages/live-status-gateway/jest.config.js @@ -6,6 +6,11 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.json', + diagnostics: { + ignoreCodes: [ + 151002, // hybrid module kind (Node16/18/Next) + ], + }, }, ], }, diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index f20b973b745..6b7b6ec140c 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -15,8 +15,7 @@ "homepage": "https://github.com/Sofie-Automation/sofie-core/blob/main/packages/live-status-gateway#readme", "contributors": [], "scripts": { - "lint:raw": "run -T eslint --ignore-pattern server", - "lint": "run lint:raw .", + "lint": "run -T lint live-status-gateway", "unit": "run -T jest", "test": "run lint && run unit", "watch": "run -T jest --watch", @@ -47,28 +46,20 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "1.53.0-in-development", - "@sofie-automation/corelib": "1.53.0-in-development", - "@sofie-automation/live-status-gateway-api": "1.53.0-in-development", - "@sofie-automation/server-core-integration": "1.53.0-in-development", - "@sofie-automation/shared-lib": "1.53.0-in-development", - "debug": "^4.4.0", + "@sofie-automation/blueprints-integration": "26.3.0-2", + "@sofie-automation/corelib": "26.3.0-2", + "@sofie-automation/live-status-gateway-api": "26.3.0-2", + "@sofie-automation/server-core-integration": "26.3.0-2", + "@sofie-automation/shared-lib": "26.3.0-2", + "debug": "^4.4.3", "fast-clone": "^1.5.13", - "influx": "^5.9.7", + "influx": "^5.12.0", "tslib": "^2.8.1", "underscore": "^1.13.7", - "winston": "^3.17.0", - "ws": "^8.18.0" + "winston": "^3.19.0", + "ws": "^8.19.0" }, "devDependencies": { - "type-fest": "^4.33.0" - }, - "lint-staged": { - "*.{css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx,js,jsx,yaml}": [ - "yarn lint:raw" - ] + "type-fest": "^4.41.0" } } diff --git a/packages/live-status-gateway/src/coreHandler.ts b/packages/live-status-gateway/src/coreHandler.ts index 7899409279e..a78966e33f7 100644 --- a/packages/live-status-gateway/src/coreHandler.ts +++ b/packages/live-status-gateway/src/coreHandler.ts @@ -346,9 +346,8 @@ export class CoreHandler { private _getVersions() { const versions: { [packageName: string]: string } = {} - if (process.env.npm_package_version) { - versions['_process'] = process.env.npm_package_version - } + const pkg = require('../package.json') + if (pkg?.version) versions['_process'] = pkg.version return versions } diff --git a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts index 702ee867c6d..96bb540d775 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -19,6 +19,7 @@ import { ActivePlaylistEvent, ActivePlaylistTimingMode, SegmentCountdownType, + TTimerIndex, } from '@sofie-automation/live-status-gateway-api' function makeEmptyTestPartInstances(): SelectedPartInstances { @@ -63,6 +64,11 @@ describe('ActivePlaylistTopic', () => { timingMode: ActivePlaylistTimingMode.NONE, }, quickLoop: undefined, + tTimers: [ + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null, projected: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, projected: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, projected: null }, + ], } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -164,6 +170,11 @@ describe('ActivePlaylistTopic', () => { timingMode: ActivePlaylistTimingMode.NONE, }, quickLoop: undefined, + tTimers: [ + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null, projected: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, projected: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, projected: null }, + ], } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -270,6 +281,11 @@ describe('ActivePlaylistTopic', () => { timingMode: ActivePlaylistTimingMode.NONE, }, quickLoop: undefined, + tTimers: [ + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null, projected: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, projected: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, projected: null }, + ], } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -278,4 +294,95 @@ describe('ActivePlaylistTopic', () => { JSON.parse(JSON.stringify(expectedStatus)) ) }) + + it('transforms configured T-timers correctly', async () => { + const handlers = makeMockHandlers() + const topic = new ActivePlaylistTopic(makeMockLogger(), handlers) + const mockSubscriber = makeMockSubscriber() + + const playlist = makeTestPlaylist() + playlist.activationId = protectString('somethingRandom') + // Configure timers with different modes + playlist.tTimers = [ + { + index: 1, + label: 'Countdown Timer', + mode: { + type: 'countdown', + duration: 60000, + stopAtZero: true, + }, + state: { paused: false, zeroTime: 1600000060000 }, + projectedState: { paused: false, zeroTime: 1600000060000 }, + anchorPartId: protectString('PART_BREAK'), + }, + { + index: 2, + label: 'Paused FreeRun', + mode: { + type: 'freeRun', + }, + state: { paused: true, duration: 10000 }, + projectedState: { paused: true, duration: 5000 }, + }, + { index: 3, label: '', mode: null, state: null }, + ] + handlers.playlistHandler.notify(playlist) + + const testShowStyleBase = makeTestShowStyleBase() + handlers.showStyleBaseHandler.notify(testShowStyleBase as ShowStyleBaseExt) + + const testPartInstancesMap = makeEmptyTestPartInstances() + handlers.partInstancesHandler.notify(testPartInstancesMap) + + topic.addSubscriber(mockSubscriber) + + // eslint-disable-next-line @typescript-eslint/unbound-method + expect(mockSubscriber.send).toHaveBeenCalledTimes(1) + const receivedStatus = JSON.parse(mockSubscriber.send.mock.calls[0][0] as string) as ActivePlaylistEvent + + // Verify running countdown timer transformation + expect(receivedStatus.tTimers[0]).toEqual({ + index: TTimerIndex.NUMBER_1, + label: 'Countdown Timer', + configured: true, + mode: { + type: 'countdown', + paused: false, + zeroTime: 1600000060000, + durationMs: 60000, + stopAtZero: true, + }, + projected: { + paused: false, + zeroTime: 1600000060000, + }, + anchorPartId: 'PART_BREAK', + }) + + // Verify paused freeRun timer transformation + expect(receivedStatus.tTimers[1]).toEqual({ + index: TTimerIndex.NUMBER_2, + label: 'Paused FreeRun', + configured: true, + mode: { + type: 'freeRun', + paused: true, + elapsedMs: 10000, + }, + projected: { + paused: true, + durationMs: 5000, + }, + }) + + // Verify unconfigured timer + expect(receivedStatus.tTimers[2]).toEqual({ + index: TTimerIndex.NUMBER_3, + label: '', + configured: false, + mode: null, + projected: null, + }) + }) }) diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index 576b1cb7436..7afa37e1104 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -34,6 +34,11 @@ export function makeTestPlaylist(id?: string): DBRundownPlaylist { studioId: protectString('STUDIO_1'), timing: { type: PlaylistTimingType.None }, publicData: { a: 'b' }, + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], } } diff --git a/packages/live-status-gateway/src/topics/activePiecesTopic.ts b/packages/live-status-gateway/src/topics/activePiecesTopic.ts index cbb4f909cbb..c32890427d9 100644 --- a/packages/live-status-gateway/src/topics/activePiecesTopic.ts +++ b/packages/live-status-gateway/src/topics/activePiecesTopic.ts @@ -14,7 +14,7 @@ import { ActivePiecesEvent } from '@sofie-automation/live-status-gateway-api' const THROTTLE_PERIOD_MS = 100 -const PLAYLIST_KEYS = ['_id', 'activationId'] as const +const PLAYLIST_KEYS = ['_id', 'activationId', 'assignedAbSessions', 'trackedAbSessions'] as const type Playlist = PickKeys const PIECE_INSTANCES_KEYS = ['active'] as const @@ -24,6 +24,7 @@ export class ActivePiecesTopic extends WebSocketTopicBase implements WebSocketTo private _activePlaylistId: RundownPlaylistId | undefined private _activePieceInstances: PieceInstanceMin[] | undefined private _showStyleBaseExt: ShowStyleBaseExt | undefined + private _playlist: Playlist | undefined constructor(logger: Logger, handlers: CollectionHandlers) { super(ActivePiecesTopic.name, logger, THROTTLE_PERIOD_MS) @@ -39,7 +40,9 @@ export class ActivePiecesTopic extends WebSocketTopicBase implements WebSocketTo event: 'activePieces', rundownPlaylistId: unprotectString(this._activePlaylistId), activePieces: - this._activePieceInstances?.map((piece) => toPieceStatus(piece, this._showStyleBaseExt)) ?? [], + this._activePieceInstances?.map((piece) => + toPieceStatus(piece, this._showStyleBaseExt, this._playlist) + ) ?? [], }) : literal({ event: 'activePieces', @@ -63,6 +66,7 @@ export class ActivePiecesTopic extends WebSocketTopicBase implements WebSocketTo ) const previousActivePlaylistId = this._activePlaylistId this._activePlaylistId = unprotectString(rundownPlaylist?.activationId) ? rundownPlaylist?._id : undefined + this._playlist = rundownPlaylist if (previousActivePlaylistId !== this._activePlaylistId) { this.throttledSendStatusToAll() diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index f1f29c940d4..506c2d71d5f 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -5,6 +5,7 @@ import { DBRundownPlaylist, QuickLoopMarker, QuickLoopMarkerType, + RundownTTimer, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { assertNever, literal } from '@sofie-automation/shared-lib/dist/lib/lib' @@ -30,11 +31,16 @@ import { ActivePlaylistQuickLoop, QuickLoopMarker as QuickLoopMarkerStatus, QuickLoopMarkerType as QuickLoopMarkerStatusType, + TTimerStatus, + TTimerProjected, + TTimerModeCountdown, + TTimerModeFreeRun, + TTimerIndex, } from '@sofie-automation/live-status-gateway-api' import { CollectionHandlers } from '../liveStatusServer.js' import areElementsShallowEqual from '@sofie-automation/shared-lib/dist/lib/isShallowEqual' -import { PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' +import { Complete, PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' const THROTTLE_PERIOD_MS = 100 @@ -45,11 +51,13 @@ const PLAYLIST_KEYS = [ 'name', 'rundownIdsInOrder', 'publicData', + 'publicPlayoutPersistentState', 'currentPartInfo', 'nextPartInfo', 'timing', 'startedPlayback', 'quickLoop', + 'tTimers', ] as const type Playlist = PickKeys @@ -101,7 +109,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket (currentPart && this._partsBySegmentId[unprotectString(currentPart.segmentId)]) ?? [] const message = this._activePlaylist - ? literal({ + ? literal>({ event: 'activePlaylist', id: unprotectString(this._activePlaylist._id), externalId: this._activePlaylist.externalId, @@ -157,6 +165,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket : null, quickLoop: this.transformQuickLoopStatus(), publicData: this._activePlaylist.publicData, + playoutState: this._activePlaylist.publicPlayoutPersistentState, timing: { timingMode: translatePlaylistTimingType(this._activePlaylist.timing.type), startedPlayback: this._activePlaylist.startedPlayback, @@ -170,8 +179,9 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket ? this._activePlaylist.timing.expectedEnd : undefined, }, + tTimers: this.transformTTimers(this._activePlaylist.tTimers), }) - : literal({ + : literal>({ event: 'activePlaylist', id: null, externalId: null, @@ -182,9 +192,11 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket nextPart: null, quickLoop: undefined, publicData: undefined, + playoutState: undefined, timing: { timingMode: ActivePlaylistTimingMode.NONE, }, + tTimers: this.transformTTimers(undefined), }) this.sendMessage(subscribers, message) @@ -204,6 +216,117 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket } } + /** + * Transform T-timers from database format to API status format + */ + private transformTTimers( + tTimers: [RundownTTimer, RundownTTimer, RundownTTimer] | undefined + ): [TTimerStatus, TTimerStatus, TTimerStatus] { + if (!tTimers) { + // Return 3 unconfigured timers when no playlist is active + return [ + { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null, projected: null }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null, projected: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null, projected: null }, + ] + } + + return [this.transformTTimer(tTimers[0]), this.transformTTimer(tTimers[1]), this.transformTTimer(tTimers[2])] + } + + /** + * Transform a single T-timer from database format to API status format + */ + private transformTTimer(timer: RundownTTimer): TTimerStatus { + const index = + timer.index === 1 ? TTimerIndex.NUMBER_1 : timer.index === 2 ? TTimerIndex.NUMBER_2 : TTimerIndex.NUMBER_3 + + const projected = this.transformTimerProjected(timer.projectedState) + const anchorPartId = timer.anchorPartId ? unprotectString(timer.anchorPartId) : undefined + + if (!timer.mode || !timer.state) { + return { + index, + label: timer.label, + configured: false, + mode: null, + projected, + anchorPartId, + } + } + + if (timer.mode.type === 'countdown') { + const mode: TTimerModeCountdown = timer.state.paused + ? { + type: 'countdown', + paused: true, + remainingMs: timer.state.duration, + pauseTime: undefined, + durationMs: timer.mode.duration, + stopAtZero: timer.mode.stopAtZero, + } + : { + type: 'countdown', + paused: false, + zeroTime: timer.state.zeroTime, + pauseTime: timer.state.pauseTime ?? undefined, + durationMs: timer.mode.duration, + stopAtZero: timer.mode.stopAtZero, + } + return { + index, + label: timer.label, + configured: true, + mode, + projected, + anchorPartId, + } + } else { + const mode: TTimerModeFreeRun = timer.state.paused + ? { + type: 'freeRun', + paused: true, + elapsedMs: timer.state.duration, + pauseTime: timer.state.pauseTime ?? undefined, + } + : { + type: 'freeRun', + paused: false, + zeroTime: timer.state.zeroTime, + pauseTime: timer.state.pauseTime ?? undefined, + } + return { + index, + label: timer.label, + configured: true, + mode, + projected, + anchorPartId, + } + } + } + + /** + * Transform a TimerState from the data model to a TTimerProjected for the API + */ + private transformTimerProjected(projectedState: RundownTTimer['projectedState']): TTimerProjected | null { + if (!projectedState) return null + + if (projectedState.paused) { + return { + paused: true, + durationMs: projectedState.duration, + pauseTime: projectedState.pauseTime ?? undefined, + } + } else { + return { + paused: false, + zeroTime: projectedState.zeroTime, + pauseTime: projectedState.pauseTime ?? undefined, + } + } + } + private transformQuickLoopMarkerStatus(marker: QuickLoopMarker | undefined): QuickLoopMarkerStatus | undefined { if (!marker) return undefined diff --git a/packages/live-status-gateway/src/topics/helpers/__tests__/segmentTiming.test.ts b/packages/live-status-gateway/src/topics/helpers/__tests__/segmentTiming.test.ts new file mode 100644 index 00000000000..7add0735c26 --- /dev/null +++ b/packages/live-status-gateway/src/topics/helpers/__tests__/segmentTiming.test.ts @@ -0,0 +1,53 @@ +import { calculateSegmentTiming } from '../segmentTiming.js' +import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' +import { protectString } from '@sofie-automation/corelib/dist/protectedString' + +function makeTestPart(id: string, expectedDuration: number): Partial { + return { + _id: protectString(`part_${id}`), + segmentId: protectString('segment_1'), + rundownId: protectString('rundown_1'), + untimed: false, + expectedDurationWithTransition: expectedDuration, + } +} + +function makeTestPartInstance(id: string, partId: string, expectedDuration: number): Partial { + return { + _id: protectString(`partInstance_${id}`), + part: makeTestPart(partId, expectedDuration) as DBPart, + rundownId: protectString('rundown_1'), + segmentId: protectString('segment_1'), + playlistActivationId: protectString('activation_1'), + } +} + +describe('segmentTiming - calculateSegmentTiming', () => { + it('should use partInstance duration when available instead of original part duration', () => { + const parts = [makeTestPart('1', 5000), makeTestPart('2', 3000)] + + // Create partInstances with modified durations + const partInstances = [makeTestPartInstance('1', '1', 6000), makeTestPartInstance('2', '2', 4000)] + + const result = calculateSegmentTiming(undefined, partInstances as DBPartInstance[], parts as DBPart[]) + + // Should use the modified durations from partInstances (6000 + 4000), not the original (5000 + 3000) + expect(result.expectedDurationMs).toBe(10000) + }) + + it('should fall back to original part duration when no matching partInstance', () => { + const parts = [makeTestPart('1', 5000), makeTestPart('2', 3000), makeTestPart('3', 2000)] + + // Only provide instances for parts 1 and 2, part 3 has no instance + const partInstances = [ + makeTestPartInstance('1', '1', 6000), // modified from 5000 + makeTestPartInstance('2', '2', 3000), // unchanged + ] + + const result = calculateSegmentTiming(undefined, partInstances as DBPartInstance[], parts as DBPart[]) + + // Should use: 6000 (instance) + 3000 (instance) + 2000 (original, no instance) + expect(result.expectedDurationMs).toBe(11000) + }) +}) diff --git a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts index eebcb218037..b82099ef4e5 100644 --- a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts +++ b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts @@ -1,15 +1,22 @@ import { unprotectString } from '@sofie-automation/server-core-integration' import type { ShowStyleBaseExt } from '../../collections/showStyleBaseHandler.js' import type { PieceInstanceMin } from '../../collections/pieceInstancesHandler.js' -import type { PieceStatus } from '@sofie-automation/live-status-gateway-api' +import type { AbSessionAssignment, PieceStatus } from '@sofie-automation/live-status-gateway-api' import { clone } from '@sofie-automation/corelib/dist/lib' +import type { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import type { PickKeys } from '@sofie-automation/shared-lib/dist/lib/types' + +const _PLAYLIST_AB_SESSION_KEYS = ['assignedAbSessions', 'trackedAbSessions'] as const +type PlaylistAbSessions = PickKeys export function toPieceStatus( pieceInstance: PieceInstanceMin, - showStyleBaseExt: ShowStyleBaseExt | undefined + showStyleBaseExt: ShowStyleBaseExt | undefined, + playlist?: PlaylistAbSessions ): PieceStatus { const sourceLayerName = showStyleBaseExt?.sourceLayerNamesById.get(pieceInstance.piece.sourceLayerId) const outputLayerName = showStyleBaseExt?.outputLayerNamesById.get(pieceInstance.piece.outputLayerId) + return { id: unprotectString(pieceInstance._id), name: pieceInstance.piece.name, @@ -17,5 +24,35 @@ export function toPieceStatus( outputLayer: outputLayerName ?? 'invalid', tags: clone(pieceInstance.piece.tags), publicData: pieceInstance.piece.publicData, + abSessions: getAbSessions(pieceInstance, playlist), + } +} + +function getAbSessions(pieceInstance: PieceInstanceMin, playlist?: PlaylistAbSessions) { + if (!pieceInstance.piece.abSessions || !playlist?.trackedAbSessions || !playlist?.assignedAbSessions) { + return [] } + + const abSessions: AbSessionAssignment[] = [] + + for (const session of pieceInstance.piece.abSessions) { + const trackedSession = playlist.trackedAbSessions.find( + (s) => s.name === `${session.poolName}_${session.sessionName}` + ) + + if (trackedSession) { + const poolAssignments = playlist.assignedAbSessions[session.poolName] + const assignment = poolAssignments?.[trackedSession.id] + + if (assignment) { + abSessions.push({ + poolName: session.poolName, + sessionName: session.sessionName, + playerId: assignment.playerId, + }) + } + } + } + + return abSessions } diff --git a/packages/live-status-gateway/src/topics/helpers/segmentTiming.ts b/packages/live-status-gateway/src/topics/helpers/segmentTiming.ts index b0648e74055..62ed0e583b0 100644 --- a/packages/live-status-gateway/src/topics/helpers/segmentTiming.ts +++ b/packages/live-status-gateway/src/topics/helpers/segmentTiming.ts @@ -4,6 +4,8 @@ import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartIns import { SegmentCountdownType, SegmentTiming } from '@sofie-automation/live-status-gateway-api' import { CountdownType } from '@sofie-automation/blueprints-integration' import { assertNever } from '@sofie-automation/corelib/dist/lib' +import { unprotectString } from '@sofie-automation/server-core-integration' +import * as _ from 'underscore' export interface CurrentSegmentTiming extends SegmentTiming { projectedEndTime: number @@ -16,7 +18,7 @@ export function calculateCurrentSegmentTiming( segmentPartInstances: DBPartInstance[], segmentParts: DBPart[] ): CurrentSegmentTiming { - const segmentTiming = calculateSegmentTiming(segmentTimingInfo, segmentParts) + const segmentTiming = calculateSegmentTiming(segmentTimingInfo, segmentPartInstances, segmentParts) const playedDurations = segmentPartInstances.reduce((sum, partInstance) => { return (partInstance.timings?.duration ?? 0) + sum }, 0) @@ -38,11 +40,18 @@ export function calculateCurrentSegmentTiming( export function calculateSegmentTiming( segmentTimingInfo: SegmentTimingInfo | undefined, + segmentPartInstances: DBPartInstance[], segmentParts: DBPart[] ): SegmentTiming { + // This might be a premature optimization, at least when the number of partInstances is reasonable. + // Should we consider a separate path dependent on the length of the array? + const partInstancesByPartId: Record = _.indexBy(segmentPartInstances, (partInstance) => + unprotectString(partInstance.part._id) + ) return { budgetDurationMs: segmentTimingInfo?.budgetDuration, expectedDurationMs: segmentParts.reduce((sum, part): number => { + part = partInstancesByPartId[unprotectString(part._id)]?.part ?? part return part.expectedDurationWithTransition != null && !part.untimed ? sum + part.expectedDurationWithTransition : sum diff --git a/packages/live-status-gateway/src/topics/segmentsTopic.ts b/packages/live-status-gateway/src/topics/segmentsTopic.ts index e778a519038..d7171d6af1e 100644 --- a/packages/live-status-gateway/src/topics/segmentsTopic.ts +++ b/packages/live-status-gateway/src/topics/segmentsTopic.ts @@ -41,7 +41,7 @@ export class SegmentsTopic extends WebSocketTopicBase implements WebSocketTopic id: segmentId, rundownId: unprotectString(segment.rundownId), name: segment.name, - timing: calculateSegmentTiming(segment.segmentTiming, this._partsBySegment[segmentId] ?? []), + timing: calculateSegmentTiming(segment.segmentTiming, [], this._partsBySegment[segmentId] ?? []), // TODO: this might be inaccurate for the current/next segment, where partInstances might have some changes from adlib actions etc. identifier: segment.identifier, publicData: segment.publicData, } diff --git a/packages/live-status-gateway/tsconfig.build.json b/packages/live-status-gateway/tsconfig.build.json index a4dfc1e45c1..32e9d14c3c7 100644 --- a/packages/live-status-gateway/tsconfig.build.json +++ b/packages/live-status-gateway/tsconfig.build.json @@ -1,6 +1,6 @@ { "extends": "@sofie-automation/code-standard-preset/ts/tsconfig.bin", - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.json"], "exclude": ["node_modules/**", "**/*spec.ts", "**/__tests__/*", "**/__mocks__/*"], "compilerOptions": { "outDir": "./dist", @@ -13,7 +13,9 @@ "types": ["node"], "resolveJsonModule": true, "skipLibCheck": true, - "esModuleInterop": true + "esModuleInterop": true, + "declaration": true, + "composite": true }, "references": [ { "path": "../shared-lib/tsconfig.build.json" }, diff --git a/packages/meteor-lib/eslint.config.mjs b/packages/meteor-lib/eslint.config.mjs deleted file mode 100644 index b9e5a88fd88..00000000000 --- a/packages/meteor-lib/eslint.config.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' - -export default generateEslintConfig({}) diff --git a/packages/meteor-lib/jest.config.js b/packages/meteor-lib/jest.config.js index 76ff2b14f1e..04b8ea8dd1c 100644 --- a/packages/meteor-lib/jest.config.js +++ b/packages/meteor-lib/jest.config.js @@ -6,6 +6,11 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.json', + diagnostics: { + ignoreCodes: [ + 151002, // hybrid module kind (Node16/18/Next) + ], + }, }, ], }, diff --git a/packages/meteor-lib/package.json b/packages/meteor-lib/package.json index 5ab52f726cc..45f975e4a33 100644 --- a/packages/meteor-lib/package.json +++ b/packages/meteor-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/meteor-lib", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "private": true, "description": "Temporary internal library for some types shared by meteor and webui", "main": "dist/index.js", @@ -16,8 +16,7 @@ }, "homepage": "https://github.com/nrkno/sofie-core/blob/main/packages/corelib#readme", "scripts": { - "lint:raw": "run -T eslint", - "lint": "run lint:raw .", + "lint": "run -T lint meteor-lib", "unit": "run -T jest", "test": "run lint && run unit", "watch": "run -T jest --watch --coverage=false", @@ -38,17 +37,17 @@ ], "dependencies": { "@mos-connection/helper": "^5.0.0-alpha.0", - "@sofie-automation/blueprints-integration": "1.53.0-in-development", - "@sofie-automation/corelib": "1.53.0-in-development", - "@sofie-automation/shared-lib": "1.53.0-in-development", + "@sofie-automation/blueprints-integration": "26.3.0-2", + "@sofie-automation/corelib": "26.3.0-2", + "@sofie-automation/shared-lib": "26.3.0-2", "deep-extend": "0.6.0", - "semver": "^7.6.3", - "type-fest": "^4.33.0", + "semver": "^7.7.3", + "type-fest": "^4.41.0", "underscore": "^1.13.7" }, "devDependencies": { "@types/deep-extend": "^0.6.2", - "@types/semver": "^7.5.8", + "@types/semver": "^7.7.1", "@types/underscore": "^1.13.0" }, "peerDependencies": { @@ -56,13 +55,5 @@ "mongodb": "^6.12.0" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "lint-staged": { - "*.{js,css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx}": [ - "yarn lint:raw" - ] - }, "packageManager": "yarn@4.12.0" } diff --git a/packages/meteor-lib/src/api/__tests__/client.test.ts b/packages/meteor-lib/src/api/__tests__/client.test.ts index 969ed62ec53..0312d360cb0 100644 --- a/packages/meteor-lib/src/api/__tests__/client.test.ts +++ b/packages/meteor-lib/src/api/__tests__/client.test.ts @@ -50,6 +50,24 @@ describe('ClientAPI', () => { }) } }) + it('Extracts additionalInfo from error args', () => { + const error = ClientAPI.responseError( + UserError.create( + UserErrorMessage.TakeRateLimit, + { + duration: 1000, + nextAllowedTakeTime: 1234567890, + }, + 429 + ) + ) + expect(error.additionalInfo).toEqual({ duration: 1000, nextAllowedTakeTime: 1234567890 }) + expect(error.errorCode).toBe(429) + }) + it('Does not include additionalInfo when no args', () => { + const error = ClientAPI.responseError(UserError.create(UserErrorMessage.InactiveRundown)) + expect(error.additionalInfo).toBeUndefined() + }) describe('isClientResponseSuccess', () => { it('Correctly recognizes a responseSuccess object', () => { const response = ClientAPI.responseSuccess(undefined) diff --git a/packages/meteor-lib/src/api/client.ts b/packages/meteor-lib/src/api/client.ts index 3f301e9b02d..1fcfa54f7a2 100644 --- a/packages/meteor-lib/src/api/client.ts +++ b/packages/meteor-lib/src/api/client.ts @@ -50,6 +50,8 @@ export namespace ClientAPI { errorCode: number /** On error, provide a human-readable error message */ error: SerializedUserError + /** Optional additional information about the error, forwarded from UserError args */ + additionalInfo?: Record } /** @@ -59,7 +61,12 @@ export namespace ClientAPI { * @returns A `ClientResponseError` object containing the error and the resolved error code. */ export function responseError(userError: UserError): ClientResponseError { - return { error: UserError.serialize(userError), errorCode: userError.errorCode } + const args = userError.userMessage.args + return { + error: UserError.serialize(userError), + errorCode: userError.errorCode, + ...(args !== undefined && Object.keys(args).length > 0 && { additionalInfo: args }), + } } export interface ClientResponseSuccess { /** On success, return success code (by default, use 200) */ diff --git a/packages/meteor-lib/src/api/migration.ts b/packages/meteor-lib/src/api/migration.ts index 80f64856679..c4aaf13f836 100644 --- a/packages/meteor-lib/src/api/migration.ts +++ b/packages/meteor-lib/src/api/migration.ts @@ -1,4 +1,3 @@ -import { MigrationStepInput, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import { BlueprintId, CoreSystemId, @@ -19,7 +18,6 @@ export interface NewMigrationAPI { runMigration( chunks: Array, hash: string, - inputResults: Array, isFirstOfPartialMigrations?: boolean ): Promise forceMigration(chunks: Array): Promise @@ -103,11 +101,8 @@ export interface GetMigrationStatusResult { migrationNeeded: boolean migration: { - canDoAutomaticMigration: boolean - manualInputs: Array hash: string automaticStepCount: number - manualStepCount: number ignoredStepCount: number partialMigration: boolean chunks: Array @@ -121,9 +116,6 @@ export interface RunMigrationResult { } export enum MigrationStepType { CORE = 'core', - SYSTEM = 'system', - STUDIO = 'studio', - SHOWSTYLE = 'showstyle', } export interface MigrationChunk { sourceType: MigrationStepType diff --git a/packages/meteor-lib/src/api/userActions.ts b/packages/meteor-lib/src/api/userActions.ts index fd1a07347ef..9f4c89afd01 100644 --- a/packages/meteor-lib/src/api/userActions.ts +++ b/packages/meteor-lib/src/api/userActions.ts @@ -6,7 +6,11 @@ import { BucketAdLib } from '@sofie-automation/corelib/dist/dataModel/BucketAdLi import { AdLibActionCommon } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' import { Time } from '@sofie-automation/blueprints-integration' -import { ExecuteActionResult, QueueNextSegmentResult } from '@sofie-automation/corelib/dist/worker/studio' +import { + ExecuteActionResult, + QueueNextSegmentResult, + TakeNextPartResult, +} from '@sofie-automation/corelib/dist/worker/studio' import { AdLibActionId, BucketAdLibActionId, @@ -34,13 +38,14 @@ export interface NewUserActionAPI { eventTime: Time, rundownPlaylistId: RundownPlaylistId, fromPartInstanceId: PartInstanceId | null - ): Promise> + ): Promise> setNext( userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId, - partId: PartId, - timeOffset?: number + partOrInstanceId: PartId | PartInstanceId, + timeOffset?: number, + isInstance?: boolean ): Promise> setNextSegment( userEvent: string, @@ -121,9 +126,9 @@ export interface NewUserActionAPI { userEvent: string, eventTime: Time, rundownPlaylistId: RundownPlaylistId, - actionDocId: AdLibActionId | RundownBaselineAdLibActionId | BucketAdLibActionId, + actionDocId: AdLibActionId | RundownBaselineAdLibActionId | BucketAdLibActionId | null, actionId: string, - userData: ActionUserData, + userData: ActionUserData | null, triggerMode?: string ): Promise> segmentAdLibPieceStart( diff --git a/packages/meteor-lib/src/collections/ExpectedPackages.ts b/packages/meteor-lib/src/collections/ExpectedPackages.ts index 58159714531..aa2a2fd1c4b 100644 --- a/packages/meteor-lib/src/collections/ExpectedPackages.ts +++ b/packages/meteor-lib/src/collections/ExpectedPackages.ts @@ -1,12 +1,12 @@ import { ExpectedPackage } from '@sofie-automation/blueprints-integration' import { assertNever, literal } from '@sofie-automation/corelib/dist/lib' -import { StudioLight } from '@sofie-automation/corelib/dist/dataModel/Studio' import deepExtend from 'deep-extend' import { htmlTemplateGetSteps, htmlTemplateGetFileNamesFromSteps, } from '@sofie-automation/shared-lib/dist/package-manager/helpers' import { ReadonlyDeep } from 'type-fest' +import { StudioPackageContainerSettings } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' export function getPreviewPackageSettings( expectedPackage: ExpectedPackage.Any @@ -57,13 +57,13 @@ export function getThumbnailPackageSettings( } export function getSideEffect( expectedPackage: ReadonlyDeep, - studio: Pick + packageContainerSettings: StudioPackageContainerSettings ): ExpectedPackage.Base['sideEffect'] { return deepExtend( {}, literal({ - previewContainerId: studio.previewContainerIds[0], // just pick the first. Todo: something else? - thumbnailContainerId: studio.thumbnailContainerIds[0], // just pick the first. Todo: something else? + previewContainerId: packageContainerSettings.previewContainerIds[0], // just pick the first. Todo: something else? + thumbnailContainerId: packageContainerSettings.thumbnailContainerIds[0], // just pick the first. Todo: something else? previewPackageSettings: getPreviewPackageSettings(expectedPackage as ExpectedPackage.Any), thumbnailPackageSettings: getThumbnailPackageSettings(expectedPackage as ExpectedPackage.Any), }), diff --git a/packages/meteor-lib/src/collections/Snapshots.ts b/packages/meteor-lib/src/collections/Snapshots.ts index b2af9e9af2e..6d8532042ad 100644 --- a/packages/meteor-lib/src/collections/Snapshots.ts +++ b/packages/meteor-lib/src/collections/Snapshots.ts @@ -13,6 +13,7 @@ export interface SnapshotBase { type: SnapshotType created: Time name: string + longname: string description?: string /** Version of the system that took the snapshot */ version: string diff --git a/packages/meteor-lib/src/collections/Studios.ts b/packages/meteor-lib/src/collections/Studios.ts index 6ddb74d40fa..7572c54d9ac 100644 --- a/packages/meteor-lib/src/collections/Studios.ts +++ b/packages/meteor-lib/src/collections/Studios.ts @@ -7,6 +7,7 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Studio' import { omit } from '@sofie-automation/corelib/dist/lib' import { protectString } from '@sofie-automation/corelib/dist/protectedString' +import { ReadonlyDeep } from 'type-fest' export function getActiveRoutes(routeSets: Record): ResultingMappingRoutes { const routes: ResultingMappingRoutes = { @@ -47,7 +48,7 @@ export function getActiveRoutes(routeSets: Record): Resu return routes } -export function getRoutedMappings( +export function getRoutedMappings>( inputMappings: { [layerName: string]: M }, mappingRoutes: ResultingMappingRoutes ): { [layerName: string]: M } { diff --git a/packages/meteor-lib/src/collections/lib.ts b/packages/meteor-lib/src/collections/lib.ts index 093ba0bd432..41133e98b29 100644 --- a/packages/meteor-lib/src/collections/lib.ts +++ b/packages/meteor-lib/src/collections/lib.ts @@ -25,8 +25,9 @@ export interface MongoReadOnlyCollection }> - extends MongoReadOnlyCollection { +export interface MongoCollection< + DBInterface extends { _id: ProtectedString }, +> extends MongoReadOnlyCollection { /** * Insert a document in the collection. Returns its unique _id. * @param doc The document to insert. May not yet have an _id attribute, in which case Meteor will generate one for you. diff --git a/packages/meteor-lib/src/migrations.ts b/packages/meteor-lib/src/migrations.ts new file mode 100644 index 00000000000..a86a0ebfb9c --- /dev/null +++ b/packages/meteor-lib/src/migrations.ts @@ -0,0 +1,40 @@ +export type ValidateFunctionCore = (afterMigration: boolean) => Promise +export type ValidateFunction = ValidateFunctionCore + +export type MigrateFunctionCore = () => Promise +export type MigrateFunction = MigrateFunctionCore + +export interface MigrationStepBase { + /** Unique id for this step */ + id: string + /** If this step overrides another step. Note: It's only possible to override steps in previous versions */ + overrideSteps?: string[] + + /** + * The validate function determines whether the step is to be applied + * (it can for example check that some value in the database is present) + * The function should return falsy if step is fulfilled (ie truthy if migrate function should be applied, return value could then be a string describing why) + * The function is also run after the migration-script has been applied (and should therefore return false if all is good) + */ + validate: TValidate + + /** If true, this step can be run automatically */ + canBeRunAutomatically: true + /** + * The migration script. This is the script that performs the updates. + * The migration script is optional, and may be omitted if the user is expected to perform the update manually + */ + migrate?: TMigrate + + /** If this step depend on the result of another step. Will pause the migration before this step in that case. */ + dependOnResultFrom?: string +} +export interface MigrationStep< + TValidate extends ValidateFunction, + TMigrate extends MigrateFunction, +> extends MigrationStepBase { + /** The version this Step applies to */ + version: string +} + +export type MigrationStepCore = MigrationStep diff --git a/packages/meteor-lib/src/triggers/actionFactory.ts b/packages/meteor-lib/src/triggers/actionFactory.ts index 2f5edfeefb2..86ef61684f3 100644 --- a/packages/meteor-lib/src/triggers/actionFactory.ts +++ b/packages/meteor-lib/src/triggers/actionFactory.ts @@ -38,7 +38,10 @@ export interface ReactivePlaylistActionContext { studioId: TriggerReactiveVar rundownPlaylistId: TriggerReactiveVar rundownPlaylist: TriggerReactiveVar< - Pick + Pick< + DBRundownPlaylist, + '_id' | 'name' | 'activationId' | 'rehearsal' | 'nextPartInfo' | 'currentPartInfo' | 'studioId' + > > currentRundownId: TriggerReactiveVar diff --git a/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts b/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts index 31c2ec9b069..9f8720c49f0 100644 --- a/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts +++ b/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts @@ -26,11 +26,12 @@ import { MountedAdLibTriggerType } from '../api/MountedTriggers.js' import { assertNever, generateTranslation } from '@sofie-automation/corelib/dist/lib' import { FindOptions } from '../collections/lib.js' import { TriggersContext, TriggerTrackerComputation } from './triggersContext.js' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' export type AdLibFilterChainLink = IRundownPlaylistFilterLink | IGUIContextFilterLink | IAdLibFilterLink /** This is a compiled Filter type, targetting a particular MongoCollection */ -type CompiledFilter = { +type CompiledAdLibFilter = { selector: MongoQuery options: FindOptions pick: number | undefined @@ -306,7 +307,7 @@ type AdLibActionType = RundownBaselineAdLibAction | AdLibAction function compileAdLibActionFilter( filterChain: IAdLibFilterLink[], sourceLayers: SourceLayers -): CompiledFilter { +): CompiledAdLibFilter { const selector: MongoQuery = {} const options: FindOptions = {} let pick: number | undefined = undefined @@ -405,7 +406,7 @@ type AdLibPieceType = function compileAdLibPieceFilter( filterChain: IAdLibFilterLink[], sourceLayers: SourceLayers -): CompiledFilter { +): CompiledAdLibFilter { const selector: MongoQuery = {} const options: FindOptions = {} let pick: number | undefined = undefined @@ -496,6 +497,61 @@ function compileAdLibPieceFilter( } } +type RundownSelector = { + activationId: boolean | undefined + name: RegExp | undefined + studioId: string | undefined + rehearsal: boolean | undefined +} + +function compileRundownPlaylistFilter(filterChain: IRundownPlaylistFilterLink[]): { + selector: RundownSelector + /** + * The query compiler has determined that this filter will always match + * it's safe to skip it entirely. + */ + matchAll?: true +} { + const selector: RundownSelector = { + activationId: undefined, + name: undefined, + studioId: undefined, + rehearsal: undefined, + } + + if (filterChain.length === 0) { + // no filter, accept all + return { + selector, + matchAll: true, + } + } + + filterChain.forEach((link) => { + switch (link.field) { + case 'activationId': + selector.activationId = link.value + return + case 'name': + selector.name = new RegExp(link.value) + return + case 'studioId': + selector.studioId = link.value + return + case 'rehearsal': + selector.rehearsal = link.value + return + default: + assertNever(link) + return + } + }) + + return { + selector, + } +} + /** * Compile the filter chain and return a reactive function that will return the result set for this adLib filter * @param filterChain @@ -508,6 +564,13 @@ export function compileAdLibFilter( sourceLayers: SourceLayers ): (context: ReactivePlaylistActionContext, computation: TriggerTrackerComputation | null) => Promise { const onlyAdLibLinks = filterChain.filter((link) => link.object === 'adLib') as IAdLibFilterLink[] + const onlyRundownPlaylistLinks = filterChain.filter( + (link) => link.object === 'rundownPlaylist' + ) as IRundownPlaylistFilterLink[] + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ignore unused + const rundownPlaylistFilter = compileRundownPlaylistFilter(onlyRundownPlaylistLinks) const adLibPieceTypeFilter = compileAdLibPieceFilter(onlyAdLibLinks, sourceLayers) const adLibActionTypeFilter = compileAdLibActionFilter(onlyAdLibLinks, sourceLayers) @@ -556,6 +619,35 @@ export function compileAdLibFilter( } } + { + const matchAll = rundownPlaylistFilter.matchAll + const currentRundownPlaylist = context.rundownPlaylist.get(computation) + + const activationStateMatches = + rundownPlaylistFilter.selector.activationId !== undefined + ? (currentRundownPlaylist?.activationId !== undefined) === + rundownPlaylistFilter.selector.activationId + : true + const nameMatches = + rundownPlaylistFilter.selector.name !== undefined + ? currentRundownPlaylist?.name.match(rundownPlaylistFilter.selector.name) !== null + : true + const studioMatches = + rundownPlaylistFilter.selector.studioId !== undefined + ? unprotectString(currentRundownPlaylist?.studioId) === rundownPlaylistFilter.selector.studioId + : true + const rehearsalMatches = + rundownPlaylistFilter.selector.rehearsal !== undefined + ? currentRundownPlaylist?.rehearsal === rundownPlaylistFilter.selector.rehearsal + : true + + if (!matchAll) { + if (!activationStateMatches || !nameMatches || !studioMatches || !rehearsalMatches) { + return [] + } + } + } + { let skip = adLibPieceTypeFilter.skip const currentNextOverride: MongoQuery = {} diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index d924c004fdd..b902f9a9de3 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,47 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [26.3.0-2](https://github.com/Sofie-Automation/sofie-core/compare/v26.3.0-1...v26.3.0-2) (2026-02-18) + + +### Bug Fixes + +* Add missing 'rootDir' to tsconfig ([20e7d12](https://github.com/Sofie-Automation/sofie-core/commit/20e7d12aabdf4c9cf36f8a271435fce8aa253c2d)) + + + + + +# [26.3.0-1](https://github.com/Sofie-Automation/sofie-core/compare/v26.3.0-0...v26.3.0-1) (2026-02-11) + +**Note:** Version bump only for package mos-gateway + + + + + +# [26.3.0-0](https://github.com/Sofie-Automation/sofie-core/compare/v1.52.0...v26.3.0-0) (2026-02-04) + + +### Bug Fixes + +* docker images using CMD instead of ENTRYPOINT ([e1beb6e](https://github.com/Sofie-Automation/sofie-core/commit/e1beb6e082c7c9ce4a8009feceb58eb7ef89f308)) +* hot standby was not refering to it's full name ([28b9ce1](https://github.com/Sofie-Automation/sofie-core/commit/28b9ce1893f5f495f2798123180bae9ed35cd8f2)) +* update mos-connection for missing mosID bug fix ([#9](https://github.com/Sofie-Automation/sofie-core/issues/9)) ([e8e07e3](https://github.com/Sofie-Automation/sofie-core/commit/e8e07e3e86e0a6e4d1bb5802f0e782ad323f424e)) + + +### Features + +* add health endpoints to MOS- and Playout-Gateway ([5b590dd](https://github.com/Sofie-Automation/sofie-core/commit/5b590ddbaf86ee90d338837867a4d3bfc2e11c97)) +* Add support for Gateway configuration from the studio API ([#1539](https://github.com/Sofie-Automation/sofie-core/issues/1539)) ([963542a](https://github.com/Sofie-Automation/sofie-core/commit/963542aa060f7db768d47a1d7e4e1f25367bb321)) +* mos status flow rework ([#1356](https://github.com/Sofie-Automation/sofie-core/issues/1356)) ([672f2bd](https://github.com/Sofie-Automation/sofie-core/commit/672f2bd2873ae306db9dfcbbc3064fdcc9ea1cd0)) +* move GW config types to generated in shared lib ([f54d9ca](https://github.com/Sofie-Automation/sofie-core/commit/f54d9ca63bc00a05915aac45e0be5b595c980567)) +* update meteor to 3.3.2 ([#1529](https://github.com/Sofie-Automation/sofie-core/issues/1529)) ([9bd232e](https://github.com/Sofie-Automation/sofie-core/commit/9bd232e8f0561a46db8cc6143c5353d7fa531206)) + + + + + # [1.52.0](https://github.com/Sofie-Automation/sofie-core/compare/v1.52.0-in-testing.1...v1.52.0) (2025-06-30) **Note:** Version bump only for package mos-gateway diff --git a/packages/mos-gateway/eslint.config.mjs b/packages/mos-gateway/eslint.config.mjs deleted file mode 100644 index b9e5a88fd88..00000000000 --- a/packages/mos-gateway/eslint.config.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' - -export default generateEslintConfig({}) diff --git a/packages/mos-gateway/jest.config.js b/packages/mos-gateway/jest.config.js index 76ff2b14f1e..04b8ea8dd1c 100644 --- a/packages/mos-gateway/jest.config.js +++ b/packages/mos-gateway/jest.config.js @@ -6,6 +6,11 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.json', + diagnostics: { + ignoreCodes: [ + 151002, // hybrid module kind (Node16/18/Next) + ], + }, }, ], }, diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 55b728c761b..a4a4b198d34 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -26,8 +26,7 @@ } ], "scripts": { - "lint:raw": "run -T eslint", - "lint": "run lint:raw .", + "lint": "run -T lint mos-gateway", "lint-fix": "run lint --fix", "unit": "run -T jest", "test": "run lint && run unit", @@ -62,21 +61,13 @@ ], "dependencies": { "@mos-connection/connector": "^5.0.0-alpha.0", - "@sofie-automation/server-core-integration": "1.53.0-in-development", - "@sofie-automation/shared-lib": "1.53.0-in-development", + "@sofie-automation/server-core-integration": "26.3.0-2", + "@sofie-automation/shared-lib": "26.3.0-2", "tslib": "^2.8.1", - "type-fest": "^4.33.0", + "type-fest": "^4.41.0", "underscore": "^1.13.7", - "winston": "^3.17.0" + "winston": "^3.19.0" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "lint-staged": { - "*.{js,css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx}": [ - "yarn lint:raw" - ] - }, "packageManager": "yarn@4.12.0" } diff --git a/packages/mos-gateway/src/integrationTests/index.spec.ts b/packages/mos-gateway/src/integrationTests/index.spec.ts index 5fb4897866a..a5ea7cec7f2 100644 --- a/packages/mos-gateway/src/integrationTests/index.spec.ts +++ b/packages/mos-gateway/src/integrationTests/index.spec.ts @@ -1,3 +1,4 @@ +import { protectString } from '@sofie-automation/server-core-integration' import { Connector } from '../connector.js' import * as Winston from 'winston' @@ -31,7 +32,7 @@ test('Simple test', async () => { watchdog: false, }, device: { - deviceId: 'JestTest', + deviceId: protectString('JestTest'), deviceToken: '1234', }, certificates: { @@ -56,6 +57,9 @@ test('Simple test', async () => { }, // devices: [] }, + health: { + port: undefined, + }, }) expect(c).toBeInstanceOf(Connector) diff --git a/packages/mos-gateway/src/versions.ts b/packages/mos-gateway/src/versions.ts index 63ad2d96d86..67f6462eda2 100644 --- a/packages/mos-gateway/src/versions.ts +++ b/packages/mos-gateway/src/versions.ts @@ -4,9 +4,8 @@ import * as Winston from 'winston' export function getVersions(logger: Winston.Logger): { [packageName: string]: string } { const versions: { [packageName: string]: string } = {} - if (process.env.npm_package_version) { - versions['_process'] = process.env.npm_package_version - } + const pkg = require('../package.json') + if (pkg?.version) versions['_process'] = pkg.version const pkgNames = ['@mos-connection/connector'] try { diff --git a/packages/mos-gateway/tsconfig.build.json b/packages/mos-gateway/tsconfig.build.json index 67f2cc0cfe1..e415e6edf0d 100644 --- a/packages/mos-gateway/tsconfig.build.json +++ b/packages/mos-gateway/tsconfig.build.json @@ -1,9 +1,10 @@ { "extends": "@sofie-automation/code-standard-preset/ts/tsconfig.bin", - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.json"], "exclude": ["node_modules/**", "**/*spec.ts", "**/__tests__/*", "**/__mocks__/*"], "compilerOptions": { "outDir": "./dist", + "rootDir": "./src", "baseUrl": "./", "paths": { "*": ["./node_modules/*"], @@ -12,7 +13,9 @@ "types": ["node"], "resolveJsonModule": true, "skipLibCheck": true, - "esModuleInterop": true + "esModuleInterop": true, + "declaration": true, + "composite": true }, "references": [ // diff --git a/packages/openapi/api/actions.yaml b/packages/openapi/api/actions.yaml index 70f1788148c..9756d30da13 100644 --- a/packages/openapi/api/actions.yaml +++ b/packages/openapi/api/actions.yaml @@ -45,6 +45,8 @@ paths: $ref: 'definitions/playlists.yaml#/resources/activate' /playlists/{playlistId}/deactivate: $ref: 'definitions/playlists.yaml#/resources/deactivate' + /playlists/{playlistId}/rundowns/{rundownId}/activate-adlib-testing: + $ref: 'definitions/playlists.yaml#/resources/activateAdlibTesting' /playlists/{playlistId}/execute-adlib: $ref: 'definitions/playlists.yaml#/resources/executeAdLib' /playlists/{playlistId}/execute-bucket-adlib: @@ -111,3 +113,20 @@ paths: # snapshot operations /snapshots: $ref: 'definitions/snapshots.yaml#/resources/snapshots' + # ingest operations + /ingest/{studioId}/playlists: + $ref: 'definitions/ingest.yaml#/resources/playlists' + /ingest/{studioId}/playlists/{playlistId}: + $ref: 'definitions/ingest.yaml#/resources/playlist' + /ingest/{studioId}/playlists/{playlistId}/rundowns: + $ref: 'definitions/ingest.yaml#/resources/rundowns' + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}: + $ref: 'definitions/ingest.yaml#/resources/rundown' + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments: + $ref: 'definitions/ingest.yaml#/resources/segments' + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}: + $ref: 'definitions/ingest.yaml#/resources/segment' + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts: + $ref: 'definitions/ingest.yaml#/resources/parts' + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts/{partId}: + $ref: 'definitions/ingest.yaml#/resources/part' diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml new file mode 100644 index 00000000000..3826b8c209f --- /dev/null +++ b/packages/openapi/api/definitions/ingest.yaml @@ -0,0 +1,1283 @@ +title: ingest +description: Ingest methods +resources: + playlists: + get: + operationId: getPlaylists + summary: Gets all Playlists. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + responses: + 200: + description: Command successfully handled - returns an array of Playlists. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/playlistResponse' + delete: + operationId: deletePlaylists + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + summary: Deletes all Playlists. Resources under the Playlists (e.g. Rundowns) will also be removed. + responses: + 202: + description: Request for deleting accepted. + playlist: + get: + operationId: getPlaylist + summary: Gets the specified Playlist. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist to return. + required: true + schema: + type: string + responses: + 200: + description: Playlist is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/playlistResponse' + 404: + description: Invalid playlistId + $ref: '#/components/responses/playlistNotFound' + delete: + operationId: deletePlaylist + summary: Deletes a specified Playlist. Resources under the Playlist (e.g. Rundowns) will also be removed. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist to delete. + required: true + schema: + type: string + responses: + 202: + description: Request for deleting accepted. + 404: + $ref: '#/components/responses/playlistNotFound' + rundowns: + get: + operationId: getRundowns + summary: Gets all Rundowns belonging to a specified Playlist. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundowns belong to. + required: true + schema: + type: string + responses: + 200: + description: Command successfully handled - returns an array of Rundowns. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/rundownResponse' + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + post: + operationId: postRundown + summary: Creates a Rundown in a specified Playlist. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the new Rundown belongs to. + required: true + schema: + type: string + requestBody: + description: Rundown data to ingest. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/rundown' + responses: + 202: + description: Request has been accepted. + 400: + description: Bad request. + put: + operationId: putRundowns + summary: Updates Rundowns belonging to a specified Playlist. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundowns to update belong to. + required: true + schema: + type: string + requestBody: + description: Contains the Rundown data. + required: true + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/rundown' + responses: + 202: + description: Request has been accepted. + 400: + description: Bad request. + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + delete: + operationId: deleteRundowns + tags: + - ingest + summary: Deletes all Rundowns belonging to specified Playlist. Resources under the Rundowns (e.g. Segments) will also be removed. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundowns to delete belong to. + required: true + schema: + type: string + responses: + 202: + description: Request accepted. + 404: + $ref: '#/components/responses/idNotFound' + rundown: + get: + operationId: getRundown + summary: Gets the specified Rundown. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundown belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown to return. + required: true + schema: + type: string + responses: + 200: + description: Rundown is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/rundownResponse' + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + put: + operationId: putRundown + summary: Updates an existing specified Rundown. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundown to update belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown to update. + required: true + schema: + type: string + requestBody: + description: Contains the Rundown data. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/rundown' + responses: + 202: + description: Request has been accepted. + 400: + description: Bad request. + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + delete: + operationId: deleteRundown + summary: Deletes a specified Rundown. Resources under the Rundown (e.g. Segments) will also be removed. + tags: + - ingest + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Rundown belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown to delete. + required: true + schema: + type: string + responses: + 202: + description: Request for deleting accepted. + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + segments: + get: + operationId: getSegments + tags: + - ingest + summary: Gets all Segments belonging to a specified Rundown. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segments belong to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segments belong to. + required: true + schema: + type: string + responses: + 200: + description: Command successfully handled - returns an array of Segments. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/segmentResponse' + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + post: + operationId: postSegment + tags: + - ingest + summary: Creates a Segment in a specified Rundown. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the new Segment belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the new Segment belongs to. + required: true + schema: + type: string + requestBody: + description: Contains the Segment data. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/segment' + responses: + 202: + description: Request accepted. + 400: + description: Bad request. + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + put: + operationId: putSegments + tags: + - ingest + summary: Updates Segments belonging to a specified Rundown. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segments to update belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segments to update belong to. + required: true + schema: + type: string + requestBody: + description: Contains the Segment data. + required: true + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/segment' + responses: + 202: + description: Request accepted. + 400: + description: Bad request. + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + delete: + operationId: deleteSegments + tags: + - ingest + summary: Deletes all Segments belonging to specified Rundown. Resources under the Segments (e.g. Parts) will also be removed. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segments belong to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segments to delete belong to. + required: true + schema: + type: string + responses: + 202: + description: Request accepted. + 404: + $ref: '#/components/responses/idNotFound' + segment: + get: + operationId: getSegment + tags: + - ingest + summary: Gets the specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segment belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segment belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment to return. + required: true + schema: + type: string + responses: + 200: + description: Segment is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/segmentResponse' + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/segmentNotFound' + put: + operationId: putSegment + tags: + - ingest + summary: Updates an existing specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segment to update belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segment to update belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment to update. + required: true + schema: + type: string + requestBody: + description: Contains the Segment data. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/segment' + responses: + 202: + description: Request accepted. + 400: + description: Bad request. + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + delete: + operationId: deleteSegment + tags: + - ingest + summary: Deletes a specified Segment. Resources under the Segment (e.g. Parts) will also be removed. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segment belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Segment belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment to delete. + required: true + schema: + type: string + responses: + 202: + description: Request accepted. + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + parts: + get: + operationId: getParts + tags: + - ingest + summary: Gets all Parts belonging to a specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Parts belong to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Parts belong to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Parts belong to. + required: true + schema: + type: string + responses: + 200: + description: Command successfully handled - returns an array of Parts. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/partResponse' + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/segmentNotFound' + post: + operationId: postPart + tags: + - ingest + summary: Creates a Part in a specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the new Part belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the new Part belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the new Part belongs to. + required: true + schema: + type: string + requestBody: + description: Contains the Parts data. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/part' + responses: + 202: + description: Request accepted. + 400: + description: Bad request. + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/partNotFound' + put: + operationId: putParts + tags: + - ingest + summary: Updates Parts belonging to a specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Parts to update belong to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Parts to update belong to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Parts to update belong to. + required: true + schema: + type: string + requestBody: + description: Contains the Parts data. + required: true + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/part' + responses: + 202: + description: Request accepted. + 400: + description: Bad request. + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/partNotFound' + delete: + operationId: deleteParts + tags: + - ingest + summary: Deletes all Parts belonging to specified Segment. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Parts belong to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Parts belong to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Parts to delete belong to. + required: true + schema: + type: string + responses: + 202: + description: Request for deleting accepted. + 404: + $ref: '#/components/responses/idNotFound' + part: + get: + operationId: getPart + tags: + - ingest + summary: Gets the specified Part. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Part belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Part belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Part belongs to. + required: true + schema: + type: string + - name: partId + in: path + description: Internal or external ID of the Part to return. + required: true + schema: + type: string + responses: + 200: + description: Part is returned. + content: + application/json: + schema: + $ref: '#/components/schemas/partResponse' + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/segmentNotFound' + # - $ref: '#/components/responses/partNotFound' + put: + operationId: putPart + tags: + - ingest + summary: Updates an existing specified Part. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Part to update belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Part to update belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Part to update belongs to. + schema: + type: string + - name: partId + in: path + description: Internal or external ID of the Part to update. + schema: + type: string + requestBody: + description: Contains the Rundown data. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/part' + responses: + 202: + description: Request has been accepted. + 400: + description: Bad request. + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/segmentNotFound' + delete: + operationId: deletePart + tags: + - ingest + summary: Deletes a specified Part. + parameters: + - name: studioId + in: path + description: ID of the studio that is performing ingest operation. + required: true + schema: + type: string + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Part belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Internal or external ID of the Rundown the Part belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Internal or external ID of the Segment the Part belongs to. + required: true + schema: + type: string + - name: partId + in: path + description: Internal or external ID of the Part to delete. + required: true + schema: + type: string + responses: + 202: + description: Request has been accepted. + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/segmentNotFound' + +components: + responses: + idNotFound: + # oneOf responses don't render correctly with current tools. Use this response as a replacement. + description: The specified resource does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified resource was not found. + required: + - status + - message + additionalProperties: false + playlistNotFound: + description: The specified Playlist does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + notFound: + type: string + const: playlist + example: playlist + message: + type: string + example: The specified Playlist was not found. + required: + - status + - notFound + - message + additionalProperties: false + rundownNotFound: + description: The specified Rundown does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified Rundown was not found. + required: + - status + - notFound + - message + additionalProperties: false + segmentNotFound: + description: The specified Segment does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified Segment was not found. + required: + - status + - notFound + - message + additionalProperties: false + partNotFound: + description: The specified Part does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + message: + type: string + example: The specified Part was not found. + required: + - status + - notFound + - message + additionalProperties: false + badRequest: + description: Bad request. + schemas: + playlist: + type: object + properties: + name: + type: string + example: Playlist name + externalId: + type: string + example: playlist1 + rundownIds: + type: array + items: + type: string + example: + - rundown1 + - rundown2 + - rundown3 + required: + - name + additionalProperties: false + playlistResponse: + type: object + properties: + id: + type: string + externalId: + type: string + example: playlist1 + rundownIds: + type: array + items: + type: string + example: + - rundown1 + - rundown2 + - rundown3 + studioId: + type: string + example: studio0 + required: + - id + - externalId + - rundownIds + - studioId + additionalProperties: false + rundown: + type: object + properties: + externalId: + type: string + example: rundown1 + name: + type: string + example: Rundown 1 + type: + type: string + example: external + description: Value that defines the structure of the payload, must be known by Sofie. + resyncUrl: + type: string + example: http://nrcs-url/resync/rundownId + description: URL on which the Sofie will send the POST request to request re-syncing of the Rundown. + segments: + type: array + items: + $ref: '#/components/schemas/segment' + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - type + - resyncUrl + - segments + additionalProperties: false + rundownResponse: + type: object + properties: + id: + type: string + externalId: + type: string + example: rundown1 + studioId: + type: string + example: studio0 + playlistId: + type: string + example: playlist1 + playlistExternalId: + type: string + example: playlistExternal1 + name: + type: string + example: Rundown 1 + type: + type: string + timing: + type: object + properties: + type: + type: string + enum: + - none + - forward-time + - back-time + expectedStart: + type: number + description: Epoch timestamp in milliseconds. + expectedDuration: + type: number + description: Epoch interval in milliseconds. + expectedEnd: + type: number + description: Epoch timestamp in milliseconds. + required: + - type + additionalProperties: false + required: + - id + - externalId + - studioId + - playlistId + - name + segment: + type: object + properties: + externalId: + type: string + example: segment1 + name: + type: string + example: Segment 1 + rank: + type: number + description: The position of the Segment in the parent Rundown. + inclusiveMinimum: 0.0 + example: 1 + parts: + type: array + items: + $ref: '#/components/schemas/part' + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + - parts + additionalProperties: false + segmentResponse: + type: object + properties: + id: + type: string + example: segment1 + externalId: + type: string + example: segmentExternal1 + rundownId: + type: string + example: rundown11 + name: + type: string + example: Segment 1 + rank: + type: number + example: 1 + isHidden: + type: boolean + timing: + type: object + properties: + expectedStart: + type: number + description: Epoch timestamp in milliseconds. + expectedEnd: + type: number + description: Epoch timestamp in milliseconds. + additionalProperties: false + required: + - id + - externalId + - rundownId + - name + - rank + additionalProperties: false + part: + type: object + properties: + externalId: + type: string + example: part1 + name: + type: string + example: Part 1 + rank: + type: number + description: Position of the Part in the Segment. + example: 0 + payload: + type: object + additionalProperties: true + required: + - externalId + - name + - rank + additionalProperties: false + partResponse: + type: object + properties: + id: + type: string + example: part1 + externalId: + type: string + example: partExternal1 + rundownId: + type: string + example: rundown1 + segmentId: + type: string + example: segment1 + name: + type: string + example: Part 1 + rank: + type: number + example: 0 + expectedDuration: + type: number + description: Calculated based on pieces. + example: 10000 + autoNext: + type: boolean + example: false + required: + - id + - externalId + - rundownId + - segmentId + - name + - rank + additionalProperties: false diff --git a/packages/openapi/api/definitions/playlists.yaml b/packages/openapi/api/definitions/playlists.yaml index 943778f6418..cdb34f65a26 100644 --- a/packages/openapi/api/definitions/playlists.yaml +++ b/packages/openapi/api/definitions/playlists.yaml @@ -89,6 +89,32 @@ resources: $ref: '#/components/responses/playlistNotFound' 500: $ref: '#/components/responses/internalServerError' + activateAdlibTesting: + put: + operationId: activateAdlibTesting + tags: + - playlists + summary: Activates AdLib testing mode. + parameters: + - name: playlistId + in: path + description: Playlist to activate testing mode for. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown to activate testing mode for. + required: true + schema: + type: string + responses: + 200: + $ref: '#/components/responses/putSuccess' + 404: + $ref: '#/components/responses/playlistNotFound' + 500: + $ref: '#/components/responses/internalServerError' executeAdLib: post: operationId: executeAdLib @@ -563,7 +589,22 @@ resources: description: May be specified to ensure that multiple take requests from the same Part do not result in multiple takes. responses: 200: - $ref: '#/components/responses/putSuccess' + description: Take was successful - returns the next allowed take time. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 200 + result: + type: object + properties: + nextTakeTime: + type: number + description: Unix timestamp (ms) of when the next take will be allowed. + example: 1707024000000 404: $ref: '#/components/responses/playlistNotFound' 412: @@ -579,6 +620,40 @@ resources: message: type: string example: No Next point found, please set a part as Next before doing a TAKE. + 425: + description: Take is blocked due to a transition or adlib action. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 425 + message: + type: string + example: Cannot take during a transition + additionalInfo: + type: object + description: Additional error details, e.g. includes nextAllowedTakeTime (Unix timestamp ms) for blocked takes. + additionalProperties: true + 429: + description: Take rate limit exceeded - takes are happening too quickly. + content: + application/json: + schema: + type: object + properties: + status: + type: number + example: 429 + message: + type: string + example: Ignoring TAKES that are too quick after eachother (1000 ms) + additionalInfo: + type: object + description: Additional error details, e.g. includes nextAllowedTakeTime (Unix timestamp ms) for blocked takes. + additionalProperties: true 500: $ref: '#/components/responses/internalServerError' diff --git a/packages/openapi/eslint.config.mjs b/packages/openapi/eslint.config.mjs deleted file mode 100644 index 6b708af6b4f..00000000000 --- a/packages/openapi/eslint.config.mjs +++ /dev/null @@ -1,17 +0,0 @@ -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' -import pluginYaml from 'eslint-plugin-yml' - -const extendedRules = await generateEslintConfig({ - ignores: ['client', 'server'], -}) -extendedRules.push(...pluginYaml.configs['flat/recommended'], { - files: ['**/*.yaml'], - - rules: { - 'yml/quotes': ['error', { prefer: 'single' }], - 'yml/spaced-comment': ['error'], - 'spaced-comment': ['off'], - }, -}) - -export default extendedRules diff --git a/packages/openapi/jest.config.js b/packages/openapi/jest.config.js index 423eeb19d64..296e097a01b 100644 --- a/packages/openapi/jest.config.js +++ b/packages/openapi/jest.config.js @@ -6,6 +6,11 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.json', + diagnostics: { + ignoreCodes: [ + 151002, // hybrid module kind (Node16/18/Next) + ], + }, }, ], }, diff --git a/packages/openapi/package.json b/packages/openapi/package.json index c4c4a887c4e..f91fc7aee31 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "license": "MIT", "repository": { "type": "git", @@ -13,10 +13,8 @@ "build:main": "run -T tsc -p tsconfig.build.json", "cov": "run unit && open-cli coverage/lcov-report/index.html", "cov-open": "open-cli coverage/lcov-report/index.html", + "lint": "run -T lint openapi", "unit": "run genserver && node --experimental-fetch run_server_tests.mjs", - "lint:raw": "run -T eslint", - "lint": "run lint:raw .", - "lint-fix": "run lint --fix", "genclient:ts": "run -T rimraf client/ts && openapi-generator-cli generate -i ./api/actions.yaml -o client/ts -g typescript-fetch -p supportsES6=true", "genclient:rs": "run -T rimraf client/rs && openapi-generator-cli generate -i ./api/actions.yaml -o client/rs -g rust", "genclient:cs": "run -T rimraf client/cs && openapi-generator-cli generate -i ./api/actions.yaml -o client/cs -g csharp", @@ -39,20 +37,11 @@ "tslib": "^2.8.1" }, "devDependencies": { - "@openapitools/openapi-generator-cli": "^2.20.2", - "eslint": "^9.18.0", - "eslint-plugin-yml": "^1.16.0", - "js-yaml": "^4.1.0", + "@openapitools/openapi-generator-cli": "^2.28.0", + "eslint": "^9.39.2", + "js-yaml": "^4.1.1", "wget-improved": "^3.4.0" }, - "lint-staged": { - "*.{css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx,js,jsx,yaml}": [ - "yarn lint:raw" - ] - }, "publishConfig": { "access": "public" }, diff --git a/packages/openapi/run_server_tests.mjs b/packages/openapi/run_server_tests.mjs index 80c3dacd83a..994d80c461d 100644 --- a/packages/openapi/run_server_tests.mjs +++ b/packages/openapi/run_server_tests.mjs @@ -8,7 +8,7 @@ import { exec } from 'child_process' import { exit } from 'process' import { join } from 'path' import { createServer } from 'http' -// eslint-disable-next-line n/no-missing-import + import { expressAppConfig } from './server/node_modules/oas3-tools/dist/index.js' const testTimeout = 120000 diff --git a/packages/openapi/src/__tests__/ingest.spec.ts b/packages/openapi/src/__tests__/ingest.spec.ts new file mode 100644 index 00000000000..d0ad0c78446 --- /dev/null +++ b/packages/openapi/src/__tests__/ingest.spec.ts @@ -0,0 +1,454 @@ +import { Configuration, IngestApi, Part } from '../../client/ts/index.js' +import { checkServer } from '../checkServer.js' +import Logging from '../httpLogging.js' + +const httpLogging = false +const studioId = 'studio0' + +describe('Ingest API', () => { + const config = new Configuration({ + basePath: process.env.SERVER_URL, + middleware: [new Logging(httpLogging)], + }) + + beforeAll(async () => await checkServer(config)) + + const ingestApi = new IngestApi(config) + + /** + * PLAYLISTS + */ + const playlistIds: string[] = [] + test('Can request all playlists', async () => { + const playlists = await ingestApi.getPlaylists({ studioId }) + + expect(playlists.length).toBeGreaterThanOrEqual(1) + playlists.forEach((playlist) => { + expect(typeof playlist).toBe('object') + expect(typeof playlist.id).toBe('string') + expect(typeof playlist.externalId).toBe('string') + expect(typeof playlist.studioId).toBe('string') + expect(typeof playlist.rundownIds).toBe('object') + playlist.rundownIds.forEach((rundownId) => { + expect(typeof rundownId).toBe('string') + }) + + playlistIds.push(playlist.externalId) + }) + }) + + test('Can request a playlist by id', async () => { + const playlist = await ingestApi.getPlaylist({ + studioId, + playlistId: playlistIds[0], + }) + + expect(typeof playlist).toBe('object') + expect(typeof playlist.id).toBe('string') + expect(typeof playlist.externalId).toBe('string') + expect(typeof playlist.studioId).toBe('string') + expect(typeof playlist.rundownIds).toBe('object') + playlist.rundownIds.forEach((rundownId) => { + expect(typeof rundownId).toBe('string') + }) + }) + + test('Can delete multiple playlists', async () => { + const result = await ingestApi.deletePlaylists({ studioId }) + expect(result).toBe(undefined) + }) + + test('Can delete playlist by id', async () => { + const result = await ingestApi.deletePlaylist({ + studioId, + playlistId: playlistIds[0], + }) + expect(result).toBe(undefined) + }) + + /** + * RUNDOWNS + */ + const rundownIds: string[] = [] + test('Can request all rundowns', async () => { + const rundowns = await ingestApi.getRundowns({ + studioId, + playlistId: playlistIds[0], + }) + + expect(rundowns.length).toBeGreaterThanOrEqual(1) + + rundowns.forEach((rundown) => { + expect(typeof rundown).toBe('object') + expect(rundown).toHaveProperty('id') + expect(rundown).toHaveProperty('externalId') + expect(rundown).toHaveProperty('name') + expect(rundown).toHaveProperty('studioId') + expect(rundown).toHaveProperty('playlistId') + expect(rundown).toHaveProperty('playlistExternalId') + expect(rundown).toHaveProperty('type') + expect(rundown).toHaveProperty('timing') + expect(rundown.timing).toHaveProperty('type') + expect(typeof rundown.id).toBe('string') + expect(typeof rundown.externalId).toBe('string') + expect(typeof rundown.name).toBe('string') + expect(typeof rundown.studioId).toBe('string') + expect(typeof rundown.playlistId).toBe('string') + expect(typeof rundown.playlistExternalId).toBe('string') + expect(typeof rundown.type).toBe('string') + expect(typeof rundown.timing).toBe('object') + expect(typeof rundown.timing.type).toBe('string') + rundownIds.push(rundown.externalId) + }) + }) + + test('Can request rundown by id', async () => { + const rundown = await ingestApi.getRundown({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + }) + + expect(typeof rundown).toBe('object') + expect(rundown).toHaveProperty('id') + expect(rundown).toHaveProperty('externalId') + expect(rundown).toHaveProperty('name') + expect(rundown).toHaveProperty('studioId') + expect(rundown).toHaveProperty('playlistId') + expect(rundown).toHaveProperty('playlistExternalId') + expect(rundown).toHaveProperty('type') + expect(rundown).toHaveProperty('timing') + expect(rundown.timing).toHaveProperty('type') + expect(typeof rundown.id).toBe('string') + expect(typeof rundown.externalId).toBe('string') + expect(typeof rundown.name).toBe('string') + expect(typeof rundown.studioId).toBe('string') + expect(typeof rundown.playlistId).toBe('string') + expect(typeof rundown.playlistExternalId).toBe('string') + expect(typeof rundown.type).toBe('string') + expect(typeof rundown.timing).toBe('object') + expect(typeof rundown.timing.type).toBe('string') + }) + + const rundown = { + externalId: 'newRundown', + name: 'New rundown', + type: 'external', + resyncUrl: 'resyncUrl', + segments: [], + } + + test('Can create rundown', async () => { + const result = await ingestApi.postRundown({ studioId, playlistId: playlistIds[0], rundown }) + expect(result).toBe(undefined) + }) + + test('Can update multiple rundowns', async () => { + const result = await ingestApi.putRundowns({ studioId, playlistId: playlistIds[0], rundown: [rundown] }) + expect(result).toBe(undefined) + }) + + const updatedRundownId = 'rundown3' + test('Can update single rundown', async () => { + const result = await ingestApi.putRundown({ + studioId, + playlistId: playlistIds[0], + rundownId: updatedRundownId, + rundown, + }) + expect(result).toBe(undefined) + }) + + test('Can delete multiple rundowns', async () => { + const result = await ingestApi.deleteRundowns({ studioId, playlistId: playlistIds[0] }) + expect(result).toBe(undefined) + }) + + test('Can delete rundown by id', async () => { + const result = await ingestApi.deleteRundown({ + studioId, + playlistId: playlistIds[0], + rundownId: updatedRundownId, + }) + expect(result).toBe(undefined) + }) + + /** + * INGEST SEGMENT + */ + const segmentIds: string[] = [] + test('Can request all segments', async () => { + const segments = await ingestApi.getSegments({ studioId, playlistId: playlistIds[0], rundownId: rundownIds[0] }) + + expect(segments.length).toBeGreaterThanOrEqual(1) + + segments.forEach((segment) => { + expect(typeof segment).toBe('object') + expect(typeof segment.id).toBe('string') + expect(typeof segment.externalId).toBe('string') + expect(typeof segment.rundownId).toBe('string') + expect(typeof segment.name).toBe('string') + expect(typeof segment.rank).toBe('number') + expect(typeof segment.timing).toBe('object') + expect(typeof segment.timing.expectedStart).toBe('number') + expect(typeof segment.timing.expectedEnd).toBe('number') + segmentIds.push(segment.externalId) + }) + }) + + test('Can request segment by id', async () => { + const segment = await ingestApi.getSegment({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + }) + + expect(segment).toHaveProperty('id') + expect(segment).toHaveProperty('externalId') + expect(segment).toHaveProperty('rundownId') + expect(segment).toHaveProperty('name') + expect(segment).toHaveProperty('rank') + expect(segment).toHaveProperty('timing') + expect(segment.timing).toHaveProperty('expectedStart') + expect(segment.timing).toHaveProperty('expectedEnd') + expect(typeof segment.id).toBe('string') + expect(typeof segment.externalId).toBe('string') + expect(typeof segment.rundownId).toBe('string') + expect(typeof segment.name).toBe('string') + expect(typeof segment.rank).toBe('number') + expect(typeof segment.timing).toBe('object') + expect(typeof segment.timing.expectedStart).toBe('number') + expect(typeof segment.timing.expectedEnd).toBe('number') + }) + + const segment = { + externalId: 'segment1', + name: 'Segment 1', + rank: 0, + parts: [], + } + + test('Can create segment', async () => { + const result = await ingestApi.postSegment({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segment, + }) + + expect(result).toBe(undefined) + }) + + test('Can update multiple segments', async () => { + const result = await ingestApi.putSegments({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segment: [segment], + }) + expect(result).toBe(undefined) + }) + + const updatedSegmentId = 'segment2' + test('Can update single segment', async () => { + const result = await ingestApi.putSegment({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: updatedSegmentId, + segment, + }) + expect(result).toBe(undefined) + }) + + test('Can delete multiple segments', async () => { + const result = await ingestApi.deleteSegments({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + }) + expect(result).toBe(undefined) + }) + + test('Can delete segment by id', async () => { + const result = await ingestApi.deleteSegment({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: updatedSegmentId, + }) + expect(result).toBe(undefined) + }) + + /** + * INGEST PARTS + */ + const partIds: string[] = [] + test('Can request all parts', async () => { + const parts = await ingestApi.getParts({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + }) + + expect(parts.length).toBeGreaterThanOrEqual(1) + + parts.forEach((part) => { + expect(typeof part).toBe('object') + expect(typeof part.externalId).toBe('string') + partIds.push(part.externalId) + }) + }) + + let newIngestPart: Part | undefined + test('Can request part by id', async () => { + const part = await ingestApi.getPart({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + partId: partIds[0], + }) + + expect(part).toHaveProperty('id') + expect(part).toHaveProperty('externalId') + expect(part).toHaveProperty('rundownId') + expect(part).toHaveProperty('segmentId') + expect(part).toHaveProperty('name') + expect(part).toHaveProperty('expectedDuration') + expect(part).toHaveProperty('autoNext') + expect(part).toHaveProperty('rank') + expect(typeof part.id).toBe('string') + expect(typeof part.externalId).toBe('string') + expect(typeof part.rundownId).toBe('string') + expect(typeof part.segmentId).toBe('string') + expect(typeof part.name).toBe('string') + expect(typeof part.expectedDuration).toBe('number') + expect(typeof part.autoNext).toBe('boolean') + expect(typeof part.rank).toBe('number') + newIngestPart = JSON.parse(JSON.stringify(part)) + }) + + test('Can create part', async () => { + const result = await ingestApi.postPart({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + part: { + externalId: 'part1', + name: 'Part 1', + rank: 0, + payload: { + type: 'CAMERA', + guest: true, + script: '', + pieces: [ + { + id: 'piece1', + objectType: 'CAMERA', + objectTime: '00:00:00:00', + duration: { + type: 'within-part', + duration: '00:00:10:00', + }, + resourceName: 'camera1', + label: 'Piece 1', + attributes: {}, + transition: 'cut', + transitionDuration: '00:00:00:00', + target: 'pgm', + }, + ], + }, + }, + }) + expect(result).toBe(undefined) + }) + + test('Can update multiple parts', async () => { + const result = await ingestApi.putParts({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + part: [ + { + externalId: 'part1', + name: 'Part 1', + rank: 0, + payload: { + type: 'CAMERA', + guest: true, + script: '', + pieces: [ + { + id: 'piece1', + label: 'Piece 1', + attributes: {}, + objectType: 'CAMERA', + resourceName: 'camera1', + }, + ], + }, + }, + ], + }) + expect(result).toBe(undefined) + }) + + const updatedPartId = 'part2' + test('Can update a part', async () => { + newIngestPart.name = newIngestPart.name + ' added' + const result = await ingestApi.putPart({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + partId: updatedPartId, + part: { + externalId: 'part1', + name: 'Part 1', + rank: 0, + payload: { + type: 'CAMERA', + guest: true, + script: '', + pieces: [ + { + id: 'piece1', + label: 'Piece 1', + attributes: {}, + objectType: 'CAMERA', + resourceName: 'camera1', + }, + ], + }, + }, + }) + expect(result).toBe(undefined) + }) + + test('Can delete multiple parts', async () => { + const result = await ingestApi.deleteParts({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + }) + expect(result).toBe(undefined) + }) + + test('Can delete part by id', async () => { + const result = await ingestApi.deletePart({ + studioId, + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + partId: updatedPartId, + }) + expect(result).toBe(undefined) + }) +}) diff --git a/packages/openapi/src/__tests__/playlists.spec.ts b/packages/openapi/src/__tests__/playlists.spec.ts index 4e5ed1d69ad..efa545be3a0 100644 --- a/packages/openapi/src/__tests__/playlists.spec.ts +++ b/packages/openapi/src/__tests__/playlists.spec.ts @@ -62,6 +62,18 @@ describe('Network client', () => { expect(active.status).toBe(200) }) + if (testServer) { + test('can activate adlib testing mode', async () => { + const active = await playlistsApi.activateAdlibTesting({ + playlistId: playlistIds[0], + rundownId: 'rundownId', + }) + expect(active.status).toBe(200) + }) + } else { + test.todo('activate adlib testing mode - need to read a rundown ID') + } + let partId = '' test('can move next part in a playlist', async () => { const move = await playlistsApi.moveNextPart({ diff --git a/packages/package.json b/packages/package.json index d0646cdcfe1..d6f10193687 100644 --- a/packages/package.json +++ b/packages/package.json @@ -30,7 +30,7 @@ "validate:dependencies": "yarn npm audit --environment production && run license-validate", "validate:dev-dependencies": "yarn npm audit --environment development", "license-validate": "sofie-licensecheck --allowPackages \"caniuse-lite@1.0.30001448;mos-gateway@$(node -p \"require('mos-gateway/package.json').version\");playout-gateway@$(node -p \"require('playout-gateway/package.json').version\");sofie-documentation@$(node -p \"require('sofie-documentation/package.json').version\");@sofie-automation/corelib@$(node -p \"require('@sofie-automation/corelib/package.json').version\");@sofie-automation/shared-lib@$(node -p \"require('@sofie-automation/shared-lib/package.json').version\");@sofie-automation/job-worker@$(node -p \"require('@sofie-automation/job-worker/package.json').version\");lunr-languages@1.10.0;live-status-gateway@$(node -p \"require('live-status-gateway/package.json').version\")\"", - "lint": "lerna run --concurrency 4 --stream lint", + "lint": "eslint", "unit": "lerna run --concurrency 2 --stream unit --ignore @sofie-automation/openapi -- --coverage=false", "test": "lerna run --concurrency 2 --stream test -- --coverage=false", "docs:typedoc": "typedoc .", @@ -41,35 +41,35 @@ "eslint": "cd $INIT_CWD && \"$PROJECT_CWD/node_modules/.bin/eslint\"" }, "devDependencies": { - "@babel/core": "^7.26.7", - "@babel/plugin-transform-modules-commonjs": "^7.26.3", - "@sofie-automation/code-standard-preset": "^3.0.0", - "@types/amqplib": "^0.10.6", + "@babel/core": "^7.29.0", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@sofie-automation/code-standard-preset": "^3.2.1", + "@types/amqplib": "0.10.6", "@types/debug": "^4.1.12", "@types/ejson": "^2.2.2", - "@types/got": "^9.6.12", - "@types/jest": "^29.5.14", - "@types/node": "^22.10.10", + "@types/jest": "^30.0.0", + "@types/node": "^22.19.8", "@types/object-path": "^0.11.4", "@types/underscore": "^1.13.0", - "babel-jest": "^29.7.0", + "babel-jest": "^30.2.0", "copyfiles": "^2.4.1", - "eslint": "^9.18.0", - "eslint-plugin-react": "^7.37.4", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "jest-mock-extended": "^3.0.7", - "json-schema-to-typescript": "^10.1.5", - "lerna": "^9.0.0", + "eslint": "^9.39.2", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-yml": "^3.1.2", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0", + "jest-mock-extended": "^4.0.0", + "json-schema-to-typescript": "^15.0.4", + "lerna": "^9.0.5", "nodemon": "^2.0.22", "open-cli": "^8.0.0", "pinst": "^3.0.0", - "prettier": "^3.4.2", - "rimraf": "^6.0.1", - "semver": "^7.6.3", - "ts-jest": "^29.2.5", + "prettier": "^3.8.1", + "rimraf": "^6.1.2", + "semver": "^7.7.3", + "ts-jest": "^29.4.6", "ts-node": "^10.9.2", - "typedoc": "^0.27.6", + "typedoc": "^0.27.9", "typescript": "~5.7.3" }, "name": "packages", @@ -88,5 +88,13 @@ "nx": { "built": true } + }, + "lint-staged": { + "*.{css,json,md,scss}": [ + "yarn run prettier" + ], + "*.{ts,tsx,js,jsx}": [ + "yarn lint" + ] } } diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index c77632bc668..fda39b3db18 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,49 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [26.3.0-2](https://github.com/Sofie-Automation/sofie-core/compare/v26.3.0-1...v26.3.0-2) (2026-02-18) + + +### Bug Fixes + +* Add missing 'rootDir' to tsconfig ([20e7d12](https://github.com/Sofie-Automation/sofie-core/commit/20e7d12aabdf4c9cf36f8a271435fce8aa253c2d)) + + + + + +# [26.3.0-1](https://github.com/Sofie-Automation/sofie-core/compare/v26.3.0-0...v26.3.0-1) (2026-02-11) + +**Note:** Version bump only for package playout-gateway + + + + + +# [26.3.0-0](https://github.com/Sofie-Automation/sofie-core/compare/v1.52.0...v26.3.0-0) (2026-02-04) + + +### Bug Fixes + +* docker images using CMD instead of ENTRYPOINT ([e1beb6e](https://github.com/Sofie-Automation/sofie-core/commit/e1beb6e082c7c9ce4a8009feceb58eb7ef89f308)) +* **PGW:** handle situation when device is not initialized yet ([6060e7e](https://github.com/Sofie-Automation/sofie-core/commit/6060e7e2645dfbc19fde263de35f545d9200e02c)) +* remove over-eager debug logging filtering from connectionManager ([#1594](https://github.com/Sofie-Automation/sofie-core/issues/1594)) ([462a27a](https://github.com/Sofie-Automation/sofie-core/commit/462a27a3c68176fbcf3c5ab3d22fa0f79037db1d)) +* update mos-connection for missing mosID bug fix ([#9](https://github.com/Sofie-Automation/sofie-core/issues/9)) ([e8e07e3](https://github.com/Sofie-Automation/sofie-core/commit/e8e07e3e86e0a6e4d1bb5802f0e782ad323f424e)) +* update tsr and remove deprecated playout-gateway methods ([#1525](https://github.com/Sofie-Automation/sofie-core/issues/1525)) ([5b9c7ad](https://github.com/Sofie-Automation/sofie-core/commit/5b9c7ad68375301722057ef4927bab13ce6896c1)) + + +### Features + +* add health endpoints to MOS- and Playout-Gateway ([5b590dd](https://github.com/Sofie-Automation/sofie-core/commit/5b590ddbaf86ee90d338837867a4d3bfc2e11c97)) +* add object to timeline to trigger a regeneration at point in time ([ad450c3](https://github.com/Sofie-Automation/sofie-core/commit/ad450c39ceef5fcf3373905dd6a55adf4dd9cbb6)) +* enable support for tsr plugins ([51a2379](https://github.com/Sofie-Automation/sofie-core/commit/51a237969092deda4972734e04e2aea01b78fe5a)) +* move GW config types to generated in shared lib ([f54d9ca](https://github.com/Sofie-Automation/sofie-core/commit/f54d9ca63bc00a05915aac45e0be5b595c980567)) +* update meteor to 3.3.2 ([#1529](https://github.com/Sofie-Automation/sofie-core/issues/1529)) ([9bd232e](https://github.com/Sofie-Automation/sofie-core/commit/9bd232e8f0561a46db8cc6143c5353d7fa531206)) + + + + + # [1.52.0](https://github.com/Sofie-Automation/sofie-core/compare/v1.52.0-in-testing.1...v1.52.0) (2025-06-30) **Note:** Version bump only for package playout-gateway diff --git a/packages/playout-gateway/eslint.config.mjs b/packages/playout-gateway/eslint.config.mjs deleted file mode 100644 index b9e5a88fd88..00000000000 --- a/packages/playout-gateway/eslint.config.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' - -export default generateEslintConfig({}) diff --git a/packages/playout-gateway/jest.config.js b/packages/playout-gateway/jest.config.js index 60dc56d5bf6..f57a00fcb8a 100644 --- a/packages/playout-gateway/jest.config.js +++ b/packages/playout-gateway/jest.config.js @@ -6,6 +6,11 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.json', + diagnostics: { + ignoreCodes: [ + 151002, // hybrid module kind (Node16/18/Next) + ], + }, }, ], }, diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 1c3033d736b..b73a05c2875 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -20,8 +20,7 @@ }, "contributors": [], "scripts": { - "lint:raw": "run -T eslint", - "lint": "run lint:raw .", + "lint": "run -T lint playout-gateway", "unit": "run -T jest", "test": "run lint && run unit", "watch": "run -T jest --watch", @@ -52,22 +51,14 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "1.53.0-in-development", - "@sofie-automation/shared-lib": "1.53.0-in-development", - "debug": "^4.4.0", - "influx": "^5.9.7", - "timeline-state-resolver": "10.0.0-nightly-release53-20251217-143607-df590aa96.0", + "@sofie-automation/server-core-integration": "26.3.0-2", + "@sofie-automation/shared-lib": "26.3.0-2", + "debug": "^4.4.3", + "influx": "^5.12.0", + "timeline-state-resolver": "10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0", "tslib": "^2.8.1", "underscore": "^1.13.7", - "winston": "^3.17.0" - }, - "lint-staged": { - "*.{css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx,js,jsx}": [ - "yarn lint:raw" - ] + "winston": "^3.19.0" }, "packageManager": "yarn@4.12.0" } diff --git a/packages/playout-gateway/src/atemUploader.ts b/packages/playout-gateway/src/atemUploader.ts index 41357de9181..676a3496300 100644 --- a/packages/playout-gateway/src/atemUploader.ts +++ b/packages/playout-gateway/src/atemUploader.ts @@ -1,5 +1,5 @@ /* eslint-disable n/no-process-exit */ -// eslint-disable-next-line n/no-extraneous-import + import { Atem } from 'atem-connection' import * as fs from 'fs' import { AtemMediaPoolAsset, AtemMediaPoolType } from 'timeline-state-resolver' diff --git a/packages/playout-gateway/src/coreHandler.ts b/packages/playout-gateway/src/coreHandler.ts index 70cefba706d..fe6cbdecf24 100644 --- a/packages/playout-gateway/src/coreHandler.ts +++ b/packages/playout-gateway/src/coreHandler.ts @@ -18,7 +18,7 @@ import _ from 'underscore' import { DeviceConfig } from './connector.js' import { TSRHandler } from './tsrHandler.js' import { Logger } from 'winston' -// eslint-disable-next-line n/no-extraneous-import + import { MemUsageReport as ThreadMemUsageReport } from 'threadedclass' import { compilePlayoutGatewayConfigManifest } from './configManifest.js' import { BaseRemoteDeviceIntegration } from 'timeline-state-resolver/dist/service/remoteDeviceInstance' diff --git a/packages/playout-gateway/src/tsrHandler.ts b/packages/playout-gateway/src/tsrHandler.ts index 6faab8bc61d..617f3c96b66 100644 --- a/packages/playout-gateway/src/tsrHandler.ts +++ b/packages/playout-gateway/src/tsrHandler.ts @@ -56,8 +56,9 @@ export interface TSRConfig {} // ---------------------------------------------------------------------------- -export interface TimelineContentObjectTmp - extends TSRTimelineObj { +export interface TimelineContentObjectTmp< + TContent extends { deviceType: DeviceType }, +> extends TSRTimelineObj { inGroup?: string } diff --git a/packages/playout-gateway/src/versions.ts b/packages/playout-gateway/src/versions.ts index f97f09b11ed..df533046104 100644 --- a/packages/playout-gateway/src/versions.ts +++ b/packages/playout-gateway/src/versions.ts @@ -4,9 +4,8 @@ import * as Winston from 'winston' export function getVersions(logger: Winston.Logger): { [packageName: string]: string } { const versions: { [packageName: string]: string } = {} - if (process.env.npm_package_version) { - versions['_process'] = process.env.npm_package_version - } + const pkg = require('../package.json') + if (pkg?.version) versions['_process'] = pkg.version const pkgNames = [ 'timeline-state-resolver', diff --git a/packages/playout-gateway/tsconfig.build.json b/packages/playout-gateway/tsconfig.build.json index 14692f9baf2..20e0ac52be7 100644 --- a/packages/playout-gateway/tsconfig.build.json +++ b/packages/playout-gateway/tsconfig.build.json @@ -1,9 +1,10 @@ { "extends": "@sofie-automation/code-standard-preset/ts/tsconfig.bin", - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.json"], "exclude": ["node_modules/**", "**/*spec.ts", "**/__tests__/*", "**/__mocks__/*"], "compilerOptions": { "outDir": "./dist", + "rootDir": "./src", "baseUrl": "./", "paths": { "*": ["./node_modules/*"] @@ -11,7 +12,9 @@ "types": ["node"], // TSR throws some typings issues "skipLibCheck": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "declaration": true, + "composite": true }, "references": [ // diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index 5853b34a212..86e74dfbdcd 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,39 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [26.3.0-2](https://github.com/Sofie-Automation/sofie-core/compare/v26.3.0-1...v26.3.0-2) (2026-02-18) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + +# [26.3.0-1](https://github.com/Sofie-Automation/sofie-core/compare/v26.3.0-0...v26.3.0-1) (2026-02-11) + +**Note:** Version bump only for package @sofie-automation/server-core-integration + + + + + +# [26.3.0-0](https://github.com/Sofie-Automation/sofie-core/compare/v1.52.0...v26.3.0-0) (2026-02-04) + + +### Bug Fixes + +* **core-integration:** use setMaxListeners on CoreConnection to avoid MaxListenersExceededWarning message ([a02ef23](https://github.com/Sofie-Automation/sofie-core/commit/a02ef236b8a396847bc467ccd5f459a0862e6abe)) + + +### Features + +* add health endpoints to MOS- and Playout-Gateway ([5b590dd](https://github.com/Sofie-Automation/sofie-core/commit/5b590ddbaf86ee90d338837867a4d3bfc2e11c97)) +* update meteor to 3.3.2 ([#1529](https://github.com/Sofie-Automation/sofie-core/issues/1529)) ([9bd232e](https://github.com/Sofie-Automation/sofie-core/commit/9bd232e8f0561a46db8cc6143c5353d7fa531206)) + + + + + # [1.52.0](https://github.com/Sofie-Automation/sofie-core/compare/v1.52.0-in-testing.1...v1.52.0) (2025-06-30) **Note:** Version bump only for package @sofie-automation/server-core-integration diff --git a/packages/server-core-integration/eslint.config.mjs b/packages/server-core-integration/eslint.config.mjs deleted file mode 100644 index b9e5a88fd88..00000000000 --- a/packages/server-core-integration/eslint.config.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' - -export default generateEslintConfig({}) diff --git a/packages/server-core-integration/jest.config.js b/packages/server-core-integration/jest.config.js index c1389299dc6..660eb87a241 100644 --- a/packages/server-core-integration/jest.config.js +++ b/packages/server-core-integration/jest.config.js @@ -6,6 +6,11 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.json', + diagnostics: { + ignoreCodes: [ + 151002, // hybrid module kind (Node16/18/Next) + ], + }, }, ], }, diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 5670ac9d608..ff8e3f3ed96 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/server-core-integration", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -32,8 +32,7 @@ } ], "scripts": { - "lint:raw": "run -T eslint", - "lint": "run lint:raw .", + "lint": "run -T lint server-core-integration", "unit": "run -T jest", "test": "run lint && run unit", "test:integration": "run lint && run -T jest --config=jest-integration.config.js", @@ -68,27 +67,19 @@ "production" ], "devDependencies": { - "@types/koa": "^3.0.0", - "@types/koa__router": "^12.0.4" + "@types/koa": "^3.0.1", + "@types/koa__router": "^12.0.5" }, "dependencies": { - "@koa/router": "^14.0.0", - "@sofie-automation/shared-lib": "1.53.0-in-development", + "@koa/router": "^15.3.0", + "@sofie-automation/shared-lib": "26.3.0-2", "ejson": "^2.2.3", "faye-websocket": "^0.11.4", "got": "^11.8.6", - "koa": "^3.0.1", + "koa": "^3.1.1", "tslib": "^2.8.1", "underscore": "^1.13.7" }, - "lint-staged": { - "*.{js,css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx}": [ - "yarn lint:raw" - ] - }, "publishConfig": { "access": "public" }, diff --git a/packages/shared-lib/eslint.config.mjs b/packages/shared-lib/eslint.config.mjs deleted file mode 100644 index b9e5a88fd88..00000000000 --- a/packages/shared-lib/eslint.config.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' - -export default generateEslintConfig({}) diff --git a/packages/shared-lib/jest.config.js b/packages/shared-lib/jest.config.js index 76ff2b14f1e..04b8ea8dd1c 100644 --- a/packages/shared-lib/jest.config.js +++ b/packages/shared-lib/jest.config.js @@ -6,6 +6,11 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.json', + diagnostics: { + ignoreCodes: [ + 151002, // hybrid module kind (Node16/18/Next) + ], + }, }, ], }, diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 998995392cb..32730339cba 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "1.53.0-in-development", + "version": "26.3.0-2", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -15,8 +15,7 @@ }, "homepage": "https://github.com/nrkno/sofie-core/blob/master/packages/shared-lib#readme", "scripts": { - "lint:raw": "run -T eslint", - "lint": "run lint:raw .", + "lint": "run -T lint shared-lib", "unit": "run -T jest", "test": "run lint && run unit", "watch": "run -T jest --watch", @@ -38,19 +37,11 @@ "dependencies": { "@mos-connection/model": "^5.0.0-alpha.0", "kairos-lib": "^0.2.3", - "timeline-state-resolver-types": "10.0.0-nightly-release53-20251217-143607-df590aa96.0", + "timeline-state-resolver-types": "10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0", "tslib": "^2.8.1", - "type-fest": "^4.33.0" + "type-fest": "^4.41.0" }, "prettier": "@sofie-automation/code-standard-preset/.prettierrc.json", - "lint-staged": { - "*.{js,css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx}": [ - "yarn lint:raw" - ] - }, "publishConfig": { "access": "public" }, diff --git a/packages/shared-lib/src/core/model/PackageContainer.ts b/packages/shared-lib/src/core/model/PackageContainer.ts index 9283ed32d3a..27ce8d86a34 100644 --- a/packages/shared-lib/src/core/model/PackageContainer.ts +++ b/packages/shared-lib/src/core/model/PackageContainer.ts @@ -5,3 +5,8 @@ export interface StudioPackageContainer { deviceIds: string[] container: PackageContainer } + +export interface StudioPackageContainerSettings { + previewContainerIds: string[] + thumbnailContainerIds: string[] +} diff --git a/packages/shared-lib/src/core/model/Timeline.ts b/packages/shared-lib/src/core/model/Timeline.ts index 3f2a70c04ff..1a6231cc189 100644 --- a/packages/shared-lib/src/core/model/Timeline.ts +++ b/packages/shared-lib/src/core/model/Timeline.ts @@ -34,6 +34,14 @@ export enum TimelineObjHoldMode { /** The object is played when NOT doing a Hold */ EXCEPT = 2, } +export enum TimelineObjOnAirMode { + /** Default: The object is played as usual (behaviour is not affected by rehearsal/on-air state) */ + ALWAYS = 0, + /** The object is played ONLY when in Rehearsal */ + REHEARSAL = 1, + /** The object is played ONLY when onair */ + ONAIR = 2, +} export interface TimelineObjectCoreExt< TContent extends { deviceType: TSR.DeviceTypeExt }, @@ -47,6 +55,8 @@ export interface TimelineObjectCoreExt< /** Restrict object usage according to whether we are currently in a hold */ holdMode?: TimelineObjHoldMode + /** Restrict object usage according to whether we are currently in rehearsal or on-air */ + onAirMode?: TimelineObjOnAirMode /** Arbitrary data storage for plugins */ metaData?: TMetadata /** Keyframes: Arbitrary data storage for plugins */ @@ -133,8 +143,9 @@ export enum LookaheadMode { export interface BlueprintMappings extends TSR.Mappings { [layerName: string]: BlueprintMapping } -export interface BlueprintMapping - extends TSR.Mapping { +export interface BlueprintMapping< + TOptions extends { mappingType: string } | unknown = TSR.TSRMappingOptions, +> extends TSR.Mapping { /** What method core should use to create lookahead objects for this layer */ lookahead: LookaheadMode /** How many lookahead objects to create for this layer. Default = 1 */ @@ -146,8 +157,10 @@ export interface BlueprintMapping - extends Omit, 'deviceId'> { +export interface MappingExt extends Omit< + BlueprintMapping, + 'deviceId' +> { deviceId: PeripheralDeviceId } export interface RoutedMappings { diff --git a/packages/shared-lib/src/lib/JSONSchemaTypes.ts b/packages/shared-lib/src/lib/JSONSchemaTypes.ts index 476cfb51498..caa10e064f0 100644 --- a/packages/shared-lib/src/lib/JSONSchemaTypes.ts +++ b/packages/shared-lib/src/lib/JSONSchemaTypes.ts @@ -33,9 +33,8 @@ * POSSIBILITY OF SUCH DAMAGE. */ -// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion export const draft = '2020-12' as const -// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + export const $schema = 'https://json-schema.org/draft/2020-12/schema' as const type MaybeReadonlyArray = Array | ReadonlyArray diff --git a/packages/shared-lib/src/peripheralDevice/ingest.ts b/packages/shared-lib/src/peripheralDevice/ingest.ts index c53739f87e6..2ee1c810c1f 100644 --- a/packages/shared-lib/src/peripheralDevice/ingest.ts +++ b/packages/shared-lib/src/peripheralDevice/ingest.ts @@ -18,6 +18,9 @@ export interface IngestRundown[] + + /** Id of the playlist this rundown belongs to */ + playlistExternalId?: string } export interface IngestSegment { /** Id of the segment as reported by the ingest gateway. Must be unique for each segment in the rundown */ diff --git a/packages/tsconfig.build.json b/packages/tsconfig.build.json index ffb9abd079e..2b155543fa0 100644 --- a/packages/tsconfig.build.json +++ b/packages/tsconfig.build.json @@ -1,6 +1,7 @@ { "files": [], "references": [ + { "path": "./tsconfig.test.json" }, { "path": "./blueprints-integration/tsconfig.build.json" }, { "path": "./server-core-integration/tsconfig.build.json" }, { "path": "./mos-gateway/tsconfig.build.json" }, diff --git a/packages/tsconfig.json b/packages/tsconfig.json index e3041c036f7..06322e93c1f 100644 --- a/packages/tsconfig.json +++ b/packages/tsconfig.json @@ -1,6 +1,7 @@ { "files": [], "references": [ + { "path": "./tsconfig.test.json" }, { "path": "./blueprints-integration/tsconfig.json" }, { "path": "./server-core-integration/tsconfig.json" }, { "path": "./mos-gateway/tsconfig.json" }, diff --git a/packages/tsconfig.test.json b/packages/tsconfig.test.json new file mode 100644 index 00000000000..09ae33dd197 --- /dev/null +++ b/packages/tsconfig.test.json @@ -0,0 +1,25 @@ +{ + "extends": "@sofie-automation/code-standard-preset/ts/tsconfig.lib", + "include": ["*/src/**/__tests__/**/*", "*/src/**/__mocks__/**/*", "*/src/**/integrationTests/**/*"], + "exclude": ["node_modules/**", "webui/**", "openapi/**"], + "compilerOptions": { + "baseUrl": "./", + "resolveJsonModule": true, + "types": ["jest", "node"], + "composite": true, + "noEmit": true, + "skipLibCheck": true, + "importHelpers": false // To mitigate tslib errors which are meaningless + }, + "references": [ + { "path": "./blueprints-integration/tsconfig.build.json" }, + { "path": "./server-core-integration/tsconfig.build.json" }, + { "path": "./mos-gateway/tsconfig.build.json" }, + { "path": "./playout-gateway/tsconfig.build.json" }, + { "path": "./job-worker/tsconfig.build.json" }, + { "path": "./corelib/tsconfig.build.json" }, + { "path": "./shared-lib/tsconfig.build.json" }, + { "path": "./meteor-lib/tsconfig.build.json" }, + { "path": "./live-status-gateway/tsconfig.build.json" } + ] +} diff --git a/packages/webui/jest.config.cjs b/packages/webui/jest.config.cjs index 4132ac19b17..c9d553b42a6 100644 --- a/packages/webui/jest.config.cjs +++ b/packages/webui/jest.config.cjs @@ -16,6 +16,11 @@ module.exports = { 'ts-jest', { tsconfig: 'tsconfig.jest.json', + diagnostics: { + ignoreCodes: [ + 151002, // hybrid module kind (Node16/18/Next) + ], + }, }, ], '^.+\\.(js|jsx)$': ['babel-jest', { presets: ['@babel/preset-env'] }], diff --git a/packages/webui/package.json b/packages/webui/package.json index b736f7e979c..575bf57f441 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -1,7 +1,7 @@ { "name": "@sofie-automation/webui", "private": true, - "version": "1.53.0-in-development", + "version": "26.3.0-2", "type": "module", "license": "MIT", "repository": { @@ -14,12 +14,11 @@ }, "homepage": "https://github.com/nrkno/sofie-core/blob/main/packages/webui#readme", "scripts": { + "lint": "run -T lint webui", "dev": "vite --port=3005 --force", "build": "tsc -b && vite build", "check-types": "tsc -p tsconfig.app.json --noEmit", "preview": "vite preview", - "lint:raw": "run -T eslint", - "lint": "run lint:raw .", "unit": "run -T jest", "test": "run lint && run unit", "watch": "run -T jest --watch", @@ -31,86 +30,79 @@ "license-validate": "run -T sofie-licensecheck" }, "dependencies": { - "@crello/react-lottie": "0.0.9", - "@fortawesome/fontawesome-free": "^6.7.2", - "@fortawesome/fontawesome-svg-core": "^6.7.2", - "@fortawesome/free-solid-svg-icons": "^6.7.2", - "@fortawesome/react-fontawesome": "^0.2.2", - "@jstarpl/react-contextmenu": "^2.15.1", - "@nrk/core-icons": "^9.6.0", + "@fortawesome/fontawesome-free": "^7.1.0", + "@fortawesome/fontawesome-svg-core": "^7.1.0", + "@fortawesome/free-solid-svg-icons": "^7.1.0", + "@fortawesome/react-fontawesome": "^3.1.1", + "@jstarpl/react-contextmenu": "^2.15.3", "@popperjs/core": "^2.11.8", - "@sofie-automation/blueprints-integration": "1.53.0-in-development", - "@sofie-automation/corelib": "1.53.0-in-development", - "@sofie-automation/meteor-lib": "1.53.0-in-development", - "@sofie-automation/shared-lib": "1.53.0-in-development", + "@sofie-automation/blueprints-integration": "26.3.0-2", + "@sofie-automation/corelib": "26.3.0-2", + "@sofie-automation/meteor-lib": "26.3.0-2", + "@sofie-automation/shared-lib": "26.3.0-2", "@sofie-automation/sorensen": "^1.5.11", "@testing-library/user-event": "^14.6.1", - "@types/sinon": "^10.0.20", - "bootstrap": "^5.3.3", + "bootstrap": "^5.3.8", "classnames": "^2.5.1", "cubic-spline": "^3.0.3", "deep-extend": "0.6.0", "ejson": "^2.2.3", "i18next": "^21.10.0", - "i18next-browser-languagedetector": "^6.1.8", - "i18next-http-backend": "^1.4.5", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "immutability-helper": "^3.1.1", - "lottie-web": "^5.12.2", + "lottie-react": "^2.4.1", "moment": "^2.30.1", - "motion": "^12.4.7", + "motion": "^12.31.0", "promise.allsettled": "^1.0.7", "query-string": "^6.14.1", "rc-tooltip": "^6.4.0", "react": "^18.3.1", - "react-bootstrap": "^2.10.9", - "react-circular-progressbar": "^2.1.0", - "react-datepicker": "^3.8.0", + "react-bootstrap": "^2.10.10", + "react-datepicker": "^9.1.0", "react-dnd": "^14.0.5", "react-dnd-html5-backend": "^14.1.0", "react-dom": "^18.3.1", "react-focus-bounder": "^1.1.6", "react-hotkeys": "^2.0.0", "react-i18next": "^11.18.6", - "react-intersection-observer": "^9.15.1", - "react-moment": "^0.9.7", + "react-intersection-observer": "^9.16.0", + "react-moment": "^1.2.1", "react-popper": "^2.3.0", "react-router-bootstrap": "^0.25.0", "react-router-dom": "^5.3.4", - "semver": "^7.6.3", - "sha.js": "^2.4.11", - "shuttle-webhid": "^0.0.2", - "type-fest": "^4.33.0", + "semver": "^7.7.3", + "sha.js": "^2.4.12", + "shuttle-webhid": "^0.1.3", + "type-fest": "^4.41.0", "underscore": "^1.13.7", "webmidi": "^2.5.3", "xmlbuilder": "^15.1.1" }, "devDependencies": { - "@babel/preset-env": "^7.26.7", - "@testing-library/dom": "^10.4.0", - "@testing-library/jest-dom": "^6.6.3", - "@testing-library/react": "^16.2.0", - "@types/bootstrap": "^5", + "@babel/preset-env": "^7.29.0", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/bootstrap": "^5.2.10", "@types/classnames": "^2.3.4", "@types/deep-extend": "^0.6.2", - "@types/react": "^18.3.18", - "@types/react-circular-progressbar": "^1.1.0", - "@types/react-datepicker": "^3.1.8", - "@types/react-dom": "^18.3.5", + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.7", "@types/react-router": "^5.1.20", - "@types/react-router-bootstrap": "^0", + "@types/react-router-bootstrap": "^0.26.8", "@types/react-router-dom": "^5.3.3", "@types/sha.js": "^2.4.4", "@types/xml2js": "^0.4.14", - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^5.1.3", "@welldone-software/why-did-you-render": "^4.3.2", - "@xmldom/xmldom": "^0.8.10", - "babel-jest": "^29.7.0", - "globals": "^15.14.0", - "sass": "^1.83.4", - "sinon": "^14.0.2", + "@xmldom/xmldom": "^0.8.11", + "babel-jest": "^30.2.0", + "globals": "^17.3.0", + "sass-embedded": "^1.97.3", "typescript": "~5.7.3", - "vite": "^6.0.11", - "vite-plugin-node-polyfills": "^0.23.0", + "vite": "^7.3.1", + "vite-plugin-node-polyfills": "^0.25.0", "vite-tsconfig-paths": "^5.1.4", "xml2js": "^0.6.2" }, diff --git a/packages/webui/src/__mocks__/defaultCollectionObjects.ts b/packages/webui/src/__mocks__/defaultCollectionObjects.ts index 7434f499fbf..3df301315a4 100644 --- a/packages/webui/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/webui/src/__mocks__/defaultCollectionObjects.ts @@ -48,6 +48,11 @@ export function defaultRundownPlaylist(_id: RundownPlaylistId, studioId: StudioI type: 'none' as any, }, rundownIdsInOrder: [], + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], } } export function defaultRundown( @@ -111,8 +116,10 @@ export function defaultStudio(_id: StudioId): DBStudio { routeSetsWithOverrides: wrapDefaultObject({}), routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}), packageContainersWithOverrides: wrapDefaultObject({}), - previewContainerIds: [], - thumbnailContainerIds: [], + packageContainerSettingsWithOverrides: wrapDefaultObject({ + previewContainerIds: [], + thumbnailContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: wrapDefaultObject({}), playoutDevices: wrapDefaultObject({}), diff --git a/packages/webui/src/client/collections/lib.ts b/packages/webui/src/client/collections/lib.ts index 910a85d1416..0b0aa7d8ef6 100644 --- a/packages/webui/src/client/collections/lib.ts +++ b/packages/webui/src/client/collections/lib.ts @@ -151,9 +151,9 @@ export function createSyncPeripheralDeviceCustomPublicationMongoCollection< return wrapped } -class WrappedMongoReadOnlyCollection }> - implements MongoReadOnlyCollection -{ +class WrappedMongoReadOnlyCollection< + DBInterface extends { _id: ProtectedString }, +> implements MongoReadOnlyCollection { protected readonly _collection: Mongo.Collection public readonly name: string | null diff --git a/packages/webui/src/client/lib/Components/BreadCrumbTextInput.tsx b/packages/webui/src/client/lib/Components/BreadCrumbTextInput.tsx index 25001a9e4ca..6113e4b6e52 100644 --- a/packages/webui/src/client/lib/Components/BreadCrumbTextInput.tsx +++ b/packages/webui/src/client/lib/Components/BreadCrumbTextInput.tsx @@ -330,8 +330,10 @@ export function BreadCrumbTextInput({ ) } -interface ICombinedMultiLineTextInputControlProps - extends Omit { +interface ICombinedMultiLineTextInputControlProps extends Omit< + IBreadCrumbTextInputControlProps, + 'value' | 'handleUpdate' +> { value: string handleUpdate: (value: string) => void } diff --git a/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx b/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx index b6aa6a0983b..c2fef23781c 100644 --- a/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx +++ b/packages/webui/src/client/lib/Components/MultiLineTextInput.tsx @@ -86,8 +86,10 @@ export function MultiLineTextInputControl({ ) } -interface ICombinedMultiLineTextInputControlProps - extends Omit { +interface ICombinedMultiLineTextInputControlProps extends Omit< + IMultiLineTextInputControlProps, + 'value' | 'handleUpdate' +> { value: string handleUpdate: (value: string) => void } diff --git a/packages/webui/src/client/lib/LottieButton.tsx b/packages/webui/src/client/lib/LottieButton.tsx index 80988e19c0c..85103eba9c4 100644 --- a/packages/webui/src/client/lib/LottieButton.tsx +++ b/packages/webui/src/client/lib/LottieButton.tsx @@ -1,6 +1,6 @@ import * as React from 'react' -import { Lottie } from '@crello/react-lottie' +import Lottie, { LottieComponentProps } from 'lottie-react' interface IProps { inAnimation?: any @@ -26,8 +26,8 @@ export class LottieButton extends React.Component - + {this.props.children}
- + diff --git a/packages/webui/src/client/lib/__tests__/__snapshots__/rundown.test.ts.snap b/packages/webui/src/client/lib/__tests__/__snapshots__/rundown.test.ts.snap index 9af78812d9e..e603ce6e42b 100644 --- a/packages/webui/src/client/lib/__tests__/__snapshots__/rundown.test.ts.snap +++ b/packages/webui/src/client/lib/__tests__/__snapshots__/rundown.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing exports[`client/lib/rundown RundownUtils.getResolvedSegment Basic Segment resolution 1`] = ` { diff --git a/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts b/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts index 8e402449d98..a7cabd427ea 100644 --- a/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts +++ b/packages/webui/src/client/lib/__tests__/rundownTiming.test.ts @@ -28,6 +28,12 @@ function makeMockPlaylist(): DBRundownPlaylist { type: PlaylistTimingType.None, }, rundownIdsInOrder: [], + + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], }) } diff --git a/packages/webui/src/client/lib/lib.tsx b/packages/webui/src/client/lib/lib.tsx index aa697f2c14e..20c0f2a1db9 100644 --- a/packages/webui/src/client/lib/lib.tsx +++ b/packages/webui/src/client/lib/lib.tsx @@ -11,13 +11,7 @@ import RundownViewEventBus, { RundownViewEvents, } from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' -export { multilineText, isEventInInputField } - -function multilineText(txt: string): React.ReactNode { - return _.map((txt + '').split('\n'), (line: string, i) => { - return

{line}

- }) -} +export { isEventInInputField } function isEventInInputField(e: Event): boolean { // @ts-expect-error localName diff --git a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx index b1ea5bb7f40..82d8e8bce15 100644 --- a/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx +++ b/packages/webui/src/client/lib/notifications/NotificationCenterPanel.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import * as CoreIcon from '@nrk/core-icons/jsx' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import ClassNames from 'classnames' import { motion, AnimatePresence, HTMLMotionProps } from 'motion/react' import { translateWithTracker, Translated, useTracker } from '../ReactMeteorData/ReactMeteorData.js' @@ -88,12 +88,7 @@ class NotificationPopUp extends React.Component { className="btn btn-default notification-pop-up__actions--button" onClick={(e) => this.triggerEvent(defaultAction, e)} > - + {defaultAction.label}
@@ -133,7 +128,7 @@ class NotificationPopUp extends React.Component { }} aria-label={i18nTranslator('Dismiss')} > - {this.props.item.persistent ? : } + {this.props.item.persistent ? : } )} diff --git a/packages/webui/src/client/lib/rundownPlaylistUtil.ts b/packages/webui/src/client/lib/rundownPlaylistUtil.ts index 4721dff60d4..363bf841870 100644 --- a/packages/webui/src/client/lib/rundownPlaylistUtil.ts +++ b/packages/webui/src/client/lib/rundownPlaylistUtil.ts @@ -96,6 +96,7 @@ export class RundownPlaylistClientUtil { currentPartInstance: PartInstance | undefined nextPartInstance: PartInstance | undefined previousPartInstance: PartInstance | undefined + partInstanceToCountTimeFrom: PartInstance | undefined } { let unorderedRundownIds = rundownIds0 if (!unorderedRundownIds) { @@ -116,10 +117,26 @@ export class RundownPlaylistClientUtil { }).fetch() : [] + const areAllPartsTimed = !!UIPartInstances.findOne({ + rundownId: { $in: unorderedRundownIds }, + ['part.untimed']: { $ne: true }, + }) + + const partInstanceToCountTimeFrom = UIPartInstances.findOne( + { + rundownId: { $in: unorderedRundownIds }, + reset: { $ne: true }, + takeCount: { $exists: true }, + ['part.untimed']: { $ne: areAllPartsTimed }, + }, + { sort: { takeCount: 1 } } + ) + return { currentPartInstance: instances.find((inst) => inst._id === playlist.currentPartInfo?.partInstanceId), nextPartInstance: instances.find((inst) => inst._id === playlist.nextPartInfo?.partInstanceId), previousPartInstance: instances.find((inst) => inst._id === playlist.previousPartInfo?.partInstanceId), + partInstanceToCountTimeFrom, } } diff --git a/packages/webui/src/client/lib/rundownTiming.ts b/packages/webui/src/client/lib/rundownTiming.ts index 77d5716b9b7..a273b072ed2 100644 --- a/packages/webui/src/client/lib/rundownTiming.ts +++ b/packages/webui/src/client/lib/rundownTiming.ts @@ -25,7 +25,8 @@ import { Settings } from '../lib/Settings.js' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' import { CountdownType } from '@sofie-automation/blueprints-integration' -import { isLoopDefined, isEntirePlaylistLooping, isLoopRunning } from '../lib/RundownResolver.js' +import { isLoopDefined, isEntirePlaylistLooping, isLoopRunning, PartExtended } from '../lib/RundownResolver.js' +import { RundownUtils } from './rundown.js' // Minimum duration that a part can be assigned. Used by gap parts to allow them to "compress" to indicate time running out. const MINIMAL_NONZERO_DURATION = 1 @@ -166,12 +167,17 @@ export class RundownTimingCalculator { const liveSegment = segmentsMap.get(liveSegmentIds.segmentId) if (liveSegment?.segmentTiming?.countdownType === CountdownType.SEGMENT_BUDGET_DURATION) { - remainingBudgetOnCurrentSegment = - (playlist.segmentsStartedPlayback?.[unprotectString(liveSegmentIds.segmentPlayoutId)] ?? - lastStartedPlayback ?? - now) + - (liveSegment.segmentTiming.budgetDuration ?? 0) - - now + const budgetDuration = liveSegment.segmentTiming.budgetDuration ?? 0 + if (budgetDuration > 0) { + remainingBudgetOnCurrentSegment = + (playlist.segmentsStartedPlayback?.[ + unprotectString(liveSegmentIds.segmentPlayoutId) + ] ?? + lastStartedPlayback ?? + now) + + budgetDuration - + now + } } } segmentDisplayDuration = 0 @@ -784,23 +790,21 @@ export interface RundownTimingContext { */ export function computeSegmentDuration( timingDurations: RundownTimingContext, - partIds: PartId[], + parts: PartExtended[], display?: boolean ): number { - const partDurations = timingDurations.partDurations - - if (partDurations === undefined) return 0 - - return partIds.reduce((memo, partId) => { - const pId = unprotectString(partId) - let partDuration = 0 - if (partDurations && partDurations[pId] !== undefined) { - partDuration = partDurations[pId] - } - if (!partDuration && display) { - partDuration = Settings.defaultDisplayDuration - } - return memo + partDuration + const partDisplayDurations = timingDurations?.partDisplayDurations + + if (!partDisplayDurations) return RundownUtils.getSegmentDuration(parts, display) + + return parts.reduce((memo, partExtended) => { + // total += durations.partDurations ? durations.partDurations[item._id] : (item.duration || item.renderedDuration || 1) + const partInstanceTimingId = getPartInstanceTimingId(partExtended.instance) + const duration = Math.max( + partExtended.instance.timings?.duration || partExtended.renderedDuration || 0, + partDisplayDurations?.[partInstanceTimingId] || (display ? Settings.defaultDisplayDuration : 0) + ) + return memo + duration }, 0) } diff --git a/packages/webui/src/client/lib/tTimerUtils.ts b/packages/webui/src/client/lib/tTimerUtils.ts new file mode 100644 index 00000000000..637c8d75e54 --- /dev/null +++ b/packages/webui/src/client/lib/tTimerUtils.ts @@ -0,0 +1,51 @@ +import { RundownTTimer, timerStateToDuration } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' + +/** + * Calculate the display diff for a T-Timer. + * For countdown/timeOfDay: positive = time remaining, negative = overrun. + * For freeRun: positive = elapsed time. + */ +export function calculateTTimerDiff(timer: RundownTTimer, now: number): number { + if (!timer.state) { + return 0 + } + + // Get current time: either frozen duration or calculated from zeroTime + const currentDuration = timerStateToDuration(timer.state, now) + + // Free run counts up, so negate to get positive elapsed time + if (timer.mode?.type === 'freeRun') { + return -currentDuration + } + + // Apply stopAtZero if configured + if (timer.mode?.stopAtZero && currentDuration < 0) { + return 0 + } + + return currentDuration +} + +/** + * Calculate the over/under difference between the timer's current value + * and its projected time. + * + * Positive = over (behind schedule, will reach anchor after timer hits zero) + * Negative = under (ahead of schedule, will reach anchor before timer hits zero) + * + * Returns undefined if no projection is available. + */ +export function calculateTTimerOverUnder(timer: RundownTTimer, now: number): number | undefined { + if (!timer.state || !timer.projectedState) { + return undefined + } + + const duration = timerStateToDuration(timer.state, now) + const projectedDuration = timerStateToDuration(timer.projectedState, now) + + return projectedDuration - duration +} + +export function getDefaultTTimer(tTimers: [RundownTTimer, RundownTTimer, RundownTTimer]): RundownTTimer | undefined { + return tTimers.find((t) => t.mode) +} diff --git a/packages/webui/src/client/lib/triggers/TriggersHandler.tsx b/packages/webui/src/client/lib/triggers/TriggersHandler.tsx index c9b7e8e3635..dc67786e43b 100644 --- a/packages/webui/src/client/lib/triggers/TriggersHandler.tsx +++ b/packages/webui/src/client/lib/triggers/TriggersHandler.tsx @@ -323,8 +323,15 @@ export const TriggersHandler: React.FC = function TriggersHandler( activationId: 1, nextPartInfo: 1, currentPartInfo: 1, + studioId: 1, + rehearsal: 1, }, - }) as Pick | undefined + }) as + | Pick< + DBRundownPlaylist, + '_id' | 'name' | 'activationId' | 'studioId' | 'rehearsal' | 'nextPartInfo' | 'currentPartInfo' + > + | undefined if (playlist) { let context = rundownPlaylistContext.get() if (context === null) { diff --git a/packages/webui/src/client/lib/triggers/triggersContext.ts b/packages/webui/src/client/lib/triggers/triggersContext.ts index ff4a71de61a..4499a7a8db0 100644 --- a/packages/webui/src/client/lib/triggers/triggersContext.ts +++ b/packages/webui/src/client/lib/triggers/triggersContext.ts @@ -28,9 +28,9 @@ import { TriggerReactiveVar } from '@sofie-automation/meteor-lib/dist/triggers/r import { FindOptions, MongoQuery } from '@sofie-automation/corelib/dist/mongo' import { memoizedIsolatedAutorunAsync } from '../memoizedIsolatedAutorun.js' -class UiTriggersCollectionWrapper }> - implements TriggersAsyncCollection -{ +class UiTriggersCollectionWrapper< + DBInterface extends { _id: ProtectedString }, +> implements TriggersAsyncCollection { readonly #collection: MongoReadOnlyCollection constructor(collection: MongoReadOnlyCollection) { diff --git a/packages/webui/src/client/lib/ui/containers/modals/Modal.tsx b/packages/webui/src/client/lib/ui/containers/modals/Modal.tsx index 67f609b4e44..7ffd8183005 100644 --- a/packages/webui/src/client/lib/ui/containers/modals/Modal.tsx +++ b/packages/webui/src/client/lib/ui/containers/modals/Modal.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import type { Sorensen } from '@sofie-automation/sorensen' -import * as CoreIcons from '@nrk/core-icons/jsx' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Escape from './../../../Escape.js' import { SorensenContext } from '../../../SorensenContext.js' @@ -101,7 +101,7 @@ export class Modal extends React.Component

diff --git a/packages/webui/src/client/lib/ui/icons/looping.tsx b/packages/webui/src/client/lib/ui/icons/looping.tsx index 96d7b46c066..54b5023a84b 100644 --- a/packages/webui/src/client/lib/ui/icons/looping.tsx +++ b/packages/webui/src/client/lib/ui/icons/looping.tsx @@ -1,6 +1,6 @@ -import React, { JSX } from 'react' -import * as loopAnimation from './icon-loop.json' -import { Lottie } from '@crello/react-lottie' +import React, { JSX, useEffect, useRef } from 'react' +import loopAnimation from './icon-loop.json' +import Lottie, { LottieComponentProps, LottieRefCurrentProps } from 'lottie-react' export function LoopingIcon(props?: Readonly>): JSX.Element { return ( @@ -25,19 +25,34 @@ export function LoopingIcon(props?: Readonly>): JS export function LoopingPieceIcon({ className, playing, -}: Readonly<{ className?: string; playing: boolean }>): JSX.Element { +}: Readonly<{ className?: string; playing?: boolean }>): JSX.Element { + const lottieRef = useRef(null) + + useEffect(() => { + if (!lottieRef.current) return + if (playing) { + lottieRef.current.play() + } else { + lottieRef.current.stop() + } + }, [playing]) + return (
- +
) } -const LOOPING_PIECE_ICON = { +const LOOPING_PIECE_ICON: LottieComponentProps = { loop: true, autoplay: false, animationData: loopAnimation, rendererSettings: { preserveAspectRatio: 'xMidYMid slice', }, + style: { + width: '24px', + height: '24px', + }, } diff --git a/packages/webui/src/client/styles/_colorScheme.scss b/packages/webui/src/client/styles/_colorScheme.scss index 0a50e271f17..eea945a755c 100644 --- a/packages/webui/src/client/styles/_colorScheme.scss +++ b/packages/webui/src/client/styles/_colorScheme.scss @@ -37,3 +37,9 @@ $ui-button-primary--translucent: var(--ui-button-primary--translucent); $ui-dark-color: var(--ui-dark-color); $ui-dark-color-brighter: var(--ui-dark-color-brighter); + +$color-interactive-highlight: var(--color-interactive-highlight); + +$color-header-inactive: var(--color-header-inactive); +$color-header-rehearsal: var(--color-header-rehearsal); +$color-header-on-air: var(--color-header-on-air); diff --git a/packages/webui/src/client/styles/bootstrap-customize.scss b/packages/webui/src/client/styles/bootstrap-customize.scss index 2c53e0bf9b9..011f2e273dc 100644 --- a/packages/webui/src/client/styles/bootstrap-customize.scss +++ b/packages/webui/src/client/styles/bootstrap-customize.scss @@ -6,6 +6,7 @@ } :root { + --bs-body-font-size: 16px; -webkit-font-smoothing: antialiased; --color-dark-1: #{$dark-1}; --color-dark-2: #{$dark-2}; diff --git a/packages/webui/src/client/styles/contextMenu.scss b/packages/webui/src/client/styles/contextMenu.scss index 40e4c97e670..2bd3730c65f 100644 --- a/packages/webui/src/client/styles/contextMenu.scss +++ b/packages/webui/src/client/styles/contextMenu.scss @@ -3,7 +3,7 @@ nav.react-contextmenu { font-size: 1.0875rem; font-weight: 400; line-height: 1.5; - letter-spacing: 0.5px; + letter-spacing: -0.01em; z-index: 900; user-select: none; @@ -22,7 +22,6 @@ nav.react-contextmenu { .react-contextmenu-item, .react-contextmenu-label { margin: 0; - padding: 4px 13px 7px 13px; display: block; border: none; background: none; @@ -37,20 +36,23 @@ nav.react-contextmenu { .react-contextmenu-label { color: #49c0fb; background: #3e4041; + padding-left: 8px; cursor: default; } .react-contextmenu-item { + padding: 2px 13px 4px 13px; color: #494949; font-weight: 300; - padding-left: 25px; - padding-right: 25px; + padding-left: 18px; + padding-right: 30px; cursor: pointer; display: flex; flex-direction: row; align-items: center; + &:hover:not(.react-contextmenu-item--disabled), &.react-contextmenu-item--selected:not(.react-contextmenu-item--disabled) { background: #313334; color: #ffffff; @@ -59,6 +61,16 @@ nav.react-contextmenu { &.react-contextmenu-item--disabled { opacity: 0.5; + cursor: default; + } + + &.react-contextmenu-item--divider { + cursor: default; + padding: 0; + margin: 0 15px; + width: auto; + border-bottom: 1px solid #ddd; + height: 0; } > svg, diff --git a/packages/webui/src/client/styles/countdown/director.scss b/packages/webui/src/client/styles/countdown/director.scss index 7cc5dd8813e..0c034624ee1 100644 --- a/packages/webui/src/client/styles/countdown/director.scss +++ b/packages/webui/src/client/styles/countdown/director.scss @@ -39,8 +39,12 @@ $hold-status-color: $liveline-timecode-color; text-align: left; } + .director-screen__top__time-to { + text-align: center; + } + .director-screen__top__planned-to { - text-align: left; + text-align: center; } .director-screen__top__planned-since { margin-left: -50px; @@ -93,22 +97,6 @@ $hold-status-color: $liveline-timecode-color; color: #ffffff; - // Default Roboto Flex settings: - font-variation-settings: - 'GRAD' 0, - 'XOPQ' 96, - 'XTRA' 468, - 'YOPQ' 79, - 'YTAS' 750, - 'YTDE' -203, - 'YTFI' 738, - 'YTLC' 548, - 'YTUC' 712, - 'opsz' 100, - 'slnt' 0, - 'wdth' 100, - 'wght' 550; - text-shadow: 0px 0px 2px rgba(0, 0, 0, 0.2); box-shadow: 0px 8px 12px rgba(0, 0, 0, 0.3); z-index: 1; @@ -214,6 +202,70 @@ $hold-status-color: $liveline-timecode-color; } } + .director-screen__body__part__timeto-content { + grid-row: 2; + grid-column: 2; + text-align: center; + width: 100vw; + margin-left: -13vw; + + .director-screen__body__part__timeto-name { + color: #888; + font-size: 9.63em; + text-transform: uppercase; + margin-top: -2vh; + } + + .director-screen__body__part__timeto-countdown { + margin-top: 4vh; + grid-row: inherit; + text-align: center; + justify-content: center; + font-size: 60em; + color: $general-countdown-to-next-color; + font-feature-settings: + 'liga' off, + 'tnum' on; + letter-spacing: 0.01em; + font-variation-settings: + 'GRAD' 0, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 514, + 'YTUC' 712, + 'opsz' 120, + 'slnt' 0, + 'wdth' 70, + 'wght' 500; + padding: 0 0.1em; + line-height: 1em; + display: flex; + align-items: center; + + > .overtime { + color: $general-late-color; + font-variation-settings: + 'GRAD' 0, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTDE' -203, + 'YTFI' 738, + 'YTLC' 548, + 'YTUC' 712, + 'opsz' 100, + 'slnt' 0, + 'wdth' 70, + 'wght' 600; + } + } + } + .director-screen__body__part__piece-content { grid-row: 2; grid-column: 2; @@ -428,5 +480,75 @@ $hold-status-color: $liveline-timecode-color; .clocks-counter-heavy { font-weight: 600; } + + .director-screen__body__t-timer { + position: absolute; + bottom: 0; + right: 0; + text-align: right; + font-size: 5vh; + z-index: 10; + line-height: 1; + + .t-timer-display { + display: flex; + align-items: stretch; + justify-content: flex-end; + font-weight: 500; + background: #333; + border-radius: 0; + overflow: hidden; + + &__label { + display: flex; + align-items: center; + color: #fff; + padding-left: 0.4em; + padding-right: 0.2em; + font-size: 1em; + text-transform: uppercase; + letter-spacing: 0.05em; + font-stretch: condensed; + } + + &__value { + display: flex; + align-items: center; + color: #fff; + font-variant-numeric: tabular-nums; + padding: 0 0.2em; + font-size: 1em; + + .t-timer-display__part { + &--dimmed { + color: #aaa; + } + } + } + + &__over-under { + display: flex; + align-items: center; + justify-content: center; + margin: 0 0 0 0.2em; + font-size: 1em; + font-variant-numeric: tabular-nums; + padding: 0 0.4em; + line-height: 1.1; + min-width: 3.5em; + border-radius: 1em; + + &--over { + background-color: $general-late-color; + color: #fff; + } + + &--under { + background-color: #ffe900; + color: #000; + } + } + } + } } } diff --git a/packages/webui/src/client/styles/countdown/presenter.scss b/packages/webui/src/client/styles/countdown/presenter.scss index 0f2a939f43d..df9a20d66a0 100644 --- a/packages/webui/src/client/styles/countdown/presenter.scss +++ b/packages/webui/src/client/styles/countdown/presenter.scss @@ -163,7 +163,7 @@ $hold-status-color: $liveline-timecode-color; .presenter-screen__rundown-status-bar { display: grid; - grid-template-columns: auto fit-content(5em); + grid-template-columns: auto fit-content(20em) fit-content(5em); grid-template-rows: fit-content(1em); font-size: 6em; color: #888; @@ -176,6 +176,73 @@ $hold-status-color: $liveline-timecode-color; line-height: 1.44em; } + .presenter-screen__rundown-status-bar__t-timer { + margin-right: 1em; + font-size: 0.8em; + align-self: center; + justify-self: end; + + .t-timer-display { + display: flex; + align-items: stretch; + justify-content: flex-end; + font-weight: 500; + background: #333; + border-radius: 0; + overflow: hidden; + + &__label { + display: flex; + align-items: center; + color: #fff; + padding-left: 0.4em; + padding-right: 0.2em; + font-size: 1em; + text-transform: uppercase; + letter-spacing: 0.05em; + font-stretch: condensed; + } + + &__value { + display: flex; + align-items: center; + color: #fff; + font-variant-numeric: tabular-nums; + padding: 0 0.2em; + font-size: 1em; + + .t-timer-display__part { + &--dimmed { + color: #aaa; + } + } + } + + &__over-under { + display: flex; + align-items: center; + justify-content: center; + margin: 0 0 0 0.2em; + font-size: 1em; + font-variant-numeric: tabular-nums; + padding: 0 0.4em; + line-height: 1.1; + min-width: 3.5em; + border-radius: 1em; + + &--over { + background-color: $general-late-color; + color: #fff; + } + + &--under { + background-color: #ffe900; + color: #000; + } + } + } + } + .presenter-screen__rundown-status-bar__countdown { white-space: nowrap; diff --git a/packages/webui/src/client/styles/counterComponents.scss b/packages/webui/src/client/styles/counterComponents.scss index 97d5c537e81..36b206f604b 100644 --- a/packages/webui/src/client/styles/counterComponents.scss +++ b/packages/webui/src/client/styles/counterComponents.scss @@ -105,8 +105,6 @@ .counter-component__time-to-planned-end { color: $general-countdown-to-next-color; letter-spacing: 0%; - text-align: right; - margin-left: 1.2vw; letter-spacing: 0.01em; font-variation-settings: diff --git a/packages/webui/src/client/styles/defaultColors.scss b/packages/webui/src/client/styles/defaultColors.scss index ef618d611d0..41be88ec005 100644 --- a/packages/webui/src/client/styles/defaultColors.scss +++ b/packages/webui/src/client/styles/defaultColors.scss @@ -41,5 +41,11 @@ --ui-dark-color: #252627; --ui-dark-color-brighter: #5f6164; + --color-interactive-highlight: #40b8fa99; + + --color-header-inactive: rgb(38, 137, 186); + --color-header-rehearsal: #666600; + --color-header-on-air: #000000; + --segment-timeline-background-color: #{$segment-timeline-background-color}; } diff --git a/packages/webui/src/client/styles/notifications.scss b/packages/webui/src/client/styles/notifications.scss index 7b86c04aa51..c3bd5a439ab 100644 --- a/packages/webui/src/client/styles/notifications.scss +++ b/packages/webui/src/client/styles/notifications.scss @@ -490,7 +490,7 @@ .rundown-view { &.notification-center-open { padding-right: 25vw !important; - > .rundown-header .rundown-overview { + > .rundown-header_OLD .rundown-overview { padding-right: calc(25vw + 1.5em) !important; } } diff --git a/packages/webui/src/client/styles/prompter.scss b/packages/webui/src/client/styles/prompter.scss index 1c03e8c9401..c3ca335957f 100644 --- a/packages/webui/src/client/styles/prompter.scss +++ b/packages/webui/src/client/styles/prompter.scss @@ -308,3 +308,49 @@ body.prompter-scrollbar { } } } + +.script-text-formatted { + --background-color: #000; + --foreground-color: #fff; + + color: var(--foreground-color); + background: var(--background-color); + + * { + color: var(--foreground-color); + } + + p { + margin: 0; + padding: 0; + } + + b, + b * { + font-weight: 700; + } + + i, + i * { + font-style: italic; + } + + u, + u * { + text-decoration: underline; + } + + .reverse, + .reverse * { + background-color: var(--foreground-color); + color: var(--background-color); + } + + .spacer { + height: 100vh; + } + + span.screen-marker { + margin: 0 0.1em; + } +} diff --git a/packages/webui/src/client/styles/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index b647eabfa65..e95ec2c58ed 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -142,11 +142,11 @@ $break-width: 35rem; } } - .rundown-header .notification-pop-ups { + .rundown-header_OLD .notification-pop-ups { top: 65px; } - > .rundown-header .rundown-overview { + > .rundown-header_OLD .rundown-overview { transition: 0s padding-right 0.5s; } @@ -154,7 +154,7 @@ $break-width: 35rem; padding-right: $notification-center-width; transition: 0s padding-right 1s; - > .rundown-header .rundown-overview { + > .rundown-header_OLD .rundown-overview { padding-right: calc(#{$notification-center-width} + 1.5em); transition: 0s padding-right 1s; } @@ -164,7 +164,7 @@ $break-width: 35rem; padding-right: $properties-panel-width; transition: 0s padding-right 1s; - > .rundown-header .rundown-overview { + > .rundown-header_OLD .rundown-overview { padding-right: calc(#{$properties-panel-width} + 1.5em); transition: 0s padding-right 1s; } @@ -209,11 +209,16 @@ body.no-overflow { bottom: 0; right: 0; - background: - linear-gradient(-45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), - linear-gradient(-45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), - linear-gradient(-45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), - linear-gradient(-45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%); + background: linear-gradient( + 45deg, + $color-status-fatal 33%, + transparent 33%, + transparent 66%, + $color-status-fatal 66% + ), // Top border + linear-gradient(45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), // Bottom border + linear-gradient(45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%), // Left border + linear-gradient(45deg, $color-status-fatal 33%, transparent 33%, transparent 66%, $color-status-fatal 66%); // Right border background-repeat: repeat-x, repeat-x, repeat-y, repeat-y; background-size: 30px 8px, @@ -240,7 +245,7 @@ body.no-overflow { } } -.rundown-header { +.rundown-header_OLD { padding: 0; .header-row { @@ -266,7 +271,16 @@ body.no-overflow { .timing__header__left { text-align: left; + display: flex; } + + .timing__header__center { + position: relative; + display: flex; + justify-content: center; + align-items: center; + } + .timing__header__right { display: grid; grid-template-columns: auto auto; @@ -474,17 +488,17 @@ body.no-overflow { cursor: default; } -.rundown-header.not-active .first-row { +.rundown-header_OLD.not-active .first-row { background-color: rgb(38, 137, 186); } -.rundown-header.not-active .first-row .timing-clock, -.rundown-header.not-active .first-row .timing-clock-label { +.rundown-header_OLD.not-active .first-row .timing-clock, +.rundown-header_OLD.not-active .first-row .timing-clock-label { color: #fff !important; } -// .rundown-header.active .first-row { +// .rundown-header_OLD.active .first-row { // background-color: #600 // } -.rundown-header.active.rehearsal .first-row { +.rundown-header_OLD.active.rehearsal .first-row { background-color: #660; } @@ -1100,8 +1114,7 @@ svg.icon { } .segment-timeline__part { .segment-timeline__part__invalid-cover { - background-image: - repeating-linear-gradient( + background-image: repeating-linear-gradient( 45deg, var(--invalid-reason-color-transparent) 0%, var(--invalid-reason-color-transparent) 4px, @@ -1383,8 +1396,7 @@ svg.icon { left: 2px; right: 2px; z-index: 3; - background: - repeating-linear-gradient( + background: repeating-linear-gradient( 45deg, var(--invalid-reason-color-opaque) 0, var(--invalid-reason-color-opaque) 5px, @@ -1566,8 +1578,7 @@ svg.icon { right: 1px; z-index: 10; pointer-events: all; - background-image: - repeating-linear-gradient( + background-image: repeating-linear-gradient( 45deg, var(--invalid-reason-color-transparent) 0%, var(--invalid-reason-color-transparent) 5px, @@ -3573,3 +3584,81 @@ svg.icon { } @import 'rundownOverview'; + +.rundown-header_OLD .timing__header_t-timers { + position: absolute; + right: 100%; + top: 50%; + transform: translateY(-38%); + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-end; + margin-right: 1em; + + .timing__header_t-timers__timer { + display: flex; + gap: 0.5em; + justify-content: space-between; + align-items: baseline; + white-space: nowrap; + line-height: 1.3; + + .timing__header_t-timers__timer__label { + font-size: 0.7em; + color: #b8b8b8; + text-transform: uppercase; + white-space: nowrap; + } + + .timing__header_t-timers__timer__value { + font-family: + 'Roboto', + Helvetica Neue, + Arial, + sans-serif; + font-variant-numeric: tabular-nums; + font-weight: 500; + color: #fff; + font-size: 1.1em; + } + + .timing__header_t-timers__timer__sign { + display: inline-block; + width: 0.6em; + text-align: center; + font-weight: 500; + font-size: 0.9em; + color: #fff; + margin-right: 0.3em; + } + + .timing__header_t-timers__timer__part { + color: #fff; + &.timing__header_t-timers__timer__part--dimmed { + color: #888; + font-weight: 400; + } + } + .timing__header_t-timers__timer__separator { + margin: 0 0.05em; + color: #888; + } + + .timing__header_t-timers__timer__over-under { + font-size: 0.75em; + font-weight: 400; + font-variant-numeric: tabular-nums; + margin-left: 0.5em; + white-space: nowrap; + + &.timing__header_t-timers__timer__over-under--over { + color: $general-late-color; + } + + &.timing__header_t-timers__timer__over-under--under { + color: #0f0; + } + } + } +} diff --git a/packages/webui/src/client/styles/shelf/dashboard.scss b/packages/webui/src/client/styles/shelf/dashboard.scss index 86cfd1ff6b0..dd457288a45 100644 --- a/packages/webui/src/client/styles/shelf/dashboard.scss +++ b/packages/webui/src/client/styles/shelf/dashboard.scss @@ -120,7 +120,7 @@ $dashboard-button-height: 3.625em; > svg { height: 1em; vertical-align: top; - margin-top: 0.05em; + margin-top: 0.05em; } } } @@ -437,7 +437,9 @@ $dashboard-button-height: 3.625em; 1px -1px 0px rgba(0, 0, 0, 0.5), 0.5px 0.5px 2px rgba(0, 0, 0, 1); z-index: 2; - text-wrap: balance; + word-break: break-word; + overflow-wrap: break-word; + white-space: normal; &.dashboard-panel__panel__button__label--editable { text-shadow: none; diff --git a/packages/webui/src/client/ui/App.tsx b/packages/webui/src/client/ui/App.tsx index 33b2d3713e3..279c3b8dab0 100644 --- a/packages/webui/src/client/ui/App.tsx +++ b/packages/webui/src/client/ui/App.tsx @@ -29,6 +29,7 @@ import { RundownList } from './RundownList.js' import { RundownView } from './RundownView.js' import { ActiveRundownView } from './ActiveRundownView.js' import { ClockView } from './ClockView/ClockView.js' +import { FullscreenOverlay } from './ClockView/FullscreenOverlay.js' import { ConnectionStatusNotification } from '../lib/ConnectionStatusNotification.js' import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom' import { ErrorBoundary } from '../lib/ErrorBoundary.js' @@ -232,6 +233,14 @@ export const App: React.FC = function App() { + + + {/* Screens that should show the fullscreen overlay prompt */} + + + + + diff --git a/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimDialog.tsx b/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimDialog.tsx index 5bc2e1ad2ed..62030bf8171 100644 --- a/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimDialog.tsx +++ b/packages/webui/src/client/ui/ClipTrimPanel/ClipTrimDialog.tsx @@ -85,15 +85,13 @@ export function ClipTrimDialog({ new Notification( undefined, NoticeLevel.CRITICAL, - ( - <> - {selectedPiece.name}:  - {t( - "Trimming this clip has timed out. It's possible that the story is currently locked for writing in {{nrcsName}} and will eventually be updated. Make sure that the story is not being edited by other users.", - { nrcsName: getRundownNrcsName(rundown) } - )} - - ), + <> + {selectedPiece.name}:  + {t( + "Trimming this clip has timed out. It's possible that the story is currently locked for writing in {{nrcsName}} and will eventually be updated. Make sure that the story is not being edited by other users.", + { nrcsName: getRundownNrcsName(rundown) } + )} + , protectString('ClipTrimDialog') ) ) @@ -102,14 +100,12 @@ export function ClipTrimDialog({ new Notification( undefined, NoticeLevel.CRITICAL, - ( - <> - {selectedPiece.name}:  - {t('Trimming this clip has failed due to an error: {{error}}.', { - error: err.message || err.error || err, - })} - - ), + <> + {selectedPiece.name}:  + {t('Trimming this clip has failed due to an error: {{error}}.', { + error: err.message || err.error || err, + })} + , protectString('ClipTrimDialog') ) ) @@ -118,12 +114,10 @@ export function ClipTrimDialog({ new Notification( undefined, NoticeLevel.NOTIFICATION, - ( - <> - {selectedPiece.name}:  - {t('Trimmed succesfully.')} - - ), + <> + {selectedPiece.name}:  + {t('Trimmed succesfully.')} + , protectString('ClipTrimDialog') ) ) @@ -137,15 +131,13 @@ export function ClipTrimDialog({ new Notification( undefined, NoticeLevel.WARNING, - ( - <> - {selectedPiece.name}:  - {t( - "Trimming this clip is taking longer than expected. It's possible that the story is locked for writing in {{nrcsName}}.", - { nrcsName: getRundownNrcsName(rundown) } - )} - - ), + <> + {selectedPiece.name}:  + {t( + "Trimming this clip is taking longer than expected. It's possible that the story is locked for writing in {{nrcsName}}.", + { nrcsName: getRundownNrcsName(rundown) } + )} + , protectString('ClipTrimDialog') ) ) diff --git a/packages/webui/src/client/ui/ClockView/CameraConfigForm.tsx b/packages/webui/src/client/ui/ClockView/CameraConfigForm.tsx index 6380fa9e672..78007abc61b 100644 --- a/packages/webui/src/client/ui/ClockView/CameraConfigForm.tsx +++ b/packages/webui/src/client/ui/ClockView/CameraConfigForm.tsx @@ -8,19 +8,18 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData.js' import { ShowStyleBases } from '../../collections/index.js' import { applyAndValidateOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { FullscreenLink } from './FullscreenLink.js' import './PrompterConfigForm.scss' interface CameraConfigState { selectedSourceLayerIds: Set studioLabels: string - fullscreen: boolean } const initialState: CameraConfigState = { selectedSourceLayerIds: new Set(), studioLabels: '', - fullscreen: false, } /** Source layer types that are relevant for the camera screen */ @@ -36,9 +35,6 @@ function generateCameraUrl(studioId: StudioId, config: CameraConfigState): strin if (config.studioLabels.trim()) { params.set('studioLabels', config.studioLabels.trim()) } - if (config.fullscreen) { - params.set('fullscreen', '1') - } const queryString = params.toString() return `/countdowns/${studioId}/camera${queryString ? '?' + queryString : ''}` @@ -157,17 +153,6 @@ export function CameraConfigForm({ studioId }: Readonly<{ studioId: StudioId }>) {t('Comma-separated list of studio labels to filter by. Leave empty for all.')} - - - updateConfig('fullscreen', e.target.checked)} - /> - {t('Click anywhere on the screen to enter fullscreen.')} - {/* Generated URL and Open Button */} @@ -178,9 +163,12 @@ export function CameraConfigForm({ studioId }: Readonly<{ studioId: StudioId }>) e.currentTarget.select()} /> - + {t('Open Camera Screen')} + + {t('Open Fullscreen')} + ) diff --git a/packages/webui/src/client/ui/ClockView/CameraScreen/Part.tsx b/packages/webui/src/client/ui/ClockView/CameraScreen/Part.tsx index 3ea9e4a83a6..5bfa710d783 100644 --- a/packages/webui/src/client/ui/ClockView/CameraScreen/Part.tsx +++ b/packages/webui/src/client/ui/ClockView/CameraScreen/Part.tsx @@ -7,7 +7,7 @@ import { PieceExtended } from '../../../lib/RundownResolver.js' import { getAllowSpeaking, getAllowVibrating } from '../../../lib/localStorage.js' import { getPartInstanceTimingValue } from '../../../lib/rundownTiming.js' import { AutoNextStatus } from '../../RundownView/RundownTiming/AutoNextStatus.js' -import { CurrentPartOrSegmentRemaining } from '../../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js' +import { CurrentPartOrSegmentRemaining } from '../../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js' import { PartCountdown } from '../../RundownView/RundownTiming/PartCountdown.js' import { PartDisplayDuration } from '../../RundownView/RundownTiming/PartDuration.js' import { TimingDataResolution, TimingTickResolution, useTiming } from '../../RundownView/RundownTiming/withTiming.js' diff --git a/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx b/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx index 74df1647f65..c93f274242e 100644 --- a/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx +++ b/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx @@ -14,7 +14,7 @@ import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PieceExtended } from '../../../lib/RundownResolver.js' import { Rundowns } from '../../../collections/index.js' -import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData.js' +import { useSubscription, useSubscriptionIfEnabled, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData.js' import { UIPartInstances, UIStudios } from '../../Collections.js' import { Rundown as RundownComponent } from './Rundown.js' import { useLocation } from 'react-router-dom' @@ -26,7 +26,7 @@ import { useTranslation } from 'react-i18next' import { Spinner } from '../../../lib/Spinner.js' import { useBlackBrowserTheme } from '../../../lib/useBlackBrowserTheme.js' import { useWakeLock } from './useWakeLock.js' -import { catchError, useDebounce } from '../../../lib/lib.js' +import { useDebounce } from '../../../lib/lib.js' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { useSetDocumentClass, useSetDocumentDarkTheme } from '../../util/useSetDocumentClass.js' @@ -54,14 +54,12 @@ export const CanvasSizeContext = React.createContext(1) const PARAM_NAME_SOURCE_LAYER_IDS = 'sourceLayerIds' const PARAM_NAME_STUDIO_LABEL = 'studioLabels' -const PARAM_NAME_FULLSCREEN = 'fullscreen' export function CameraScreen({ playlist, studioId }: Readonly): JSX.Element | null { const playlistIds = playlist ? [playlist._id] : [] const [studioLabels, setStudioLabels] = useState(null) const [sourceLayerIds, setSourceLayerIds] = useState(null) - const [fullScreenMode, setFullScreenMode] = useState(false) useBlackBrowserTheme() @@ -73,7 +71,6 @@ export function CameraScreen({ playlist, studioId }: Readonly): JSX.Elem const studioLabelParam = queryParams[PARAM_NAME_STUDIO_LABEL] ?? null const sourceLayerTypeParam = queryParams[PARAM_NAME_SOURCE_LAYER_IDS] ?? null - const fullscreenParam = queryParams[PARAM_NAME_FULLSCREEN] ?? false setStudioLabels( Array.isArray(studioLabelParam) ? studioLabelParam : studioLabelParam === null ? null : [studioLabelParam] @@ -85,7 +82,6 @@ export function CameraScreen({ playlist, studioId }: Readonly): JSX.Elem ? null : [sourceLayerTypeParam] ) - setFullScreenMode(Array.isArray(fullscreenParam) ? fullscreenParam[0] === '1' : fullscreenParam === '1') }, [location.search]) const rundowns = useTracker( @@ -102,17 +98,17 @@ export function CameraScreen({ playlist, studioId }: Readonly): JSX.Elem const rundownIds = useMemo(() => rundowns.map((rundown) => rundown._id), [rundowns]) const showStyleBaseIds = useMemo(() => rundowns.map((rundown) => rundown.showStyleBaseId), [rundowns]) - const rundownsReady = useSubscription(CorelibPubSub.rundownsInPlaylists, playlistIds) - useSubscription(CorelibPubSub.segments, rundownIds, {}) + const rundownsReady = useSubscriptionIfEnabled(CorelibPubSub.rundownsInPlaylists, playlistIds.length > 0, playlistIds) + useSubscriptionIfEnabled(CorelibPubSub.segments, rundownIds.length > 0, rundownIds, {}) const studioReady = useSubscription(MeteorPubSub.uiStudio, studioId) - useSubscription(MeteorPubSub.uiPartInstances, playlist?.activationId ?? null) + useSubscriptionIfEnabled(MeteorPubSub.uiPartInstances, !!playlist?.activationId, playlist?.activationId ?? null) - useSubscription(CorelibPubSub.parts, rundownIds, null) + useSubscriptionIfEnabled(CorelibPubSub.parts, rundownIds.length > 0, rundownIds, null) - useSubscription(CorelibPubSub.pieceInstancesSimple, rundownIds, null) + useSubscriptionIfEnabled(CorelibPubSub.pieceInstancesSimple, rundownIds.length > 0, rundownIds, null) - const piecesReady = useSubscription(CorelibPubSub.pieces, rundownIds, null) + const piecesReady = useSubscriptionIfEnabled(CorelibPubSub.pieces, rundownIds.length > 0, rundownIds, null) const [piecesReadyOnce, setPiecesReadyOnce] = useState(false) useEffect(() => { @@ -217,27 +213,6 @@ export function CameraScreen({ playlist, studioId }: Readonly): JSX.Elem } }, [canvasElRef.current]) - useLayoutEffect(() => { - if (!document.fullscreenEnabled || !fullScreenMode) return - - const targetEl = document.documentElement - - function onCanvasClick() { - if (document.fullscreenElement !== null) return - targetEl - ?.requestFullscreen({ - navigationUI: 'hide', - }) - .catch(catchError('targetEl.requestFullscreen')) - } - - document.documentElement.addEventListener('click', onCanvasClick) - - return () => { - document.documentElement.removeEventListener('click', onCanvasClick) - } - }, [fullScreenMode]) - useWakeLock() if (!studio && studioReady) return

{t("This studio doesn't exist.")}

diff --git a/packages/webui/src/client/ui/ClockView/ClockView.tsx b/packages/webui/src/client/ui/ClockView/ClockView.tsx index e75add9c112..8674369ad95 100644 --- a/packages/webui/src/client/ui/ClockView/ClockView.tsx +++ b/packages/webui/src/client/ui/ClockView/ClockView.tsx @@ -5,7 +5,7 @@ import { RundownTimingProvider } from '../RundownView/RundownTiming/RundownTimin import { StudioScreenSaver } from '../StudioScreenSaver/StudioScreenSaver.js' import { PresenterScreen } from './PresenterScreen.js' -import { DirectorScreen } from './DirectorScreen.js' +import { DirectorScreen } from './DirectorScreen/DirectorScreen' import { OverlayScreen } from './OverlayScreen.js' import { OverlayScreenSaver } from './OverlayScreenSaver.js' import { RundownPlaylists } from '../../collections/index.js' diff --git a/packages/webui/src/client/ui/ClockView/ClockViewIndex.tsx b/packages/webui/src/client/ui/ClockView/ClockViewIndex.tsx index 8b74a3e8069..b48283a81d9 100644 --- a/packages/webui/src/client/ui/ClockView/ClockViewIndex.tsx +++ b/packages/webui/src/client/ui/ClockView/ClockViewIndex.tsx @@ -7,6 +7,7 @@ import Accordion from 'react-bootstrap/esm/Accordion' import { PresenterConfigForm } from './PresenterConfigForm' import { CameraConfigForm } from './CameraConfigForm' import { PrompterConfigForm } from './PrompterConfigForm' +import { FullscreenLink } from './FullscreenLink' type AccordionKey = 'presenter' | 'camera' | 'prompter' @@ -38,12 +39,21 @@ export function ClockViewIndex({ studioId }: Readonly<{ studioId: StudioId }>):
  • {t('Director Screen')} + {' ('} + {t('fullscreen')} + {')'}
  • {t('Overlay Screen')} + {' ('} + {t('fullscreen')} + {')'}
  • {t('All Screens in a MultiViewer')} + {' ('} + {t('fullscreen')} + {')'}
  • {t('Active Rundown View')} diff --git a/packages/webui/src/client/ui/ClockView/DirectorScreen.tsx b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx similarity index 76% rename from packages/webui/src/client/ui/ClockView/DirectorScreen.tsx rename to packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx index 98b36e7f32d..5b9c2c3f978 100644 --- a/packages/webui/src/client/ui/ClockView/DirectorScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx @@ -1,25 +1,24 @@ import ClassNames from 'classnames' import { DBSegment } from '@sofie-automation/corelib/dist/dataModel/Segment' -import { PartUi } from '../SegmentTimeline/SegmentTimelineContainer.js' +import { PartUi } from '../../SegmentTimeline/SegmentTimelineContainer.js' import { DBRundownPlaylist, ABSessionAssignment } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { useTiming } from '../RundownView/RundownTiming/withTiming.js' import { useSubscription, useSubscriptions, useTracker, withTracker, -} from '../../lib/ReactMeteorData/ReactMeteorData.js' -import { getCurrentTime } from '../../lib/systemTime.js' +} from '../../../lib/ReactMeteorData/ReactMeteorData.js' +import { getCurrentTime } from '../../../lib/systemTime.js' import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' -import { PieceIconContainer } from './ClockViewPieceIcons/ClockViewPieceIcon.js' -import { PieceNameContainer } from './ClockViewPieceIcons/ClockViewPieceName.js' -import { Timediff } from './Timediff.js' -import { RundownUtils } from '../../lib/rundown.js' +import { PieceIconContainer } from '../ClockViewPieceIcons/ClockViewPieceIcon.js' +import { PieceNameContainer } from '../ClockViewPieceIcons/ClockViewPieceName.js' +import { Timediff } from '../Timediff.js' +import { RundownUtils } from '../../../lib/rundown.js' import { PieceLifespan, SourceLayerType } from '@sofie-automation/blueprints-integration' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' -import { PieceFreezeContainer } from './ClockViewPieceIcons/ClockViewFreezeCount.js' +import { PieceFreezeContainer } from '../ClockViewPieceIcons/ClockViewFreezeCount.js' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' import { RundownId, @@ -30,28 +29,26 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Ids' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' import { calculatePartInstanceExpectedDurationWithTransition } from '@sofie-automation/corelib/dist/playout/timings' -import { getPlaylistTimingDiff } from '../../lib/rundownTiming.js' import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyles' -import { UIShowStyleBases, UIStudios } from '../Collections.js' +import { UIShowStyleBases, UIStudios } from '../../Collections.js' import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' -import { PieceInstances, RundownPlaylists, Rundowns, ShowStyleVariants } from '../../collections/index.js' -import { RundownPlaylistCollectionUtil } from '../../collections/rundownPlaylistUtil.js' +import { PieceInstances, RundownPlaylists, Rundowns, ShowStyleVariants } from '../../../collections/index.js' +import { RundownPlaylistCollectionUtil } from '../../../collections/rundownPlaylistUtil.js' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' -import { useSetDocumentClass } from '../util/useSetDocumentClass.js' -import { useRundownAndShowStyleIdsForPlaylist } from '../util/useRundownAndShowStyleIdsForPlaylist.js' -import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil.js' -import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js' -import { - OverUnderClockComponent, - PlannedEndComponent, - TimeSincePlannedEndComponent, - TimeToPlannedEndComponent, -} from '../../lib/Components/CounterComponents.js' -import { AdjustLabelFit } from '../util/AdjustLabelFit.js' -import { AutoNextStatus } from '../RundownView/RundownTiming/AutoNextStatus.js' +import { useSetDocumentClass } from '../../util/useSetDocumentClass.js' +import { useRundownAndShowStyleIdsForPlaylist } from '../../util/useRundownAndShowStyleIdsForPlaylist.js' +import { RundownPlaylistClientUtil } from '../../../lib/rundownPlaylistUtil.js' +import { CurrentPartOrSegmentRemaining } from '../../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js' + +import { AdjustLabelFit } from '../../util/AdjustLabelFit.js' +import { AutoNextStatus } from '../../RundownView/RundownTiming/AutoNextStatus.js' import { useTranslation } from 'react-i18next' import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' +import { TTimerDisplay } from '../TTimerDisplay.js' +import { getDefaultTTimer } from '../../../lib/tTimerUtils.js' import { PieceInstance } from '@sofie-automation/corelib/dist/dataModel/PieceInstance.js' +import { DirectorScreenTop } from './DirectorScreenTop.js' +import { useTiming } from '../../RundownView/RundownTiming/withTiming.js' interface SegmentUi extends DBSegment { items: Array @@ -143,6 +140,7 @@ export interface DirectorScreenTrackedProps { nextShowStyleBaseId: ShowStyleBaseId | undefined showStyleBaseIds: ShowStyleBaseId[] rundownIds: RundownId[] + partInstanceToCountTimeFrom: PartInstance | undefined } function getShowStyleBaseIdSegmentPartUi( @@ -242,12 +240,14 @@ const getDirectorScreenReactive = (props: DirectorScreenProps): DirectorScreenTr fields: { lastIncorrectPartPlaybackReported: 0, modified: 0, - previousPersistentState: 0, + publicPlayoutPersistentState: 0, + privatePlayoutPersistentState: 0, rundownRanksAreSetInSofie: 0, // Note: Do not exclude assignedAbSessions/trackedAbSessions so they stay reactive restoredFromSnapshotId: 0, }, }) + const segments: Array = [] let showStyleBaseIds: ShowStyleBaseId[] = [] let rundowns: Rundown[] = [] @@ -263,17 +263,24 @@ const getDirectorScreenReactive = (props: DirectorScreenProps): DirectorScreenTr let nextSegment: SegmentUi | undefined = undefined let nextPartInstanceUi: PartUi | undefined = undefined let nextShowStyleBaseId: ShowStyleBaseId | undefined = undefined + let partInstanceToCountTimeFromUi: PartInstance | undefined = undefined if (playlist) { rundowns = RundownPlaylistCollectionUtil.getRundownsOrdered(playlist) + const orderedSegmentsAndParts = RundownPlaylistClientUtil.getSegmentsAndPartsSync(playlist) rundownIds = rundowns.map((rundown) => rundown._id) const rundownsToShowstyles: Map = new Map() for (const rundown of rundowns) { rundownsToShowstyles.set(rundown._id, rundown.showStyleBaseId) } + showStyleBaseIds = rundowns.map((rundown) => rundown.showStyleBaseId) - const { currentPartInstance, nextPartInstance } = RundownPlaylistClientUtil.getSelectedPartInstances(playlist) + const { currentPartInstance, nextPartInstance, partInstanceToCountTimeFrom } = + RundownPlaylistClientUtil.getSelectedPartInstances(playlist) + + partInstanceToCountTimeFromUi = partInstanceToCountTimeFrom + const partInstance = currentPartInstance ?? nextPartInstance if (partInstance) { // This is to register a reactive dependency on Rundown-spanning PieceInstances, that we may miss otherwise. @@ -325,6 +332,7 @@ const getDirectorScreenReactive = (props: DirectorScreenProps): DirectorScreenTr } } } + return { studio, segments, @@ -341,6 +349,7 @@ const getDirectorScreenReactive = (props: DirectorScreenProps): DirectorScreenTr nextSegment, nextPartInstance: nextPartInstanceUi, nextShowStyleBaseId, + partInstanceToCountTimeFrom: partInstanceToCountTimeFromUi, } } @@ -372,7 +381,11 @@ function useDirectorScreenSubscriptions(props: DirectorScreenProps): void { useSubscription(CorelibPubSub.showStyleVariants, null, showStyleVariantIds) useSubscription(MeteorPubSub.rundownLayouts, showStyleBaseIds) - const { currentPartInstance, nextPartInstance } = useTracker( + const { + currentPartInstance, + nextPartInstance, + partInstanceToCountTimeFrom: firstTakenPartInstance, + } = useTracker( () => { const playlist = RundownPlaylists.findOne(props.playlistId, { fields: { @@ -386,16 +399,27 @@ function useDirectorScreenSubscriptions(props: DirectorScreenProps): void { if (playlist) { return RundownPlaylistClientUtil.getSelectedPartInstances(playlist) } else { - return { currentPartInstance: undefined, nextPartInstance: undefined, previousPartInstance: undefined } + return { + currentPartInstance: undefined, + nextPartInstance: undefined, + previousPartInstance: undefined, + partInstanceToCountTimeFrom: undefined, + } } }, [props.playlistId], - { currentPartInstance: undefined, nextPartInstance: undefined, previousPartInstance: undefined } + { + currentPartInstance: undefined, + nextPartInstance: undefined, + previousPartInstance: undefined, + partInstanceToCountTimeFrom: undefined, + } ) useSubscriptions(CorelibPubSub.pieceInstances, [ currentPartInstance && [[currentPartInstance.rundownId], [currentPartInstance._id], {}], nextPartInstance && [[nextPartInstance.rundownId], [nextPartInstance._id], {}], + firstTakenPartInstance && [[firstTakenPartInstance.rundownId], [firstTakenPartInstance._id], {}], ]) } @@ -417,11 +441,12 @@ function DirectorScreenRender({ nextPartInstance, nextSegment, rundownIds, + partInstanceToCountTimeFrom, }: Readonly) { useSetDocumentClass('dark', 'xdark') const { t } = useTranslation() - const timingDurations = useTiming() + useTiming() // Compute current and next clip player ids (for pieces with AB sessions) const currentClipPlayer: string | undefined = useTracker(() => { @@ -487,21 +512,12 @@ function DirectorScreenRender({ if (playlist && playlistId && segments) { const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) || 0 - const expectedEnd = PlaylistTiming.getExpectedEnd(playlist.timing) - const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) || 0 - const now = timingDurations.currentTime ?? getCurrentTime() - - const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) ?? 0 - - // Precompute conditional blocks to satisfy linting rules (avoid nested ternaries) - let expectedStartCountdown: JSX.Element | null = null - if (!(currentPartInstance && currentShowStyleBaseId) && expectedStart) { - expectedStartCountdown = ( -
    - -
    - ) - } + + // Show countdown if it is the first segment and the current part is untimed: + const currentSegmentIsFirst = currentSegment?._rank === 0 + const isFirstPieceAndNoDuration = + (currentSegmentIsFirst && currentPartInstance?.instance.part.untimed) || + (currentSegment === undefined && nextPartInstance?.instance.part.untimed) // Precompute player icon elements to avoid nested ternaries in JSX let currentPlayerEl: JSX.Element | null = null @@ -550,127 +566,110 @@ function DirectorScreenRender({ } } + const activeTTimer = getDefaultTTimer(playlist.tTimers) + return (
    -
    - {expectedEnd ? ( -
    -
    - -
    - {t('Planned End')} -
    - ) : null} - {expectedEnd ? ( -
    -
    - -
    - {t('Time to planned end')} -
    - ) : ( -
    -
    - - {t('Time since planned end')} -
    -
    - )} -
    -
    - -
    - {t('Over/Under')} -
    -
    +
    { // Current Part: }
    -
    - - {playlist.currentPartInfo?.partInstanceId ? ( - - - - ) : null} -
    - {currentPartInstance && currentShowStyleBaseId ? ( + {!isFirstPieceAndNoDuration ? ( <> -
    - + + {playlist.currentPartInfo?.partInstanceId ? ( + + + + ) : null}
    -
    -
    - - {currentPlayerEl} -
    -
    - - - - {' '} - - +
    + - -
    -
    +
    +
    +
    + + {currentPlayerEl} +
    +
    + + + + {' '} + + + +
    +
    + + )} + ) : expectedStart ? ( +
    +
    + +
    +
    {t('Time to planned start')}
    +
    ) : null} - {expectedStartCountdown}
    { // Next Part: @@ -754,6 +753,11 @@ function DirectorScreenRender({ ) : null}
    + {!!activeTTimer && ( +
    + +
    + )}
    ) diff --git a/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreenTop.tsx b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreenTop.tsx new file mode 100644 index 00000000000..7aec7690fdf --- /dev/null +++ b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreenTop.tsx @@ -0,0 +1,86 @@ +import { + OverUnderClockComponent, + PlannedEndComponent, + TimeSincePlannedEndComponent, + TimeToPlannedEndComponent, +} from '../../../lib/Components/CounterComponents' +import { useTiming } from '../../RundownView/RundownTiming/withTiming' +import { getPlaylistTimingDiff } from '../../../lib/rundownTiming' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { getCurrentTime } from '../../../lib/systemTime' +import { useTranslation } from 'react-i18next' +import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' +import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' + +export interface DirectorScreenTopProps { + playlist: DBRundownPlaylist + partInstanceToCountTimeFrom: PartInstance | undefined +} + +export function DirectorScreenTop({ + playlist, + partInstanceToCountTimeFrom, +}: Readonly): JSX.Element { + const timingDurations = useTiming() + + const rehearsalInProgress = Boolean(playlist.rehearsal && partInstanceToCountTimeFrom?.timings?.take) + + const expectedStart = rehearsalInProgress + ? partInstanceToCountTimeFrom?.timings?.take || 0 + : PlaylistTiming.getExpectedStart(playlist.timing) || 0 + const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) || 0 + + const expectedEnd = rehearsalInProgress + ? (partInstanceToCountTimeFrom?.timings?.take || 0) + expectedDuration + : PlaylistTiming.getExpectedEnd(playlist.timing) + + const now = timingDurations.currentTime ?? getCurrentTime() + + const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) ?? 0 + + const { t } = useTranslation() + + return ( +
    + {expectedEnd ? ( +
    +
    + +
    + {t('Planned End')} +
    + ) : null} + {expectedEnd && expectedEnd > now ? ( +
    +
    + +
    + + {rehearsalInProgress ? t('Time to rehearsal end') : t('Time to planned end')} + +
    + ) : ( +
    +
    + + + {rehearsalInProgress ? t('Time since rehearsal end') : t('Time since planned end')} + +
    +
    + )} +
    +
    + +
    + {t('Over/Under')} +
    +
    + ) +} diff --git a/packages/webui/src/client/ui/ClockView/FullscreenLink.tsx b/packages/webui/src/client/ui/ClockView/FullscreenLink.tsx new file mode 100644 index 00000000000..9e184f7a100 --- /dev/null +++ b/packages/webui/src/client/ui/ClockView/FullscreenLink.tsx @@ -0,0 +1,60 @@ +import { useCallback, MouseEvent } from 'react' +import { Link, useHistory } from 'react-router-dom' +import { catchError } from '../../lib/lib.js' + +interface FullscreenLinkProps { + to: string + children: React.ReactNode + className?: string +} + +/** + * Appends fullscreen=1 to a URL path, handling existing query strings. + */ +function addFullscreenParam(url: string): string { + const hasQuery = url.includes('?') + return hasQuery ? `${url}&fullscreen=1` : `${url}?fullscreen=1` +} + +/** + * A link that navigates to a destination and also triggers fullscreen mode. + * Regular clicks will navigate AND trigger fullscreen. + * Cmd-click, Ctrl-click, or middle-click will open in a new tab (normal link behavior). + * The URL will include ?fullscreen=1 so the FullscreenOverlay can prompt for fullscreen if needed. + */ +export function FullscreenLink({ to, children, className }: Readonly): JSX.Element { + const history = useHistory() + const fullscreenUrl = addFullscreenParam(to) + + const handleClick = useCallback( + (e: MouseEvent) => { + // Allow normal link behavior for modifier keys or non-left clicks + if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) { + return + } + + e.preventDefault() + + // Request fullscreen first, then navigate + document.documentElement + .requestFullscreen({ + navigationUI: 'hide', + }) + .then(() => { + history.push(fullscreenUrl) + }) + .catch((err) => { + // If fullscreen fails (e.g., not allowed), still navigate + catchError('FullscreenLink.requestFullscreen')(err) + history.push(fullscreenUrl) + }) + }, + [fullscreenUrl, history] + ) + + return ( + + {children} + + ) +} diff --git a/packages/webui/src/client/ui/ClockView/FullscreenOverlay.scss b/packages/webui/src/client/ui/ClockView/FullscreenOverlay.scss new file mode 100644 index 00000000000..a2ea55b2a74 --- /dev/null +++ b/packages/webui/src/client/ui/ClockView/FullscreenOverlay.scss @@ -0,0 +1,32 @@ +.fullscreen-overlay { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + background-color: rgb(0 0 0 / 0.7); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 10000; + backdrop-filter: blur(2px); + border: none; + padding: 0; + margin: 0; +} + +.fullscreen-overlay__content { + text-align: center; + color: white; + user-select: none; +} + +.fullscreen-overlay__icon { + font-size: 4rem; + margin-bottom: 1rem; +} + +.fullscreen-overlay__text { + font-size: 1.5rem; + font-weight: 500; +} diff --git a/packages/webui/src/client/ui/ClockView/FullscreenOverlay.tsx b/packages/webui/src/client/ui/ClockView/FullscreenOverlay.tsx new file mode 100644 index 00000000000..c4e24349c29 --- /dev/null +++ b/packages/webui/src/client/ui/ClockView/FullscreenOverlay.tsx @@ -0,0 +1,71 @@ +import { useState, useEffect, useCallback } from 'react' +import { useLocation } from 'react-router-dom' +import { parse as queryStringParse } from 'query-string' +import { useTranslation } from 'react-i18next' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faExpand } from '@fortawesome/free-solid-svg-icons' +import { catchError } from '../../lib/lib.js' + +import './FullscreenOverlay.scss' + +const PARAM_NAME_FULLSCREEN = 'fullscreen' + +/** + * A semi-transparent overlay that prompts the user to click to enter fullscreen mode. + * Only shows when fullscreen=1 is in the URL query string. + * Automatically hides when fullscreen is active, and reappears when fullscreen is exited. + */ +export function FullscreenOverlay(): JSX.Element | null { + const { t } = useTranslation() + const location = useLocation() + const [isFullscreen, setIsFullscreen] = useState(() => document.fullscreenElement !== null) + + // Check if fullscreen=1 is in the URL + const fullscreenRequested = (() => { + const queryParams = queryStringParse(location.search, { arrayFormat: 'comma' }) + const fullscreenParam = queryParams[PARAM_NAME_FULLSCREEN] ?? false + return Array.isArray(fullscreenParam) ? fullscreenParam[0] === '1' : fullscreenParam === '1' + })() + + useEffect(() => { + function handleFullscreenChange() { + setIsFullscreen(document.fullscreenElement !== null) + } + + document.addEventListener('fullscreenchange', handleFullscreenChange) + return () => { + document.removeEventListener('fullscreenchange', handleFullscreenChange) + } + }, []) + + const requestFullscreen = useCallback(() => { + if (document.fullscreenElement !== null) return + + document.documentElement + .requestFullscreen({ + navigationUI: 'hide', + }) + .catch(catchError('FullscreenOverlay.requestFullscreen')) + }, []) + + // Don't render if fullscreen not requested, already fullscreen, or not supported + if (!fullscreenRequested || isFullscreen || !document.fullscreenEnabled) { + return null + } + + return ( + + ) +} diff --git a/packages/webui/src/client/ui/ClockView/PresenterConfigForm.tsx b/packages/webui/src/client/ui/ClockView/PresenterConfigForm.tsx index b73b5c59d08..d087f59ae60 100644 --- a/packages/webui/src/client/ui/ClockView/PresenterConfigForm.tsx +++ b/packages/webui/src/client/ui/ClockView/PresenterConfigForm.tsx @@ -8,6 +8,7 @@ import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' import { useSubscription, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData.js' import { RundownLayouts } from '../../collections/index.js' import { RundownLayoutsAPI } from '../../lib/rundownLayouts.js' +import { FullscreenLink } from './FullscreenLink.js' import './PrompterConfigForm.scss' @@ -86,9 +87,12 @@ export function PresenterConfigForm({ studioId }: Readonly<{ studioId: StudioId e.currentTarget.select()} /> - + {t('Open Presenter Screen')} + + {t('Open Fullscreen')} + ) diff --git a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx index 68e65817894..f0cb0a64e1e 100644 --- a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx @@ -4,7 +4,12 @@ import { PartUi } from '../SegmentTimeline/SegmentTimelineContainer.js' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { useTiming } from '../RundownView/RundownTiming/withTiming.js' -import { useSubscription, useSubscriptions, useTracker } from '../../lib/ReactMeteorData/ReactMeteorData.js' +import { + useSubscription, + useSubscriptions, + useSubscriptionIfEnabled, + useTracker, +} from '../../lib/ReactMeteorData/ReactMeteorData.js' import { protectString, unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { getCurrentTime } from '../../lib/systemTime.js' import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' @@ -47,7 +52,9 @@ import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' import { useSetDocumentClass, useSetDocumentDarkTheme } from '../util/useSetDocumentClass.js' import { useRundownAndShowStyleIdsForPlaylist } from '../util/useRundownAndShowStyleIdsForPlaylist.js' import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil.js' -import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js' +import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js' +import { TTimerDisplay } from './TTimerDisplay.js' +import { getDefaultTTimer } from '../../lib/tTimerUtils.js' interface SegmentUi extends DBSegment { items: Array @@ -179,7 +186,8 @@ export const getPresenterScreenReactive = ( fields: { lastIncorrectPartPlaybackReported: 0, modified: 0, - previousPersistentState: 0, + publicPlayoutPersistentState: 0, + privatePlayoutPersistentState: 0, rundownRanksAreSetInSofie: 0, trackedAbSessions: 0, restoredFromSnapshotId: 0, @@ -386,21 +394,21 @@ export function usePresenterScreenSubscriptions(props: PresenterScreenProps): vo [props.playlistId] ) - useSubscription(CorelibPubSub.rundownsInPlaylists, playlist ? [playlist._id] : []) + useSubscriptionIfEnabled(CorelibPubSub.rundownsInPlaylists, !!playlist, playlist ? [playlist._id] : []) const { rundownIds, showStyleBaseIds, showStyleVariantIds } = useRundownAndShowStyleIdsForPlaylist(playlist?._id) - useSubscription(CorelibPubSub.segments, rundownIds, {}) - useSubscription(CorelibPubSub.parts, rundownIds, null) - useSubscription(MeteorPubSub.uiParts, playlist?._id ?? null) - useSubscription(MeteorPubSub.uiPartInstances, playlist?.activationId ?? null) - useSubscription(CorelibPubSub.pieces, rundownIds, null) + useSubscriptionIfEnabled(CorelibPubSub.segments, rundownIds.length > 0, rundownIds, {}) + useSubscriptionIfEnabled(CorelibPubSub.parts, rundownIds.length > 0, rundownIds, null) + useSubscriptionIfEnabled(MeteorPubSub.uiParts, !!playlist, playlist?._id ?? null) + useSubscriptionIfEnabled(MeteorPubSub.uiPartInstances, !!playlist?.activationId, playlist?.activationId ?? null) + useSubscriptionIfEnabled(CorelibPubSub.pieces, rundownIds.length > 0, rundownIds, null) useSubscriptions( MeteorPubSub.uiShowStyleBase, showStyleBaseIds.map((id) => [id]) ) - useSubscription(CorelibPubSub.showStyleVariants, null, showStyleVariantIds) - useSubscription(MeteorPubSub.rundownLayouts, showStyleBaseIds) + useSubscriptionIfEnabled(CorelibPubSub.showStyleVariants, showStyleVariantIds.length > 0, null, showStyleVariantIds) + useSubscriptionIfEnabled(MeteorPubSub.rundownLayouts, showStyleBaseIds.length > 0, showStyleBaseIds) const { currentPartInstance, nextPartInstance } = useTracker( () => { @@ -482,6 +490,7 @@ function PresenterScreenContentDefaultLayout({ const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) ?? 0 + const activeTTimer = getDefaultTTimer(playlist.tTimers) return (
    @@ -587,6 +596,9 @@ function PresenterScreenContentDefaultLayout({
    {playlist ? playlist.name : 'UNKNOWN'}
    +
    + {!!activeTTimer && } +
    = 0, diff --git a/packages/webui/src/client/ui/ClockView/PrompterConfigForm.tsx b/packages/webui/src/client/ui/ClockView/PrompterConfigForm.tsx index 60ea990a687..f9c86b18ad0 100644 --- a/packages/webui/src/client/ui/ClockView/PrompterConfigForm.tsx +++ b/packages/webui/src/client/ui/ClockView/PrompterConfigForm.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import { StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import Form from 'react-bootstrap/esm/Form' import Collapse from 'react-bootstrap/esm/Collapse' +import { FullscreenLink } from './FullscreenLink.js' import './PrompterConfigForm.scss' @@ -668,9 +669,12 @@ export function PrompterConfigForm({ studioId }: Readonly<{ studioId: StudioId } e.currentTarget.select()} /> - + {t('Open Prompter')} + + {t('Open Fullscreen')} +
    ) diff --git a/packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx b/packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx new file mode 100644 index 00000000000..ec0ef952a06 --- /dev/null +++ b/packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx @@ -0,0 +1,55 @@ +import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownUtils } from '../../lib/rundown' +import { calculateTTimerDiff, calculateTTimerOverUnder } from '../../lib/tTimerUtils' +import { useTiming } from '../RundownView/RundownTiming/withTiming' +import classNames from 'classnames' + +interface TTimerDisplayProps { + timer: RundownTTimer +} + +export function TTimerDisplay({ timer }: Readonly): JSX.Element | null { + useTiming() + + if (!timer.mode) return null + + const now = Date.now() + + const diff = calculateTTimerDiff(timer, now) + const overUnder = calculateTTimerOverUnder(timer, now) + + const timerStr = RundownUtils.formatDiffToTimecode(Math.abs(diff), false, true, true, false, true) + const timerParts = timerStr.split(':') + const timerSign = diff >= 0 ? '' : '-' + + return ( +
    + {timer.label} + + {timerSign} + {timerParts.map((p, i) => ( + + {p} + {i < timerParts.length - 1 && :} + + ))} + + {overUnder !== undefined && ( + 0, + 't-timer-display__over-under--under': overUnder <= 0, + })} + > + {overUnder > 0 ? '+' : '\u2013'} + {RundownUtils.formatDiffToTimecode(Math.abs(overUnder), false, true, true, false, true)} + + )} +
    + ) +} diff --git a/packages/webui/src/client/ui/Header.tsx b/packages/webui/src/client/ui/Header.tsx index 538122b3a7c..b52adb6668a 100644 --- a/packages/webui/src/client/ui/Header.tsx +++ b/packages/webui/src/client/ui/Header.tsx @@ -1,11 +1,11 @@ -import * as React from 'react' -import { WithTranslation } from 'react-i18next' +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' import { Link } from 'react-router-dom' import { NotificationCenterPanelToggle, NotificationCenterPanel } from '../lib/notifications/NotificationCenterPanel.js' import { NotificationCenter, NoticeLevel } from '../lib/notifications/notifications.js' import { ErrorBoundary } from '../lib/ErrorBoundary.js' import { SupportPopUpToggle, SupportPopUp } from './SupportPopUp.js' -import { translateWithTracker, Translated } from '../lib/ReactMeteorData/ReactMeteorData.js' +import { useTracker } from '../lib/ReactMeteorData/ReactMeteorData.js' import { CoreSystem } from '../collections/index.js' import Container from 'react-bootstrap/Container' import Nav from 'react-bootstrap/Nav' @@ -19,125 +19,97 @@ interface IPropsHeader { allowDeveloper?: boolean } -interface ITrackedPropsHeader { - name: string | undefined -} - -interface IStateHeader { - isNotificationCenterOpen: NoticeLevel | undefined - isSupportPanelOpen: boolean -} +export default function Header({ allowConfigure, allowTesting }: IPropsHeader): JSX.Element { + const { t } = useTranslation() -class Header extends React.Component, IStateHeader> { - constructor(props: Translated) { - super(props) + const sofieName = useTracker(() => { + const coreSystem = CoreSystem.findOne() - this.state = { - isNotificationCenterOpen: undefined, - isSupportPanelOpen: false, - } - } + return coreSystem?.name + }, []) - onToggleNotifications = (_e: React.MouseEvent, filter: NoticeLevel | undefined) => { - if (this.state.isNotificationCenterOpen === filter) { - filter = undefined - } - NotificationCenter.isOpen = filter !== undefined ? true : false + const [isNotificationCenterOpen, setIsNotificationCenterOpen] = useState(undefined) + const onToggleNotifications = useCallback( + (_e: React.MouseEvent, filter: NoticeLevel | undefined) => { + setIsNotificationCenterOpen((isNotificationCenterOpen) => { + if (isNotificationCenterOpen === filter) { + filter = undefined + } + NotificationCenter.isOpen = filter !== undefined ? true : false - this.setState({ - isNotificationCenterOpen: filter, - }) - } + return filter + }) + }, + [] + ) - onToggleSupportPanel = () => { - this.setState({ - isSupportPanelOpen: !this.state.isSupportPanelOpen, - }) - } + const [isSupportPanelOpen, setIsSupportPanelOpen] = useState(false) + const onToggleSupportPanel = useCallback(() => setIsSupportPanelOpen((prev) => !prev), []) - render(): JSX.Element { - const { t } = this.props - - return ( - - - - {this.state.isNotificationCenterOpen !== undefined && ( - - )} - {this.state.isSupportPanelOpen && } - - - -
    - this.onToggleNotifications(e, NoticeLevel.CRITICAL)} - isOpen={this.state.isNotificationCenterOpen === NoticeLevel.CRITICAL} - filter={NoticeLevel.CRITICAL} - className="type-critical" - title={t('Critical Problems')} - /> - this.onToggleNotifications(e, NoticeLevel.WARNING)} - isOpen={this.state.isNotificationCenterOpen === NoticeLevel.WARNING} - filter={NoticeLevel.WARNING} - className="type-warning" - title={t('Warnings')} - /> - this.onToggleNotifications(e, NoticeLevel.NOTIFICATION | NoticeLevel.TIP)} - isOpen={ - this.state.isNotificationCenterOpen === ((NoticeLevel.NOTIFICATION | NoticeLevel.TIP) as NoticeLevel) - } - filter={NoticeLevel.NOTIFICATION | NoticeLevel.TIP} - className="type-notification" - title={t('Notes')} - /> - -
    -
    - - - - -
    -
    Sofie {this.props.name ? ' - ' + this.props.name : null}
    - - - + + + + ) } - -export default translateWithTracker((_props: IPropsHeader & WithTranslation): ITrackedPropsHeader => { - const coreSystem = CoreSystem.findOne() - let name: string | undefined = undefined - - if (coreSystem) { - name = coreSystem.name - } - - return { - name, - } -})(Header) diff --git a/packages/webui/src/client/ui/MediaStatus/MediaStatus.tsx b/packages/webui/src/client/ui/MediaStatus/MediaStatus.tsx index 430807894ec..0bc1afb2f3d 100644 --- a/packages/webui/src/client/ui/MediaStatus/MediaStatus.tsx +++ b/packages/webui/src/client/ui/MediaStatus/MediaStatus.tsx @@ -100,7 +100,8 @@ function useRundownPlaylists(playlistIds: RundownPlaylistId[]) { queuedSegmentId: 0, nextTimeOffset: 0, previousPartInfo: 0, - previousPersistentState: 0, + publicPlayoutPersistentState: 0, + privatePlayoutPersistentState: 0, resetTime: 0, rundownsStartedPlayback: 0, trackedAbSessions: 0, diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss index e7182b8e321..31ca95133d8 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss @@ -1,4 +1,4 @@ -@import '../../styles/itemTypeColors'; +@use '../../styles/itemTypeColors'; .preview-popUp { border: 1px solid var(--sofie-segment-layer-hover-popup-border); @@ -7,26 +7,8 @@ border-radius: 5px; overflow: hidden; pointer-events: none; - z-index: 9999; - - &--large { - width: 482px; - padding-bottom: 10px; - --preview-max-dimension: 480; - } - - &--small { - width: 322px; - --preview-max-dimension: 320; - } - - &--hidden { - visibility: none; - } - font-family: Roboto Flex; - font-style: normal; font-weight: 500; font-size: 16px; @@ -50,6 +32,17 @@ 'YTFI' 738, 'YTLC' 548, 'YTUC' 712; + + &--large { + width: 482px; + padding-bottom: 10px; + --preview-max-dimension: 480; + } + + &--small { + width: 322px; + --preview-max-dimension: 320; + } } .preview-popUp__preview { @@ -115,7 +108,7 @@ margin-left: 2%; margin-top: 7px; flex-shrink: 0; - @include item-type-colors(); + @include itemTypeColors.item-type-colors; } .preview-popUp__element-with-time-info__text { @@ -404,7 +397,7 @@ } & > * { - @include item-type-colors(); + @include itemTypeColors.item-type-colors; } .video-preview__label { diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsx b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsx index afb1040b55f..328a11ac83e 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsx +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsx @@ -12,13 +12,12 @@ export const PreviewPopUp = React.forwardRef< padding: Padding placement: Placement size: 'small' | 'large' - hidden?: boolean preview?: React.ReactNode initialOffsetX?: number trackMouse?: boolean }> >(function PreviewPopUp( - { anchor, padding, placement, hidden, size, children, initialOffsetX, trackMouse }, + { anchor, padding, placement, size, children, initialOffsetX, trackMouse }, ref ): React.JSX.Element { const [popperEl, setPopperEl] = useState(null) @@ -110,7 +109,6 @@ export const PreviewPopUp = React.forwardRef< className={classNames('preview-popUp', { 'preview-popUp--large': size === 'large', 'preview-popUp--small': size === 'small', - 'preview-popUp--hidden': hidden, })} style={styles.popper} {...attributes.popper} diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx index c51386e36d6..0e43ca735e0 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUpContext.tsx @@ -72,6 +72,7 @@ export function convertSourceLayerItemToPreview( contents.push({ type: 'script', script: popupPreview.preview.fullText, + scriptFormatted: popupPreview.preview.fullTextFormatted, lastWords: popupPreview.preview.lastWords, comment: popupPreview.preview.comment, lastModified: popupPreview.preview.lastModified, @@ -271,6 +272,7 @@ export function convertSourceLayerItemToPreview( { type: 'script', script: content.fullScript, + scriptFormatted: content.fullScriptFormatted, firstWords: content.firstWords, lastWords: content.lastWords, comment: content.comment, diff --git a/packages/webui/src/client/ui/PreviewPopUp/Previews/ScriptPreview.tsx b/packages/webui/src/client/ui/PreviewPopUp/Previews/ScriptPreview.tsx index c6212e53ed0..a216186ffa5 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/Previews/ScriptPreview.tsx +++ b/packages/webui/src/client/ui/PreviewPopUp/Previews/ScriptPreview.tsx @@ -1,12 +1,14 @@ -import { useMemo } from 'react' import { getScriptPreview } from '../../../lib/ui/scriptPreview.js' import { useTranslation } from 'react-i18next' import Moment from 'react-moment' +import { MdDisplay } from '../../Prompter/Formatted/MdDisplay.js' +import classNames from 'classnames' interface ScriptPreviewProps { content: { type: 'script' script?: string + scriptFormatted?: string lastWords?: string comment?: string lastModified?: number @@ -15,9 +17,10 @@ interface ScriptPreviewProps { export function ScriptPreview({ content }: ScriptPreviewProps): React.ReactElement { const { t } = useTranslation() - const { startOfScript, endOfScript, breakScript } = getScriptPreview(content.script ?? '') - const fullScript = useMemo(() => content?.script?.trim(), [content?.script]) + const fullScript = content?.script?.trim() ?? '' + + const { startOfScript, endOfScript, breakScript } = getScriptPreview(fullScript) return (
    @@ -29,7 +32,13 @@ export function ScriptPreview({ content }: ScriptPreviewProps): React.ReactEleme {'\u2026' + endOfScript} ) : ( - {fullScript} + + {content.scriptFormatted !== undefined ? : fullScript} + ) ) : content.lastWords ? ( {'\u2026' + content.lastWords} diff --git a/packages/webui/src/client/ui/Prompter/Formatted/MdDisplay.tsx b/packages/webui/src/client/ui/Prompter/Formatted/MdDisplay.tsx new file mode 100644 index 00000000000..e715539da1f --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/MdDisplay.tsx @@ -0,0 +1,59 @@ +import React, { useMemo } from 'react' +import createParser from './mdParser' +import { Node, ParentNodeBase } from './mdParser/astNodes' +import { assertNever } from '@sofie-automation/shared-lib/dist/lib/lib' + +const mdParser = createParser() + +export function MdDisplay({ source }: { source: string }): React.ReactNode { + const rootNode = useMemo(() => mdParser(source), [source]) + + return +} + +function MdNode({ content }: { content: Node }): React.ReactNode { + switch (content.type) { + case 'paragraph': + if (content.children.length === 0) return

     

    + return

    {renderChildren(content)}

    + case 'root': + return <>{renderChildren(content)} + case 'emphasis': + return {renderChildren(content)} + case 'strong': + return {renderChildren(content)} + case 'reverse': + return React.createElement('span', { className: 'reverse' }, renderChildren(content)) + case 'underline': + return React.createElement('u', {}, renderChildren(content)) + case 'colour': + return React.createElement( + 'span', + { + style: { + '--foreground-color': content.colour, + }, + }, + renderChildren(content) + ) + case 'text': + return content.value + case 'hidden': + return null + case 'screenMarker': + return React.createElement('span', { className: 'screen-marker' }, '❤️') + default: + assertNever(content) + return null + } +} + +function renderChildren(content: ParentNodeBase): React.ReactNode { + return ( + <> + {content.children.map((node, index) => ( + + ))} + + ) +} diff --git a/packages/webui/src/client/ui/Prompter/Formatted/mdParser/__tests__/mdParser.test.ts b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/__tests__/mdParser.test.ts new file mode 100644 index 00000000000..577d19febe4 --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/__tests__/mdParser.test.ts @@ -0,0 +1,683 @@ +import createParser, { Parser } from '../index' +import { RootNode, Node } from '../astNodes' + +// The parser uses performance.mark which may not exist in jsdom +if (typeof performance.mark !== 'function') { + performance.mark = (() => {}) as any +} + +let parse: Parser + +beforeEach(() => { + parse = createParser() +}) + +// ─── Helpers ──────────────────────────────────────────────────────────────── + +/** Shorthand to extract the first paragraph's children */ +function firstParagraph(root: RootNode): Node[] { + expect(root.children.length).toBeGreaterThanOrEqual(1) + const p = root.children[0] + expect(p).toHaveProperty('type', 'paragraph') + return (p as any).children +} + +// ─── Plain text ───────────────────────────────────────────────────────────── + +describe('plain text', () => { + test('simple text becomes a paragraph with a text node', () => { + const result = parse('hello world') + expect(result.type).toBe('root') + expect(result.children).toHaveLength(1) + + const p = result.children[0] + expect(p).toMatchObject({ + type: 'paragraph', + children: [{ type: 'text', value: 'hello world' }], + }) + }) + + test('empty string produces no paragraphs', () => { + const result = parse('') + expect(result.type).toBe('root') + expect(result.children).toHaveLength(0) + }) + + test('whitespace-only text produces a paragraph with whitespace', () => { + const result = parse(' ') + expect(result.children).toHaveLength(1) + expect(result.children[0]).toMatchObject({ + type: 'paragraph', + children: [{ type: 'text', value: ' ' }], + }) + }) +}) + +// ─── Paragraphs ───────────────────────────────────────────────────────────── + +describe('paragraphs', () => { + test('newline separates two paragraphs', () => { + const result = parse('first\nsecond') + expect(result.children).toHaveLength(2) + expect(result.children[0]).toMatchObject({ + type: 'paragraph', + children: [{ type: 'text', value: 'first' }], + }) + expect(result.children[1]).toMatchObject({ + type: 'paragraph', + children: [{ type: 'text', value: 'second' }], + }) + }) + + test('multiple newlines create separate paragraphs', () => { + const result = parse('a\nb\nc') + expect(result.children).toHaveLength(3) + expect(result.children[0]).toMatchObject({ + type: 'paragraph', + children: [{ type: 'text', value: 'a' }], + }) + expect(result.children[1]).toMatchObject({ + type: 'paragraph', + children: [{ type: 'text', value: 'b' }], + }) + expect(result.children[2]).toMatchObject({ + type: 'paragraph', + children: [{ type: 'text', value: 'c' }], + }) + }) + + test('trailing newline does not create an extra empty paragraph', () => { + const result = parse('hello\n') + expect(result.children).toHaveLength(1) + expect(result.children[0]).toMatchObject({ + type: 'paragraph', + children: [{ type: 'text', value: 'hello' }], + }) + }) + + test('consecutive newlines do not create empty paragraphs', () => { + const result = parse('one\n\ntwo') + expect(result.children).toHaveLength(2) + }) +}) + +// ─── Escape ───────────────────────────────────────────────────────────────── + +describe('escape', () => { + test('backslash escapes asterisk', () => { + const result = parse('hello \\*world') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: 'hello *world' }]) + }) + + test('backslash escapes backslash', () => { + const result = parse('hello \\\\world') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: 'hello \\world' }]) + }) + + test('backslash escapes tilde', () => { + const result = parse('\\~not reverse') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: '~not reverse' }]) + }) + + test('backslash escapes pipe', () => { + const result = parse('\\|not hidden') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: '|not hidden' }]) + }) + + test('backslash escapes bracket', () => { + const result = parse('\\[not colour') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: '[not colour' }]) + }) + + test('backslash escapes dollar sign', () => { + const result = parse('\\$not hidden') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: '$not hidden' }]) + }) + + test('backslash before regular character passes both through', () => { + const result = parse('\\a') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: 'a' }]) + }) + + test('trailing backslash is preserved as literal text', () => { + const result = parse('hello\\') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: 'hello\\' }]) + }) +}) + +// ─── Emphasis & Strong ───────────────────────────────────────────────────── + +describe('emphasis and strong', () => { + test('*text* produces emphasis', () => { + const result = parse('*hello*') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'emphasis', + code: '*', + children: [{ type: 'text', value: 'hello' }], + }, + ]) + }) + + test('_text_ produces emphasis', () => { + const result = parse('_hello_') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'emphasis', + code: '_', + children: [{ type: 'text', value: 'hello' }], + }, + ]) + }) + + test('**text** produces strong', () => { + const result = parse('**hello**') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'strong', + code: '**', + children: [{ type: 'text', value: 'hello' }], + }, + ]) + }) + + test('__text__ produces strong', () => { + const result = parse('__hello__') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'strong', + code: '__', + children: [{ type: 'text', value: 'hello' }], + }, + ]) + }) + + test('emphasis with surrounding text', () => { + const result = parse('before *middle* after') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { type: 'text', value: 'before ' }, + { + type: 'emphasis', + code: '*', + children: [{ type: 'text', value: 'middle' }], + }, + { type: 'text', value: ' after' }, + ]) + }) + + test('strong with surrounding text', () => { + const result = parse('before **middle** after') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { type: 'text', value: 'before ' }, + { + type: 'strong', + code: '**', + children: [{ type: 'text', value: 'middle' }], + }, + { type: 'text', value: ' after' }, + ]) + }) + + test('single * does not close ** strong', () => { + const result = parse('**bold*rest') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'strong', + code: '**', + children: [ + { type: 'text', value: 'bold' }, + { + type: 'emphasis', + code: '*', + children: [{ type: 'text', value: 'rest' }], + }, + ], + }, + ]) + }) + + test('strong nested inside emphasis: *italic **bold** italic*', () => { + const result = parse('*italic **bold** italic*') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'emphasis', + code: '*', + children: [ + { type: 'text', value: 'italic ' }, + { + type: 'strong', + code: '**', + children: [{ type: 'text', value: 'bold' }], + }, + { type: 'text', value: ' italic' }, + ], + }, + ]) + }) + + test('emphasis nested inside strong: **bold *italic* bold**', () => { + const result = parse('**bold *italic* bold**') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'strong', + code: '**', + children: [ + { type: 'text', value: 'bold ' }, + { + type: 'emphasis', + code: '*', + children: [{ type: 'text', value: 'italic' }], + }, + { type: 'text', value: ' bold' }, + ], + }, + ]) + }) + + test('mismatched markers are independent: *text_ does not close', () => { + const result = parse('*hello_') + const kids = firstParagraph(result) + expect(kids[0]).toHaveProperty('type', 'emphasis') + expect((kids[0] as any).code).toBe('*') + const emphasisChildren = (kids[0] as any).children + expect(emphasisChildren).toEqual( + expect.arrayContaining([expect.objectContaining({ type: 'text', value: 'hello' })]) + ) + }) + + test('multiple sequential emphasis nodes', () => { + const result = parse('*a* *b* *c*') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { type: 'emphasis', code: '*', children: [{ type: 'text', value: 'a' }] }, + { type: 'text', value: ' ' }, + { type: 'emphasis', code: '*', children: [{ type: 'text', value: 'b' }] }, + { type: 'text', value: ' ' }, + { type: 'emphasis', code: '*', children: [{ type: 'text', value: 'c' }] }, + ]) + }) +}) + +// ─── Reverse ──────────────────────────────────────────────────────────────── + +describe('reverse', () => { + test('~text~ produces reverse node', () => { + const result = parse('~hello~') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'reverse', + code: '~', + children: [{ type: 'text', value: 'hello' }], + }, + ]) + }) + + test('reverse with surrounding text', () => { + const result = parse('before ~middle~ after') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { type: 'text', value: 'before ' }, + { + type: 'reverse', + code: '~', + children: [{ type: 'text', value: 'middle' }], + }, + { type: 'text', value: ' after' }, + ]) + }) + + test('unclosed reverse collects text to end of paragraph', () => { + const result = parse('~unclosed') + const kids = firstParagraph(result) + expect(kids[0]).toHaveProperty('type', 'reverse') + expect((kids[0] as any).children).toEqual([{ type: 'text', value: 'unclosed' }]) + }) +}) + +// ─── Underline & Hidden ──────────────────────────────────────────────────── + +describe('underline and hidden', () => { + test('||text|| produces underline with pipe', () => { + const result = parse('||hello||') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'underline', + code: '||', + children: [{ type: 'text', value: 'hello' }], + }, + ]) + }) + + test('$$text$$ produces underline with dollar', () => { + const result = parse('$$hello$$') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'underline', + code: '$$', + children: [{ type: 'text', value: 'hello' }], + }, + ]) + }) + + test('|text| produces hidden with pipe', () => { + const result = parse('|hello|') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'hidden', + code: '|', + children: [{ type: 'text', value: 'hello' }], + }, + ]) + }) + + test('$text$ produces hidden with dollar', () => { + const result = parse('$hello$') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'hidden', + code: '$', + children: [{ type: 'text', value: 'hello' }], + }, + ]) + }) + + test('underline with surrounding text', () => { + const result = parse('before ||middle|| after') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { type: 'text', value: 'before ' }, + { + type: 'underline', + code: '||', + children: [{ type: 'text', value: 'middle' }], + }, + { type: 'text', value: ' after' }, + ]) + }) + + test('hidden with surrounding text', () => { + const result = parse('before |middle| after') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { type: 'text', value: 'before ' }, + { + type: 'hidden', + code: '|', + children: [{ type: 'text', value: 'middle' }], + }, + { type: 'text', value: ' after' }, + ]) + }) + + test('unclosed hidden collects text to end', () => { + const result = parse('|unclosed') + const kids = firstParagraph(result) + expect(kids[0]).toHaveProperty('type', 'hidden') + expect((kids[0] as any).children).toEqual([{ type: 'text', value: 'unclosed' }]) + }) + + test('unclosed underline collects text to end', () => { + const result = parse('||unclosed rest') + const kids = firstParagraph(result) + expect(kids[0]).toHaveProperty('type', 'underline') + expect((kids[0] as any).children).toEqual([{ type: 'text', value: 'unclosed rest' }]) + }) +}) + +// ─── Colour ───────────────────────────────────────────────────────────────── + +describe('colour', () => { + test('[colour=#ff0000]text[/colour] produces colour node', () => { + const result = parse('[colour=#ff0000]red text[/colour]') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'colour', + code: '[', + colour: '#ff0000', + children: [{ type: 'text', value: 'red text' }], + }, + ]) + }) + + test('colour with yellow hex', () => { + const result = parse('[colour=#ffff00]yellow[/colour]') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'colour', + code: '[', + colour: '#ffff00', + children: [{ type: 'text', value: 'yellow' }], + }, + ]) + }) + + test('colour with surrounding text', () => { + const result = parse('before [colour=#00ff00]green[/colour] after') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { type: 'text', value: 'before ' }, + { + type: 'colour', + code: '[', + colour: '#00ff00', + children: [{ type: 'text', value: 'green' }], + }, + { type: 'text', value: ' after' }, + ]) + }) + + test('unmatched [ is treated as literal text', () => { + const result = parse('hello [world') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: 'hello [world' }]) + }) + + test('incomplete colour tag is treated as literal text', () => { + const result = parse('[colour=oops]') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: '[colour=oops]' }]) + }) + + test('[/colour] without opening is not consumed as a closer', () => { + const result = parse('[/colour] text') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: '[/colour] text' }]) + }) +}) + +// ─── Screen Marker ────────────────────────────────────────────────────────── + +describe('screenMarker', () => { + test('(X) produces a screenMarker node', () => { + const result = parse('before (X) after') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { type: 'text', value: 'before ' }, + { type: 'screenMarker' }, + { type: 'text', value: ' after' }, + ]) + }) + + test('(X) at start of text', () => { + const result = parse('(X)hello') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'screenMarker' }, { type: 'text', value: 'hello' }]) + }) + + test('(X) at end of text', () => { + const result = parse('hello(X)') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: 'hello' }, { type: 'screenMarker' }]) + }) + + test('( not followed by X) is literal text', () => { + const result = parse('(hello)') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: '(hello)' }]) + }) + + test('(x) lowercase is literal text', () => { + const result = parse('(x)') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'text', value: '(x)' }]) + }) + + test('multiple screen markers', () => { + const result = parse('(X) middle (X)') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'screenMarker' }, { type: 'text', value: ' middle ' }, { type: 'screenMarker' }]) + }) +}) + +// ─── Nesting & combinations ──────────────────────────────────────────────── + +describe('nesting and combinations', () => { + test('emphasis inside reverse', () => { + const result = parse('~reverse *emphasis* reverse~') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'reverse', + code: '~', + children: [ + { type: 'text', value: 'reverse ' }, + { + type: 'emphasis', + code: '*', + children: [{ type: 'text', value: 'emphasis' }], + }, + { type: 'text', value: ' reverse' }, + ], + }, + ]) + }) + + test('hidden inside emphasis', () => { + const result = parse('*visible |hidden| visible*') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'emphasis', + code: '*', + children: [ + { type: 'text', value: 'visible ' }, + { + type: 'hidden', + code: '|', + children: [{ type: 'text', value: 'hidden' }], + }, + { type: 'text', value: ' visible' }, + ], + }, + ]) + }) + + test('colour inside emphasis', () => { + const result = parse('*text [colour=#ff0000]red[/colour] text*') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'emphasis', + code: '*', + children: [ + { type: 'text', value: 'text ' }, + { + type: 'colour', + code: '[', + colour: '#ff0000', + children: [{ type: 'text', value: 'red' }], + }, + { type: 'text', value: ' text' }, + ], + }, + ]) + }) + + test('screen marker inside emphasis', () => { + const result = parse('*before (X) after*') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'emphasis', + code: '*', + children: [ + { type: 'text', value: 'before ' }, + { type: 'screenMarker' }, + { type: 'text', value: ' after' }, + ], + }, + ]) + }) + + test('formatting across paragraphs does not leak', () => { + const result = parse('*italic*\n**bold**') + expect(result.children).toHaveLength(2) + expect(result.children[0]).toMatchObject({ + type: 'paragraph', + children: [{ type: 'emphasis', code: '*', children: [{ type: 'text', value: 'italic' }] }], + }) + expect(result.children[1]).toMatchObject({ + type: 'paragraph', + children: [{ type: 'strong', code: '**', children: [{ type: 'text', value: 'bold' }] }], + }) + }) + + test('escaped special char inside emphasis', () => { + const result = parse('*hello \\* world*') + const kids = firstParagraph(result) + expect(kids).toEqual([ + { + type: 'emphasis', + code: '*', + children: [{ type: 'text', value: 'hello * world' }], + }, + ]) + }) +}) + +// ─── ParserStateImpl edge cases ───────────────────────────────────────────── + +describe('ParserStateImpl edge cases', () => { + test('peek returns empty string at end of text', () => { + const result = parse('*a*') + const kids = firstParagraph(result) + expect(kids).toEqual([{ type: 'emphasis', code: '*', children: [{ type: 'text', value: 'a' }] }]) + }) + + test('parser can be reused for multiple inputs', () => { + const r1 = parse('first') + const r2 = parse('second') + expect(r1.children[0]).toMatchObject({ + type: 'paragraph', + children: [{ type: 'text', value: 'first' }], + }) + expect(r2.children[0]).toMatchObject({ + type: 'paragraph', + children: [{ type: 'text', value: 'second' }], + }) + }) +}) diff --git a/packages/webui/src/client/ui/Prompter/Formatted/mdParser/astNodes.ts b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/astNodes.ts new file mode 100644 index 00000000000..ba0274ebd2e --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/astNodes.ts @@ -0,0 +1,67 @@ +interface NodeBase { + type: string +} + +export interface ParentNodeBase extends NodeBase { + children: Node[] +} + +export interface RootNode extends ParentNodeBase { + type: 'root' +} + +export interface ParagraphNode extends ParentNodeBase { + type: 'paragraph' +} + +export interface TextNode extends NodeBase { + type: 'text' + value: string +} + +export interface StrongNode extends ParentNodeBase { + type: 'strong' + code: string +} + +export interface EmphasisNode extends ParentNodeBase { + type: 'emphasis' + code: string +} + +export interface UnderlineNode extends ParentNodeBase { + type: 'underline' + code: string +} + +export interface HiddenNode extends ParentNodeBase { + type: 'hidden' + code: string +} + +export interface ReverseNode extends ParentNodeBase { + type: 'reverse' + code: string +} + +export interface ColourNode extends ParentNodeBase { + type: 'colour' + code: string + colour: string +} + +export interface BackScreenMarkerNode extends NodeBase { + type: 'screenMarker' +} + +export type Node = + | RootNode + | ParagraphNode + | TextNode + | StrongNode + | EmphasisNode + | ReverseNode + | UnderlineNode + | HiddenNode + | ColourNode + | BackScreenMarkerNode diff --git a/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/colour.ts b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/colour.ts new file mode 100644 index 00000000000..28aba4bbfd0 --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/colour.ts @@ -0,0 +1,58 @@ +import { ColourNode } from '../astNodes' +import { NodeConstruct, ParserState, CharHandlerResult } from '../parserState' + +export function colour(): NodeConstruct { + function colour(char: string, state: ParserState): CharHandlerResult | void { + if (state.nodeCursor === null) throw new Error('cursor === null assertion') + + /** + * support for: + * [colour=#ff0000][/colour] => red text + * [colour=#ffff00][/colour] => yellow text + * + * i.e. the colour tag uses a hex code but does not actually support hex codes. + * in the future we can support more colours easily, and the length of the tag is stable + * which means the parsing is a bit simpler. + */ + + let rest = state.peek(15) + let end = false + if (rest?.startsWith('/')) { + end = true + rest = state.peek(8)?.slice(1) + } + if (!rest || (end ? rest.length < 7 : rest.length < 15)) return + if (!rest?.endsWith(']')) return + if (!rest.includes('colour')) return + + if (end) { + if (state.nodeCursor.type === 'colour') { + for (let i = 0; i < 8; i++) state.consume() + state.flushBuffer() + state.popNode() + return CharHandlerResult.StopProcessingNoBuffer + } + } else { + for (let i = 0; i < 15; i++) state.consume() + + state.flushBuffer() + + const colourNode: ColourNode = { + type: 'colour', + children: [], + code: char, + colour: rest.slice(7, 14), + } + state.pushNode(colourNode) + + return CharHandlerResult.StopProcessingNoBuffer + } + } + + return { + name: 'colour', + char: { + '[': colour, + }, + } +} diff --git a/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/emphasisAndStrong.ts b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/emphasisAndStrong.ts new file mode 100644 index 00000000000..f088fc3c2dc --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/emphasisAndStrong.ts @@ -0,0 +1,61 @@ +import { NodeConstruct, ParserState, CharHandlerResult } from '../parserState' +import { EmphasisNode, StrongNode, Node, ParentNodeBase } from '../astNodes' + +export function emphasisAndStrong(): NodeConstruct { + function emphasisOrStrong(char: string, state: ParserState): CharHandlerResult | void { + if (state.nodeCursor === null) throw new Error('cursor === null assertion') + + let len = 1 + let peeked = state.peek(len) + while (peeked && peeked.length === len && peeked.slice(-1) === char) { + len++ + peeked = state.peek(len) + } + + if (len > 2) return // this parser only handles 2 chars + + if (state.nodeCursor && isEmphasisOrStrongNode(state.nodeCursor)) { + if (state.nodeCursor.code === char && state.nodeCursor.type === 'emphasis' && len === 1) { + state.flushBuffer() + state.popNode() + return CharHandlerResult.StopProcessingNoBuffer + } + if (state.nodeCursor.code.startsWith(char) && state.nodeCursor.type === 'strong' && len === 2) { + state.consume() + state.flushBuffer() + state.popNode() + return CharHandlerResult.StopProcessingNoBuffer + } + } + + state.flushBuffer() + + let type: 'emphasis' | 'strong' = 'emphasis' + + if (len === 2) { + type = 'strong' + char += state.consume() + } + + const emphasisOrStrongNode: EmphasisNode | StrongNode = { + type, + children: [], + code: char, + } + state.pushNode(emphasisOrStrongNode) + + return CharHandlerResult.StopProcessingNoBuffer + } + + return { + name: 'emphasisOrStrong', + char: { + '*': emphasisOrStrong, + _: emphasisOrStrong, + }, + } +} + +function isEmphasisOrStrongNode(node: Node | ParentNodeBase): node is EmphasisNode | StrongNode { + return node.type === 'emphasis' || node.type === 'strong' +} diff --git a/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/escape.ts b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/escape.ts new file mode 100644 index 00000000000..adf58d00194 --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/escape.ts @@ -0,0 +1,26 @@ +import { CharHandlerResult, NodeConstruct, ParserState } from '../parserState' + +export function escape(): NodeConstruct { + function escapeChar(_: string, state: ParserState): CharHandlerResult | void { + if (state.peek() === undefined || state.peek() === '') { + // Trailing backslash with nothing to escape — treat as literal + return + } + state.dataStore['inEscape'] = true + return CharHandlerResult.StopProcessingNoBuffer + } + + function passthroughChar(_: string, state: ParserState): CharHandlerResult | void { + if (state.dataStore['inEscape'] !== true) return + state.dataStore['inEscape'] = false + return CharHandlerResult.StopProcessing + } + + return { + name: 'escape', + char: { + '\\': escapeChar, + any: passthroughChar, + }, + } +} diff --git a/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/paragraph.ts b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/paragraph.ts new file mode 100644 index 00000000000..df2cd08a175 --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/paragraph.ts @@ -0,0 +1,34 @@ +import { NodeConstruct, ParserState, CharHandlerResult } from '../parserState' +import { ParagraphNode } from '../astNodes' + +export function paragraph(): NodeConstruct { + function paragraphStart(char: string, state: ParserState) { + if (state.nodeCursor !== null) return + if (char === '\n') return + const newParagraph: ParagraphNode = { + type: 'paragraph', + children: [], + } + state.replaceStack(newParagraph) + } + + function paragraphEnd(_char: string, state: ParserState): CharHandlerResult { + if (state.nodeCursor === null) { + return CharHandlerResult.StopProcessingNoBuffer + } + + state.flushBuffer() + state.nodeCursor = null + + return CharHandlerResult.StopProcessingNoBuffer + } + + return { + name: 'paragraph', + char: { + end: paragraphEnd, + '\n': paragraphEnd, + any: paragraphStart, + }, + } +} diff --git a/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/reverse.ts b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/reverse.ts new file mode 100644 index 00000000000..75957bd29ec --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/reverse.ts @@ -0,0 +1,35 @@ +import { NodeConstruct, ParserState, CharHandlerResult } from '../parserState' +import { ReverseNode } from '../astNodes' + +export function reverse(): NodeConstruct { + function reverse(char: string, state: ParserState): CharHandlerResult | void { + if (state.nodeCursor === null) throw new Error('cursor === null assertion') + if (state.nodeCursor.type === 'reverse' && 'code' in state.nodeCursor) { + if (state.nodeCursor.code === char) { + state.flushBuffer() + state.popNode() + return CharHandlerResult.StopProcessingNoBuffer + } + } + + state.flushBuffer() + + const type = 'reverse' + + const reverseNode: ReverseNode = { + type, + children: [], + code: char, + } + state.pushNode(reverseNode) + + return CharHandlerResult.StopProcessingNoBuffer + } + + return { + name: 'reverse', + char: { + '~': reverse, + }, + } +} diff --git a/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/screenMarker.ts b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/screenMarker.ts new file mode 100644 index 00000000000..88066fd73e9 --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/screenMarker.ts @@ -0,0 +1,24 @@ +import { NodeConstruct, ParserState, CharHandlerResult } from '../parserState' + +export function screenMarker(): NodeConstruct { + function screenMarker(_: string, state: ParserState) { + if (state.nodeCursor === null) throw new Error('cursor === null assertion') + if (state.peek(2) !== 'X)') return + + // consume twice to rid of "X)" + state.consume() + state.consume() + + state.flushBuffer() + state.setMarker() + + return CharHandlerResult.StopProcessingNoBuffer + } + + return { + name: 'screenMarker', + char: { + '(': screenMarker, + }, + } +} diff --git a/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/underlineOrHide.ts b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/underlineOrHide.ts new file mode 100644 index 00000000000..fead6904819 --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/underlineOrHide.ts @@ -0,0 +1,84 @@ +import { NodeConstruct, ParserState, CharHandlerResult } from '../parserState' +import { HiddenNode, UnderlineNode } from '../astNodes' + +export function underlineOrHide(): NodeConstruct { + function underlineOrHide(char: string, state: ParserState): CharHandlerResult | void { + if (state.nodeCursor === null) throw new Error('cursor === null assertion') + + let len = 1 + let peeked = state.peek(len) + while (peeked && peeked.length === len && peeked.slice(-1) === char) { + len++ + peeked = state.peek(len) + } + + switch (len) { + case 2: + return underline(char, state) + case 1: + return hide(char, state) + default: + return + } + } + + return { + name: 'underline', + char: { + '|': underlineOrHide, + $: underlineOrHide, + }, + } +} + +function hide(char: string, state: ParserState): CharHandlerResult | void { + if (state.nodeCursor === null) throw new Error('cursor === null assertion') + + // consume once + // char += state.consume() + + if (state.nodeCursor.type === 'hidden' && 'code' in state.nodeCursor && state.nodeCursor.code === char) { + state.flushBuffer() + state.popNode() + return CharHandlerResult.StopProcessingNoBuffer + } + + state.flushBuffer() + + const type = 'hidden' + + const hiddenNode: HiddenNode = { + type, + children: [], + code: char, + } + state.pushNode(hiddenNode) + + return CharHandlerResult.StopProcessingNoBuffer +} + +function underline(char: string, state: ParserState): CharHandlerResult | void { + if (state.nodeCursor === null) throw new Error('cursor === null assertion') + + // consume once more to rid of the second character + char += state.consume() + + if (state.nodeCursor.type === 'underline' && 'code' in state.nodeCursor && state.nodeCursor.code === char) { + state.flushBuffer() + state.popNode() + return CharHandlerResult.StopProcessingNoBuffer + } + + state.flushBuffer() + + const type = 'underline' + + const underlineNode: UnderlineNode = { + type, + children: [], + code: char, + } + state.pushNode(underlineNode) + + return CharHandlerResult.StopProcessingNoBuffer +} diff --git a/packages/webui/src/client/ui/Prompter/Formatted/mdParser/index.ts b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/index.ts new file mode 100644 index 00000000000..d0b4e3d0002 --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/index.ts @@ -0,0 +1,128 @@ +import { ParentNodeBase, RootNode, Node } from './astNodes' +import { colour } from './constructs/colour' +import { emphasisAndStrong } from './constructs/emphasisAndStrong' +import { escape } from './constructs/escape' +import { paragraph } from './constructs/paragraph' +import { reverse } from './constructs/reverse' +import { screenMarker } from './constructs/screenMarker' +import { underlineOrHide } from './constructs/underlineOrHide' +import { CharHandler, CharHandlerResult, NodeConstruct, ParserState } from './parserState' + +export class ParserStateImpl implements ParserState { + readonly nodeStack: ParentNodeBase[] = [] + nodeCursor: ParentNodeBase | null = null + buffer: string = '' + charCursor: number = 0 + readonly dataStore: Record = {} + + constructor( + private document: RootNode, + private text: string + ) {} + + flushBuffer = (): void => { + if (this.buffer === '') return + if (this.nodeCursor === null) throw new Error('No node available to flush buffer.') + + this.nodeCursor.children.push({ + type: 'text', + value: this.buffer, + }) + this.buffer = '' + } + setMarker = (): void => { + if (this.nodeCursor === null) throw new Error('No node available to set marker.') + + this.nodeCursor.children.push({ + type: 'screenMarker', + }) + } + pushNode = (node: ParentNodeBase): void => { + if (this.nodeCursor !== null) { + this.nodeCursor.children.push(node as Node) + } + this.nodeStack.push(node) + this.nodeCursor = node + } + popNode = (): void => { + this.nodeStack.pop() + this.nodeCursor = this.nodeStack[this.nodeStack.length - 1] ?? null + } + replaceStack = (node: ParentNodeBase): void => { + this.document.children.push(node as Node) + this.nodeCursor = node + this.nodeStack.length = 0 + this.nodeStack.push(this.nodeCursor) + } + peek = (n = 1): string => { + return this.text.slice(this.charCursor + 1, this.charCursor + n + 1) + } + consume = (): string => { + if (this.text[this.charCursor + 1] === undefined) throw new Error('No more text available to parse') + this.charCursor++ + return this.text[this.charCursor] + } +} + +export type Parser = (text: string) => RootNode + +export default function createParser(): Parser { + const nodeConstructs: NodeConstruct[] = [ + paragraph(), + escape(), + emphasisAndStrong(), + reverse(), + underlineOrHide(), + colour(), + screenMarker(), + ] + + const charHandlers: Record = {} + + for (const construct of nodeConstructs) { + for (const [char, handler] of Object.entries(construct.char)) { + if (!charHandlers[char]) charHandlers[char] = [] + charHandlers[char].push(handler) + } + } + + function runAll(handlers: CharHandler[], char: string, state: ParserState): void | undefined | CharHandlerResult { + for (const handler of handlers) { + const result = handler(char, state) + if (typeof result === 'number') return result + } + } + + return function astFromMarkdownish(text: string): RootNode { + performance.mark('astFromMarkdownishBegin') + + const document: RootNode = { + type: 'root', + children: [], + } + + const state = new ParserStateImpl(document, text) + + for (state.charCursor = 0; state.charCursor < text.length; state.charCursor++) { + const char = text[state.charCursor] + let preventOthers = false + if (!preventOthers && charHandlers['any']) { + const result = runAll(charHandlers['any'], char, state) + if (result === CharHandlerResult.StopProcessingNoBuffer) continue + if (result === CharHandlerResult.StopProcessing) preventOthers = true + } + if (!preventOthers && charHandlers[char]) { + const result = runAll(charHandlers[char], char, state) + if (result === CharHandlerResult.StopProcessingNoBuffer) continue + if (result === CharHandlerResult.StopProcessing) preventOthers = true + } + state.buffer += char + } + + if (charHandlers['end']) runAll(charHandlers['end'], 'end', state) + + performance.mark('astFromMarkdownishEnd') + + return document + } +} diff --git a/packages/webui/src/client/ui/Prompter/Formatted/mdParser/parserState.ts b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/parserState.ts new file mode 100644 index 00000000000..0cf441482a4 --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/Formatted/mdParser/parserState.ts @@ -0,0 +1,43 @@ +import { ParentNodeBase } from './astNodes' + +export interface ParserState { + /** The current stack of nodes as set up leading to the current character */ + readonly nodeStack: ParentNodeBase[] + /** The top of the nodeStack */ + nodeCursor: ParentNodeBase | null + /** A buffer to collect characters that don't create or mutate nodes (text) */ + buffer: string + /** The position of the current character */ + charCursor: number + /** An dataStore that can be read and written to by NodeConstruct Handlers. */ + readonly dataStore: Record + /** Create a new text node and append as a child to the node under nodeCursor */ + flushBuffer(): void + /** Create a new backscreen marker node and append as a child to the node under nodeCursor */ + setMarker(): void + /** Append a new child node to the node at the top of the nodeStack, and push it onto the nodeStack */ + pushNode(node: ParentNodeBase): void + /** Pop a ParentNode from the nodeStack */ + popNode(): void + /** Append a new child node to the root node and clear the stack */ + replaceStack(node: ParentNodeBase): void + /** Get the specified number of characters immediately after the current one (default = 1) */ + peek(n?: number): string | undefined + /** Move the charCursor to the next character */ + consume(): string | undefined +} + +export enum CharHandlerResult { + /** Stop all processing of this character and append it to the text buffer */ + StopProcessing = 1, + /** Stop all processing of this character and don't append it to the text buffer */ + StopProcessingNoBuffer = 2, +} + +export type CharHandler = (char: string, state: ParserState) => void | undefined | CharHandlerResult + +/** A NodeConstruct is a set of character handlers that process a type of a node */ +export interface NodeConstruct { + name?: string + char: Record +} diff --git a/packages/webui/src/client/ui/Prompter/PrompterView.tsx b/packages/webui/src/client/ui/Prompter/PrompterView.tsx index de70edff3bf..0ace8ae3b43 100644 --- a/packages/webui/src/client/ui/Prompter/PrompterView.tsx +++ b/packages/webui/src/client/ui/Prompter/PrompterView.tsx @@ -1,4 +1,4 @@ -import React, { PropsWithChildren } from 'react' +import React, { createContext, PropsWithChildren, ReactNode, useRef } from 'react' import _ from 'underscore' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import ClassNames from 'classnames' @@ -10,6 +10,7 @@ import { Translated, useGlobalDelayedTrackerUpdateState, useSubscription, + useSubscriptionIfEnabled, useSubscriptions, useTracker, } from '../../lib/ReactMeteorData/ReactMeteorData.js' @@ -32,12 +33,16 @@ import { RundownTimingProvider } from '../RundownView/RundownTiming/RundownTimin import { StudioScreenSaver } from '../StudioScreenSaver/StudioScreenSaver.js' import { PrompterControlManager } from './controller/manager.js' import { OverUnderTimer } from './OverUnderTimer.js' -import { PrompterAPI, PrompterData, PrompterDataPart } from './prompter.js' +import { PrompterAPI, PrompterData, PrompterDataPart, PrompterDataPiece } from './prompter.js' import { doUserAction, UserAction } from '../../lib/clientUserAction.js' import { MeteorCall } from '../../lib/meteorApi.js' +import { MdDisplay } from './Formatted/MdDisplay.js' const DEFAULT_UPDATE_THROTTLE = 250 //ms const PIECE_MISSING_UPDATE_THROTTLE = 2000 //ms +const FROZEN_UPDATE_THROTTLE = 50 //ms + +const PIECE_CONTINUATION_CLASS = 'continuation' interface PrompterConfig { mirror?: boolean @@ -65,6 +70,7 @@ interface PrompterConfig { xbox_speedMap?: number[] xbox_reverseSpeedMap?: number[] xbox_triggerDeadZone?: number + shuttleWebHid_buttonMap?: string[] marker?: 'center' | 'top' | 'bottom' | 'hide' showMarker: boolean showScroll: boolean @@ -118,7 +124,24 @@ function asArray(value: T | T[] | null): T[] { } } +interface PrompterStore { + isFrozen: boolean +} + +type PrompterStoreRef = React.MutableRefObject + +const PrompterStoreContext = createContext(null) + +export function PrompterStoreProvider({ children }: { children: ReactNode }): JSX.Element { + const storeRef = useRef({ isFrozen: false }) + + return {children} +} + export class PrompterViewContent extends React.Component, IState> { + static contextType = PrompterStoreContext + declare context: PrompterStoreRef + autoScrollPreviousPartInstanceId: PartInstanceId | null = null configOptions: PrompterConfig @@ -191,6 +214,7 @@ export class PrompterViewContent extends React.Component(`[data-part-instance-id="${partInstanceId}"]`) + const target = document.querySelector( + `[data-part-instance-id="${partInstanceId}"]:not(:has(+ div.${PIECE_CONTINUATION_CLASS}))` + ) if (!target) return @@ -382,14 +413,14 @@ export class PrompterViewContent extends React.Component - window.scrollTo({ - top: latest, - behavior: 'instant', - }), + onUpdate: (latest) => window.scrollTo({ top: latest, behavior: 'instant' }), + onComplete: () => { + this.context.current.isFrozen = false + }, }) } listAnchorPositions(startY: number, endY: number, sortDirection = 1): [number, Element][] { @@ -421,6 +452,19 @@ export class PrompterViewContent extends React.Component + MeteorCall.userAction.executeAction(e, ts, playlist._id, null, actionId, null, triggerMode) + ) + } + private onWindowScroll = () => { this.triggerCheckCurrentTakeMarkers() } @@ -641,12 +685,14 @@ export function PrompterView(props: Readonly): JSX.Element { ) return ( - + + + ) } @@ -663,6 +709,7 @@ interface ScrollAnchor { /** offset to use to scroll the anchor. null means "just scroll the anchor into view, best effort" */ offset: number | null anchorId: string + continuationOfId?: string } type PrompterSnapshot = ScrollAnchor[] | null @@ -680,11 +727,11 @@ function Prompter(props: Readonly>): JSX.Eleme [props.rundownPlaylistId] ) const rundownIDs = playlist ? RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) : [] - useSubscription(CorelibPubSub.segments, rundownIDs, {}) + useSubscriptionIfEnabled(CorelibPubSub.segments, rundownIDs.length > 0, rundownIDs, {}) useSubscription(MeteorPubSub.uiParts, props.rundownPlaylistId) - useSubscription(MeteorPubSub.uiPartInstances, playlist?.activationId ?? null) - useSubscription(CorelibPubSub.pieces, rundownIDs, null) - useSubscription(CorelibPubSub.pieceInstancesSimple, rundownIDs, null) + useSubscriptionIfEnabled(MeteorPubSub.uiPartInstances, !!playlist?.activationId, playlist?.activationId ?? null) + useSubscriptionIfEnabled(CorelibPubSub.pieces, rundownIDs.length > 0, rundownIDs, null) + useSubscriptionIfEnabled(CorelibPubSub.pieceInstancesSimple, rundownIDs.length > 0, rundownIDs, null) const rundowns = useTracker( () => @@ -721,6 +768,9 @@ const PrompterContent = withTranslation()( Translated & IPrompterTrackedProps>, {} > { + static contextType = PrompterStoreContext + declare context: PrompterStoreRef + private _debounceUpdate: NodeJS.Timeout | undefined constructor(props: Translated & IPrompterTrackedProps>) { @@ -749,10 +799,9 @@ const PrompterContent = withTranslation()( getScrollAnchors = (): ScrollAnchor[] => { const readPosition = this.getReadPosition() - const useableTextAnchors: { + const useableTextAnchors: (ScrollAnchor & { offset: number - anchorId: string - }[] = [] + })[] = [] /** Maps anchorId -> offset */ const foundScrollAnchors: (ScrollAnchor & { /** Positive number. How "good" the anchor is. The anchor with the lowest number will preferred later. */ @@ -774,29 +823,43 @@ const PrompterContent = withTranslation()( // Gather anchors from any text blocks in view: - for (const textAnchor of document.querySelectorAll('.prompter .prompter-line:not(.empty)')) { + for (const textAnchor of document.querySelectorAll('.prompter .prompter-line:not(.empty)')) { const { top, bottom } = textAnchor.getBoundingClientRect() // Is the text block in view? if (top <= readPosition && bottom > readPosition) { - useableTextAnchors.push({ anchorId: textAnchor.id, offset: top }) + useableTextAnchors.push({ + anchorId: textAnchor.id, + offset: top, + continuationOfId: textAnchor.dataset.liveContinuationOf, + }) } } // Also use scroll-anchors (Segment and Part names) - for (const scrollAnchor of document.querySelectorAll('.prompter .scroll-anchor')) { + for (const scrollAnchor of document.querySelectorAll('.prompter .scroll-anchor')) { const { top, bottom } = scrollAnchor.getBoundingClientRect() const distanceToReadPosition = Math.abs(top - readPosition) if (top <= windowInnerHeight && bottom > 0) { // If the anchor is in view, use the offset to keep it's position unchanged, relative to the viewport - foundScrollAnchors.push({ anchorId: scrollAnchor.id, distanceToReadPosition, offset: top }) + foundScrollAnchors.push({ + anchorId: scrollAnchor.id, + distanceToReadPosition, + offset: top, + continuationOfId: scrollAnchor.dataset.liveContinuationOf, + }) } else { // If the anchor is not in view, set the offset to null, this will cause the view to // jump so that the anchor will be in view. - foundScrollAnchors.push({ anchorId: scrollAnchor.id, distanceToReadPosition, offset: null }) + foundScrollAnchors.push({ + anchorId: scrollAnchor.id, + distanceToReadPosition, + offset: null, + continuationOfId: scrollAnchor.dataset.liveContinuationOf, + }) } } @@ -822,7 +885,19 @@ const PrompterContent = withTranslation()( // Go through the anchors and use the first one that we find: for (const scrollAnchor of scrollAnchors) { - const anchor = document.getElementById(scrollAnchor.anchorId) + // if there is a live continuation of this anchor (or anchor that this anchor continues), it should be prioritized over the actual anchor, which now likely is empty + let anchor = document.querySelector( + `[data-live-continuation-of="${scrollAnchor.continuationOfId || scrollAnchor.anchorId}"]` + ) + // in case the anchor is already a continuation, but the script returned to its original part: + if (!anchor && scrollAnchor.continuationOfId) { + anchor = document.getElementById(scrollAnchor.continuationOfId) + } + // in case of a regular anchor: + if (!anchor && !scrollAnchor.continuationOfId) { + anchor = document.getElementById(scrollAnchor.anchorId) + } + if (!anchor) continue const { top } = anchor.getBoundingClientRect() @@ -858,7 +933,7 @@ const PrompterContent = withTranslation()( logger.error( `Read anchor could not be found after update: ${scrollAnchors .slice(0, 10) - .map((sa) => `"${sa.anchorId}" (${sa.offset})`) + .map((sa) => `"${sa.anchorId}" (offset: ${sa.offset}, continuationOfId: ${sa.continuationOfId})`) .join(', ')}` ) @@ -901,26 +976,28 @@ const PrompterContent = withTranslation()( const { prompterData } = this.props const { prompterData: nextPrompterData } = nextProps - const currentPrompterPieces = _.flatten( - prompterData?.rundowns.map((rundown) => - rundown.segments.map((segment) => - segment.parts.map((part) => - // collect all the PieceId's of all the non-empty pieces of script - _.compact(part.pieces.map((dataPiece) => (dataPiece.text !== '' ? dataPiece.id : null))) - ) - ) - ) ?? [] - ) as PieceId[] - const nextPrompterPieces = _.flatten( - nextPrompterData?.rundowns.map((rundown) => - rundown.segments.map((segment) => - segment.parts.map((part) => - // collect all the PieceId's of all the non-empty pieces of script - _.compact(part.pieces.map((dataPiece) => (dataPiece.text !== '' ? dataPiece.id : null))) + const hasPrompterText = (piece: PrompterDataPiece) => { + const prompterText = piece.formattedText ?? piece.text + return prompterText !== undefined && prompterText !== '' + } + + const getPrompterPieceIds = (data: PrompterData | null): PieceId[] => { + if (!data) return [] + + return _.compact( + data.rundowns.flatMap((rundown) => + rundown.segments.flatMap((segment) => + segment.parts.flatMap((part) => + // collect all the PieceId's of all the non-empty pieces of script + part.pieces.map((dataPiece) => (hasPrompterText(dataPiece) ? dataPiece.id : null)) + ) ) ) - ) ?? [] - ) as PieceId[] + ) + } + + const currentPrompterPieces = getPrompterPieceIds(prompterData) + const nextPrompterPieces = getPrompterPieceIds(nextPrompterData) // Flag for marking that a Piece is going missing during the update (was present in prompterData // no longer present in nextPrompterData) @@ -943,6 +1020,15 @@ const PrompterContent = withTranslation()( return false } + forceUpdate(callback?: () => void): void { + if (this.context.current.isFrozen) { + clearTimeout(this._debounceUpdate) + this._debounceUpdate = setTimeout(() => this.forceUpdate(), FROZEN_UPDATE_THROTTLE) + return + } + super.forceUpdate(callback) + } + getSnapshotBeforeUpdate(): PrompterSnapshot { return this.getScrollAnchors() } @@ -990,15 +1076,15 @@ const PrompterContent = withTranslation()( return } - const firstPart = segment.parts[0] - const firstPartStatus = this.getPartStatus(prompterData, firstPart) + let pieceIdToHideScript: PieceId | undefined + const partStatuses = segment.parts.map((part) => this.getPartStatus(prompterData, part)) lines.push(
    {segment.title || 'N/A'}
    @@ -1006,32 +1092,70 @@ const PrompterContent = withTranslation()( hasInsertedScript = true + for (let i = 0; i < segment.parts.length; i++) { + const part = segment.parts[i] + + const firstPiece = part.pieces[0] + if ( + firstPiece && + firstPiece.continuationOf && + partStatuses[i] === 'live' && + firstPiece.startPartId && + segment.parts.find((part) => part.id === firstPiece.startPartId) + ) { + // the i-th part is live and has taken over the infinite script from the start part, + // therefore we need to hide the script from the start part + pieceIdToHideScript = firstPiece.continuationOf + break + } + } + for (const part of segment.parts) { + const partStatus = this.getPartStatus(prompterData, part) + const firstPiece = part.pieces[0] + const continuesFromPart = firstPiece?.continuationOf && firstPiece.startPartId lines.push(
    {part.title || 'N/A'}
    ) for (const line of part.pieces) { + const isFormatted = line.formattedText !== undefined + let text = (isFormatted ? line.formattedText : line.text) || '' + if (line.id === pieceIdToHideScript) { + text = '' + } + if (line.continuationOf && partStatus !== 'live') { + // if a continuation is not in a live part, it should not display its text + text = '' + } + lines.push(
    - {line.text || ''} + {isFormatted ? : text}
    ) } @@ -1040,7 +1164,11 @@ const PrompterContent = withTranslation()( } if (hasInsertedScript) { - lines.push(
    —{t('End of script')}—
    ) + lines.push( +
    + —{t('End of script')}— +
    + ) } return lines diff --git a/packages/webui/src/client/ui/Prompter/controller/customizable-shuttle-webhid-device.ts b/packages/webui/src/client/ui/Prompter/controller/customizable-shuttle-webhid-device.ts new file mode 100644 index 00000000000..52686a50e46 --- /dev/null +++ b/packages/webui/src/client/ui/Prompter/controller/customizable-shuttle-webhid-device.ts @@ -0,0 +1,38 @@ +import { PrompterViewContent } from '../PrompterView' +import { ShuttleWebHidController } from './shuttle-webhid-device' + +enum ShuttleButtonTriggerMode { + PRESSED = 'pressed', + RELEASED = 'released', +} + +export class CustomizableShuttleWebHidController extends ShuttleWebHidController { + private actionMap: Record = {} + + constructor(view: PrompterViewContent) { + super(view) + view.configOptions.shuttleWebHid_buttonMap?.forEach((entry) => { + const substrings = entry.split(':') + if (substrings.length !== 2) return + this.actionMap[parseInt(substrings[0], 10)] = substrings[1] + }) + } + + protected onButtonPressed(keyIndex: number): void { + const actionId = this.actionMap[keyIndex] + if (actionId === undefined) return super.onButtonPressed(keyIndex) + + this.prompterView.executeAction(`Shuttle button ${keyIndex} press`, actionId, ShuttleButtonTriggerMode.PRESSED) + } + + protected onButtonReleased(keyIndex: number): void { + const actionId = this.actionMap[keyIndex] + if (actionId === undefined) return super.onButtonReleased(keyIndex) + + this.prompterView.executeAction( + `Shuttle button ${keyIndex} release`, + actionId, + ShuttleButtonTriggerMode.RELEASED + ) + } +} diff --git a/packages/webui/src/client/ui/Prompter/controller/manager.ts b/packages/webui/src/client/ui/Prompter/controller/manager.ts index e374625ef29..a2ae70af139 100644 --- a/packages/webui/src/client/ui/Prompter/controller/manager.ts +++ b/packages/webui/src/client/ui/Prompter/controller/manager.ts @@ -5,8 +5,8 @@ import { ControllerAbstract } from './lib.js' import { JoyConController } from './joycon-device.js' import { KeyboardController } from './keyboard-device.js' import { ShuttleKeyboardController } from './shuttle-keyboard-device.js' -import { ShuttleWebHidController } from './shuttle-webhid-device.js' import { XboxController } from './xbox-controller-device.js' +import { CustomizableShuttleWebHidController } from './customizable-shuttle-webhid-device.js' export class PrompterControlManager { private _view: PrompterViewContent @@ -38,7 +38,7 @@ export class PrompterControlManager { this._controllers.push(new JoyConController(this._view)) } if (this._view.configOptions.mode.includes(PrompterConfigMode.SHUTTLEWEBHID)) { - this._controllers.push(new ShuttleWebHidController(this._view)) + this._controllers.push(new CustomizableShuttleWebHidController(this._view)) } if (this._view.configOptions.mode.includes(PrompterConfigMode.XBOX)) { this._controllers.push(new XboxController(this._view)) diff --git a/packages/webui/src/client/ui/Prompter/controller/shuttle-webhid-device.ts b/packages/webui/src/client/ui/Prompter/controller/shuttle-webhid-device.ts index 8af67a0cc41..8758dd5f1e9 100644 --- a/packages/webui/src/client/ui/Prompter/controller/shuttle-webhid-device.ts +++ b/packages/webui/src/client/ui/Prompter/controller/shuttle-webhid-device.ts @@ -8,7 +8,7 @@ import { logger } from '../../../lib/logging.js' * This class handles control of the prompter using Contour Shuttle / Multimedia Controller line of devices */ export class ShuttleWebHidController extends ControllerAbstract { - private prompterView: PrompterViewContent + protected prompterView: PrompterViewContent private speedMap = [0, 1, 2, 3, 5, 7, 9, 30] @@ -87,6 +87,7 @@ export class ShuttleWebHidController extends ControllerAbstract { logger.debug(`Button ${keyIndex} down`) }) shuttle.on('up', (keyIndex: number) => { + this.onButtonReleased(keyIndex) logger.debug(`Button ${keyIndex} up`) }) shuttle.on('jog', (delta, value) => { @@ -142,6 +143,10 @@ export class ShuttleWebHidController extends ControllerAbstract { } } + protected onButtonReleased(_keyIndex: number): void { + // no-op + } + protected onJog(delta: number): void { if (Math.abs(delta) > 1) return // this is a hack because sometimes, right after connecting to the device, the delta would be larger than 1 or -1 diff --git a/packages/webui/src/client/ui/Prompter/prompter.ts b/packages/webui/src/client/ui/Prompter/prompter.ts index f577563c68f..e64c90006d3 100644 --- a/packages/webui/src/client/ui/Prompter/prompter.ts +++ b/packages/webui/src/client/ui/Prompter/prompter.ts @@ -56,6 +56,9 @@ export interface PrompterDataPart { export interface PrompterDataPiece { id: PieceId text: string + formattedText: string | undefined + continuationOf?: PieceId + startPartId?: PartId | null } export interface PrompterData { title: string @@ -264,12 +267,23 @@ export namespace PrompterAPI { const content = piece.content as ScriptContent if (!content.fullScript) continue - if (piecesIncluded.indexOf(piece._id) >= 0) continue // piece already included in prompter script + if (piecesIncluded.indexOf(piece._id) >= 0) { + // piece already included in prompter script - mark it as a continuation + partData.pieces.push({ + id: protectString(`${partData.id}_${piece._id}_continuation`), + text: content.fullScript, + formattedText: content.fullScriptFormatted, + continuationOf: piece._id, + startPartId: piece.startPartId, + }) + continue + } piecesIncluded.push(piece._id) partData.pieces.push({ id: piece._id, text: content.fullScript, + formattedText: content.fullScriptFormatted, }) } } @@ -279,6 +293,7 @@ export namespace PrompterAPI { partData.pieces.push({ id: protectString(`part_${partData.id}_empty`), text: '', + formattedText: '', }) } diff --git a/packages/webui/src/client/ui/RundownView.tsx b/packages/webui/src/client/ui/RundownView.tsx index d795959a66c..01f9b75c783 100644 --- a/packages/webui/src/client/ui/RundownView.tsx +++ b/packages/webui/src/client/ui/RundownView.tsx @@ -112,6 +112,7 @@ import { RundownViewContextProviders } from './RundownView/RundownViewContextPro import { AnimatePresence } from 'motion/react' import { UserError } from '@sofie-automation/corelib/dist/error' import { DragContextProvider } from './RundownView/DragContextProvider.js' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance.js' const HIDE_NOTIFICATIONS_AFTER_MOUNT: number | undefined = 5000 @@ -291,7 +292,7 @@ export function RundownView(props: Readonly): JSX.Element { return (
    { + private onSetNext = (part: DBPartInstance | DBPart | undefined, e: any, offset?: number, take?: boolean) => { const { t } = this.props if (this.props.userPermissions.studio && part && part._id && this.props.playlist) { const playlistId = this.props.playlist._id @@ -759,7 +760,7 @@ const RundownViewContent = translateWithTracker MeteorCall.userAction.setNext(e, ts, playlistId, part._id, offset), + (e, ts) => MeteorCall.userAction.setNext(e, ts, playlistId, part._id, offset, 'part' in part), (err) => { this.setState({ manualSetAsNext: true, @@ -1399,6 +1400,7 @@ const RundownViewContent = translateWithTracker + {parts.map((p, i) => { + const offset = 3 - parts.length + const isDimmed = absDiff < THRESHOLDS[i + offset] + return ( + + {p} + {i < parts.length - 1 && ( + + : + + )} + + ) + })} + + ) +} + +function renderContent(time: number | undefined, ms: number | undefined, children: React.ReactNode): React.ReactNode { + if (time !== undefined) { + return + } + if (typeof children === 'string') { + return + } + return children +} + +export function Countdown({ label, time, className, children, ms, postfix }: IProps): JSX.Element { + const valueClassName = time === undefined ? 'countdown__counter' : 'countdown__timeofday' + + return ( + + {label && {label}} + + {renderContent(time, ms, children)} + {postfix} + + + ) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx new file mode 100644 index 00000000000..3772d964c16 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx @@ -0,0 +1,196 @@ +import React, { useEffect, useRef } from 'react' +import ClassNames from 'classnames' +import { TimingDataResolution, TimingTickResolution, useTiming } from '../RundownTiming/withTiming.js' +import { RundownUtils } from '../../../lib/rundown.js' +import { SpeechSynthesiser } from '../../../lib/speechSynthesis.js' +import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' + +import { Countdown } from './Countdown.js' + +const SPEAK_ADVANCE = 500 + +interface IPartRemainingProps { + currentPartInstanceId: PartInstanceId | null + label?: string + hideOnZero?: boolean + className?: string + heavyClassName?: string + speaking?: boolean + vibrating?: boolean + /** Use the segment budget instead of the part duration if available */ + preferSegmentTime?: boolean +} + +// global variable for remembering last uttered displayTime +let prevDisplayTime: number | undefined = undefined + +function speak(displayTime: number) { + let text = '' // Say nothing + + switch (displayTime) { + case -1: + text = 'One' + break + case -2: + text = 'Two' + break + case -3: + text = 'Three' + break + case -4: + text = 'Four' + break + case -5: + text = 'Five' + break + case -6: + text = 'Six' + break + case -7: + text = 'Seven' + break + case -8: + text = 'Eight' + break + case -9: + text = 'Nine' + break + case -10: + text = 'Ten' + break + } + + if (text) { + SpeechSynthesiser.speak(text, 'countdown') + } +} + +function vibrate(displayTime: number) { + if ('vibrate' in navigator) { + switch (displayTime) { + case 0: + navigator.vibrate([500]) + break + case -1: + case -2: + case -3: + navigator.vibrate([250]) + break + } + } +} + +function usePartRemaining(props: IPartRemainingProps) { + const timingDurations = useTiming(TimingTickResolution.Synced, TimingDataResolution.Synced) + const prevPartInstanceId = useRef(null) + + useEffect(() => { + if (props.currentPartInstanceId !== prevPartInstanceId.current) { + prevDisplayTime = undefined + prevPartInstanceId.current = props.currentPartInstanceId + } + + if (!timingDurations?.currentTime) return + if (timingDurations.currentPartInstanceId !== props.currentPartInstanceId) return + + let displayTime = (timingDurations.remainingTimeOnCurrentPart || 0) * -1 + + if (displayTime !== 0) { + displayTime += SPEAK_ADVANCE + displayTime = Math.floor(displayTime / 1000) + } + + if (prevDisplayTime !== displayTime) { + if (props.speaking) { + speak(displayTime) + } + + if (props.vibrating) { + vibrate(displayTime) + } + + prevDisplayTime = displayTime + } + }, [ + props.currentPartInstanceId, + timingDurations?.currentTime, + timingDurations?.currentPartInstanceId, + timingDurations?.remainingTimeOnCurrentPart, + props.speaking, + props.vibrating, + ]) + + if (!timingDurations?.currentTime) return null + if (timingDurations.currentPartInstanceId !== props.currentPartInstanceId) return null + + let displayTimecode = timingDurations.remainingTimeOnCurrentPart + if (props.preferSegmentTime) { + if (timingDurations.remainingBudgetOnCurrentSegment === undefined) return null + displayTimecode = timingDurations.remainingBudgetOnCurrentSegment + } + + if (displayTimecode === undefined) return null + displayTimecode *= -1 + + return { displayTimecode } +} + +/** + * Original version used across the app — renders a plain with role="timer". + */ +export const CurrentPartOrSegmentRemaining: React.FC = (props) => { + const result = usePartRemaining(props) + if (!result) return null + + const { displayTimecode } = result + + return ( + 0 ? props.heavyClassName : undefined)} + role="timer" + > + {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} + + ) +} + +/** + * RundownHeader variant — renders inside a component with label support. + */ +export const RundownHeaderPartRemaining: React.FC = (props) => { + const result = usePartRemaining(props) + if (!result) return null + + const { displayTimecode } = result + + return ( + 0 ? props.heavyClassName : undefined)} + > + {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} + + ) +} + +/** + * RundownHeader Segment Budget variant — renders inside a wrapper with a label, and handles hiding when value is missing or 0. + */ +export const RundownHeaderSegmentBudget: React.FC<{ + currentPartInstanceId: PartInstanceId | null + label?: string +}> = ({ currentPartInstanceId, label }) => { + const result = usePartRemaining({ currentPartInstanceId, preferSegmentTime: true }) + if (!result) return null + + const { displayTimecode } = result + + return ( + + {label} + 0 ? 'overtime' : undefined)}> + {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} + + + ) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/HeaderFreezeFrameIcon.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/HeaderFreezeFrameIcon.tsx new file mode 100644 index 00000000000..7a6328d03e8 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/HeaderFreezeFrameIcon.tsx @@ -0,0 +1,59 @@ +import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { FreezeFrameIcon } from '../../../lib/ui/icons/freezeFrame' +import { useTiming, TimingTickResolution, TimingDataResolution } from '../RundownTiming/withTiming' +import { useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' +import { PartInstances, PieceInstances } from '../../../collections' +import { VTContent } from '@sofie-automation/blueprints-integration' + +export function HeaderFreezeFrameIcon({ partInstanceId }: { partInstanceId: PartInstanceId }): JSX.Element | null { + const timingDurations = useTiming(TimingTickResolution.Synced, TimingDataResolution.Synced) + + const freezeFrameIcon = useTracker( + () => { + const partInstance = PartInstances.findOne(partInstanceId) + if (!partInstance) return null + + // We use the exact display duration from the timing context just like VTSourceRenderer does. + // Fallback to static displayDuration or expectedDuration if timing context is unavailable. + const partDisplayDuration = + (timingDurations.partDisplayDurations && timingDurations.partDisplayDurations[partInstanceId as any]) ?? + partInstance.part.displayDuration ?? + partInstance.part.expectedDuration ?? + 0 + + const partDuration = timingDurations.partDurations + ? timingDurations.partDurations[partInstanceId as any] + : partDisplayDuration + + const pieceInstances = PieceInstances.find({ partInstanceId }).fetch() + + for (const pieceInstance of pieceInstances) { + const piece = pieceInstance.piece + if (piece.virtual) continue + + const content = piece.content as VTContent | undefined + if (!content || content.loop || content.sourceDuration === undefined) { + continue + } + + const seek = content.seek || 0 + const renderedInPoint = typeof piece.enable.start === 'number' ? piece.enable.start : 0 + const pieceDuration = content.sourceDuration - seek + + const isAutoNext = partInstance.part.autoNext + + if ( + (isAutoNext && renderedInPoint + pieceDuration < partDuration) || + (!isAutoNext && Math.abs(renderedInPoint + pieceDuration - partDisplayDuration) > 500) + ) { + return + } + } + return null + }, + [partInstanceId, timingDurations.partDisplayDurations, timingDurations.partDurations], + null + ) + + return freezeFrameIcon +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx new file mode 100644 index 00000000000..7f7800f9dcd --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx @@ -0,0 +1,222 @@ +import React, { useCallback, useContext, useEffect, useRef } from 'react' +import { useHistory } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import Escape from '../../../lib/Escape' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { Rundown, getRundownNrcsName } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { ContextMenu, MenuItem, ContextMenuTrigger, hideMenu, showMenu } from '@jstarpl/react-contextmenu' +import { contextMenuHoldToDisplayTime, useRundownViewEventBusListener } from '../../../lib/lib' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { faBars, faTimes } from '@fortawesome/free-solid-svg-icons' +import { + ActivateRundownPlaylistEvent, + DeactivateRundownPlaylistEvent, + IEventContext, + RundownViewEvents, +} from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' +import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' +import { UserPermissionsContext } from '../../UserPermissions' +import * as RundownResolver from '../../../lib/RundownResolver' +import { checkRundownTimes, useRundownPlaylistOperations } from './useRundownPlaylistOperations.js' +import { reloadRundownPlaylistClick } from '../RundownNotifier' + +export const RUNDOWN_CONTEXT_MENU_ID = 'rundown-context-menu' + +interface RundownContextMenuProps { + playlist: DBRundownPlaylist + studio: UIStudio + firstRundown: Rundown | undefined + onShow?: () => void + onHide?: () => void +} + +/** + * The RundownContextMenu component renders both the context menu definition and the right-click + * trigger area. It also registers event bus listeners for playlist operations (activate, + * deactivate, take, reset, etc.) since these are tightly coupled to the menu actions. + */ +export function RundownContextMenu({ + playlist, + studio, + firstRundown, + onShow, + onHide, +}: Readonly): JSX.Element { + const { t } = useTranslation() + const history = useHistory() + const userPermissions = useContext(UserPermissionsContext) + const operations = useRundownPlaylistOperations() + + const canClearQuickLoop = + !!studio.settings.enableQuickLoop && + !RundownResolver.isLoopLocked(playlist) && + RundownResolver.isAnyLoopMarkerDefined(playlist) + + const rundownTimesInfo = checkRundownTimes(playlist.timing) + + // --- Event bus listeners for playlist operations --- + const eventActivate = useCallback( + (e: ActivateRundownPlaylistEvent) => { + if (e.rehearsal) { + operations.activateRehearsal(e.context) + } else { + operations.activate(e.context) + } + }, + [operations] + ) + const eventDeactivate = useCallback( + (e: DeactivateRundownPlaylistEvent) => operations.deactivate(e.context), + [operations] + ) + const eventResync = useCallback((e: IEventContext) => operations.reloadRundownPlaylist(e.context), [operations]) + const eventTake = useCallback((e: IEventContext) => operations.take(e.context), [operations]) + const eventResetRundownPlaylist = useCallback((e: IEventContext) => operations.resetRundown(e.context), [operations]) + const eventCreateSnapshot = useCallback((e: IEventContext) => operations.takeRundownSnapshot(e.context), [operations]) + + useRundownViewEventBusListener(RundownViewEvents.ACTIVATE_RUNDOWN_PLAYLIST, eventActivate) + useRundownViewEventBusListener(RundownViewEvents.DEACTIVATE_RUNDOWN_PLAYLIST, eventDeactivate) + useRundownViewEventBusListener(RundownViewEvents.RESYNC_RUNDOWN_PLAYLIST, eventResync) + useRundownViewEventBusListener(RundownViewEvents.TAKE, eventTake) + useRundownViewEventBusListener(RundownViewEvents.RESET_RUNDOWN_PLAYLIST, eventResetRundownPlaylist) + useRundownViewEventBusListener(RundownViewEvents.CREATE_SNAPSHOT_FOR_DEBUG, eventCreateSnapshot) + + useEffect(() => { + reloadRundownPlaylistClick.set(operations.reloadRundownPlaylist) + }, [operations.reloadRundownPlaylist]) + + return ( + + +
    {playlist && playlist.name}
    + {userPermissions.studio ? ( + + {!(playlist.activationId && playlist.rehearsal) ? ( + !rundownTimesInfo.shouldHaveStarted && !playlist.activationId ? ( + + {t('Prepare Studio and Activate (Rehearsal)')} + + ) : ( + {t('Activate (Rehearsal)')} + ) + ) : ( + {t('Activate On Air')} + )} + {rundownTimesInfo.willShortlyStart && !playlist.activationId && ( + {t('Activate On Air')} + )} + {playlist.activationId ? ( + {t('Deactivate Studio')} + ) : null} + {studio.settings.allowAdlibTestingSegment && playlist.activationId ? ( + {t('AdLib Testing')} + ) : null} + {playlist.activationId ? ( + <> + + {t('Take')} + + ) : null} + {studio.settings.allowHold && playlist.activationId ? ( + {t('Hold')} + ) : null} + {playlist.activationId && canClearQuickLoop ? ( + {t('Clear QuickLoop')} + ) : null} + {!(playlist.activationId && !playlist.rehearsal && !studio.settings.allowRundownResetOnAir) ? ( + <> + + {t('Reset Rundown')} + + ) : null} + + {t('Reload {{nrcsName}} Data', { + nrcsName: getRundownNrcsName(firstRundown), + })} + + {t('Store Snapshot')} + + history.push('/')}>{t('Close Rundown')} + + ) : ( + + {t('No actions available')} + + )} +
    +
    + ) +} + +interface RundownContextMenuTriggerProps { + children: React.ReactNode +} + +export function RundownHeaderContextMenuTrigger({ children }: Readonly): JSX.Element { + return ( + + {children} + + ) +} + +/** + * A hamburger button that opens the context menu on left-click. + */ +export function RundownHamburgerButton({ + isOpen, + disabled, + onClose, + onOpen, +}: Readonly<{ isOpen?: boolean; disabled?: boolean; onClose: () => void; onOpen?: () => void }>): JSX.Element { + const { t } = useTranslation() + const buttonRef = useRef(null) + + const handleToggle = useCallback( + (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + + if (disabled) return + + if (isOpen) { + hideMenu({ id: RUNDOWN_CONTEXT_MENU_ID }) + onClose() + return + } + + if (buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect() + showMenu({ + position: { x: rect.left, y: rect.bottom + 5 }, + id: RUNDOWN_CONTEXT_MENU_ID, + }) + if (onOpen) onOpen() + } + }, + [isOpen, disabled, onClose, onOpen] + ) + + return ( + + ) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss new file mode 100644 index 00000000000..a0fce01c1ab --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -0,0 +1,645 @@ +@import '../../../styles/colorScheme'; +@import './shared'; +@import './Countdown'; + +.rundown-header { + height: 64px; + min-height: 64px; + padding: 0; + width: 100%; + border-bottom: 1px solid #333; + transition: + background-color 0.5s, + border-bottom-color 0.5s; + font-family: 'Roboto Flex', 'Roboto', sans-serif; + font-feature-settings: + 'liga' 0, + 'tnum'; + font-variant-numeric: tabular-nums; + user-select: none; + cursor: default; + + .rundown-header__trigger { + height: 100%; + width: 100%; + display: block; + } + + // State-based background colors + &.not-active { + background-color: $color-header-inactive; + } + + &.active { + background-color: $color-header-on-air; + border-bottom: 1px solid #256b91; + + .rundown-header__timers-segment-remaining, + .rundown-header__timers-onair-remaining, + .rundown-header__show-timers-countdown { + color: #fff; + } + + .rundown-header__clocks-timers__timer__label { + color: rgba(255, 255, 255, 0.9); + } + + &.rehearsal { + background-color: #06090d; + background-image: repeating-linear-gradient( + -45deg, + rgba(255, 255, 255, 0.08) 0, + rgba(255, 255, 255, 0.08) 18px, + transparent 18px, + transparent 36px + ); + border-bottom: 1px solid #256b91; + } + } + + .rundown-header__content { + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + background: transparent; + } + + .rundown-header__left { + display: flex; + align-items: center; + flex: 1 1 0px; + min-width: 0; + + .rundown-header__left-context-menu-wrapper { + display: flex; + align-items: center; + height: 100%; + } + } + + .rundown-header__right { + display: flex; + align-items: center; + justify-content: flex-end; + flex: 1 1 0px; + min-width: 0; + gap: 1em; + } + + .rundown-header__clocks { + display: flex; + align-items: center; + justify-content: center; + flex: none; + min-width: 0; + .timing-clock { + color: #40b8fa; + font-size: 1.4em; + letter-spacing: 0em; + transition: color 0.2s; + + &.time-now { + font-size: 1.8em; + font-variation-settings: + 'wdth' 85, + 'wght' 400, + 'slnt' -5, + 'GRAD' 0, + 'opsz' 44, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + } + } + + .rundown-header__clocks-clock-group { + display: flex; + flex-direction: column; + align-items: center; + } + + .rundown-header__clocks-top-row { + position: relative; + display: flex; + align-items: center; + justify-content: center; + } + + .rundown-header__clocks-playlist-name { + font-size: 0.7em; + font-variation-settings: + 'wdth' 25, + 'wght' 500, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 14, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + display: flex; + flex-direction: row; + justify-content: center; + gap: 0.4em; + max-width: 40em; + color: #fff; + max-height: 0; + padding-top: 0; + overflow: hidden; + transition: + max-height 0.2s ease, + padding-top 0.2s ease; + + .rundown-name, + .playlist-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 0 1 auto; + min-width: 0; + } + .playlist-name { + font-variation-settings: + 'wdth' 25, + 'wght' 700, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 14, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + } + } + + .rundown-header__clocks-time-now { + @extend .countdown--timeofday; + .countdown__value { + margin-left: 0; // Center it since there's no label + } + } + + .rundown-header__clocks-timing-display { + margin-right: 0.5em; + display: flex; + align-items: center; + } + } + + .rundown-header__clocks-diff { + display: flex; + align-items: center; + gap: 0.4em; + font-variant-numeric: tabular-nums; + white-space: nowrap; + + .rundown-header__clocks-diff__label { + @extend .rundown-header__hoverable-label; + font-size: 0.75em; + //opacity: 0.6; + color: #999; + font-variation-settings: + 'wdth' 25, + 'wght' 500, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 14, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + } + + .rundown-header__clocks-diff__chip--under { + font-size: 1.3em; + padding: 0em 0.3em; + line-height: 1em; + border-radius: 999px; + letter-spacing: -0.02em; + font-variation-settings: + 'wdth' 25, + 'wght' 500, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 25, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + } + .rundown-header__clocks-diff__chip--over { + font-size: 1.3em; + padding: 0em 0.3em; + line-height: 1em; + border-radius: 999px; + letter-spacing: -0.02em; + font-variation-settings: + 'wdth' 25, + 'wght' 700, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 25, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + } + + &.rundown-header__clocks-diff--under { + .rundown-header__clocks-diff__chip--under { + background-color: #ff0; // Should probably be changed to $general-fast-color; + color: #000; + } + } + + &.rundown-header__clocks-diff--over { + .rundown-header__clocks-diff__chip--over { + background-color: $general-late-color; + color: #000000; + } + } + } + + .rundown-header__clocks-timers { + margin-left: auto; + display: grid; + grid-template-columns: auto auto; + align-items: baseline; + justify-content: end; + column-gap: 0.3em; + row-gap: 0.1em; + + .rundown-header__clocks-timers__row { + display: contents; + } + + .rundown-header__clocks-timers__timer { + display: contents; + white-space: nowrap; + line-height: 1.25; + + .countdown__label { + @extend .rundown-header__hoverable-label; + margin-left: 0; + text-align: right; + white-space: nowrap; + } + + .countdown__counter { + color: #fff; + margin-left: 0; + display: flex; + align-items: center; + gap: 0; + } + + .countdown__timeofday { + color: #fff; + } + + .rundown-header__clocks-timers__timer__sign { + display: inline-block; + width: 0.6em; + text-align: center; + font-size: 1.1em; + color: #fff; + margin-right: 0em; + } + + .rundown-header__clocks-timers__timer__over-under { + display: inline-block; + line-height: -1em; + font-size: 0.75em; + padding: 0.05em 0.25em; + border-radius: 999px; + white-space: nowrap; + letter-spacing: -0.02em; + margin-left: 0.25em; + margin-top: 0em; + font-variant-numeric: tabular-nums; + font-variation-settings: + 'wdth' 25, + 'wght' 600, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 14, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + + &.rundown-header__clocks-timers__timer__over-under--over { + background-color: $general-late-color; + color: #000; + } + + &.rundown-header__clocks-timers__timer__over-under--under { + background-color: #ff0; + color: #000; + } + } + } + } + + .rundown-header__menu-btn { + background: none; + border: none; + color: #40b8fa99; + cursor: pointer; + padding: 0 1em; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + font-size: 1.2em; + transition: + color 0.2s; + + svg, + i { + filter: drop-shadow(0 0 0 rgba(255, 255, 255, 0)); + } + + &:hover:not(:disabled):not(.disabled) { + svg, + i { + filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.65)); + } + } + + &:focus-visible:not(:disabled):not(.disabled) { + svg, + i { + filter: drop-shadow(0 0 2.5px rgba(255, 255, 255, 0.75)); + } + } + + &:disabled, + &.disabled { + cursor: default; + + svg, + i { + filter: drop-shadow(0 0 0 rgba(255, 255, 255, 0)); + } + } + } + + .rundown-header__onair { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: center; + gap: 0.1em; + } + + // Common label style for header labels that react to hover + .rundown-header__hoverable-label { + @extend %hoverable-label; + } + + .rundown-header__timers-segment-remaining, + .rundown-header__timers-onair-remaining { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.3em; + color: #fff; + transition: color 0.2s; + + &__label { + @extend .rundown-header__hoverable-label; + //opacity: 1; + color: #fff; + position: relative; + top: -0.16em; /* Match alignment from Countdown.scss */ + } + + .countdown__counter { + color: $general-countdown-to-next-color; + } + + .overtime, + .overtime .countdown__counter { + color: $general-late-color; + } + } + + // Stacked Plan. Start / Plan. End / Est. End in right section + .rundown-header__show-timers-endtimes { + display: flex; + flex-direction: column; + justify-content: flex-start; + gap: 0.1em; + min-width: 7em; + } + + .rundown-header__show-timers { + display: flex; + align-items: flex-start; + gap: 1em; + cursor: pointer; + background: none; + border: none; + padding: 0; + margin: 0; + font: inherit; + color: inherit; + text-align: inherit; + + &:hover { + text-shadow: 0 0 5px rgba(255, 255, 255, 1); + } + + &:focus-visible { + text-shadow: 0 0 6px rgba(255, 255, 255, 1); + } + + &.rundown-header__show-timers--disabled { + cursor: default; + + &:hover, + &:focus-visible { + text-shadow: none; + } + } + } + + .rundown-header__show-timers-countdown { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.6em; + transition: color 0.2s; + + .countdown__label { + @extend .rundown-header__hoverable-label; + white-space: nowrap; + } + + .countdown__counter { + margin-left: auto; + font-size: 1.3em; + letter-spacing: 0em; + font-variation-settings: + 'wdth' 60, + 'wght' 550, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 40, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + } + + .countdown__timeofday { + margin-left: auto; + font-size: 1.3em; + font-variant-numeric: tabular-nums; + letter-spacing: 0.02em; + font-variation-settings: + 'wdth' 70, + 'wght' 400, + 'slnt' -5, + 'GRAD' 0, + 'opsz' 40, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + } + } + + .rundown-header__timers-onair-remaining__label { + background-color: var(--general-live-color); + color: #ffffff; + padding: 0.03em 0.45em 0.02em 0.2em; + top: 0em; + border-radius: 2px 999px 999px 2px; + // Label font styling override meant to match the ON AIR label on the On Air line + font-size: 0.8em; + letter-spacing: 0.05em; + font-variation-settings: + 'wdth' 80, + 'wght' 700, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 14, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + + opacity: 1 !important; + + .freeze-frame-icon { + margin-left: 0.3em; + vertical-align: middle; + height: 0.9em; + width: auto; + } + } + + .rundown-header__close-btn { + display: flex; + align-items: center; + margin-right: 0.75em; + cursor: pointer; + color: #40b8fa; + opacity: 0; + flex-shrink: 0; + transition: + opacity 0.2s; + + svg, + i { + filter: drop-shadow(0 0 0 rgba(255, 255, 255, 0)); + } + + &:hover { + svg, + i { + filter: drop-shadow(0 0 2px rgba(255, 255, 255, 0.65)); + } + } + + &:focus-visible { + svg, + i { + filter: drop-shadow(0 0 2.5px rgba(255, 255, 255, 0.75)); + } + } + } + + &:hover { + .rundown-header__menu-btn:not(:disabled) { + color: #40b8fa; + } + + .rundown-header__hoverable-label, + .countdown__label { + opacity: 1; + color: #fff; + } + + .rundown-header__timers-onair-remaining__label { + //opacity: 1; + color: #fff; + } + + .rundown-header__close-btn { + opacity: 1; + } + + .rundown-header__clocks-clock-group { + .rundown-header__clocks-playlist-name { + max-height: 2em; + padding-top: 0em; + } + } + } +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index ffbf52f145a..ebe5e5ca27b 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -1,40 +1,29 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import * as CoreIcon from '@nrk/core-icons/jsx' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import ClassNames from 'classnames' -import Escape from '../../../lib/Escape' -import Tooltip from 'rc-tooltip' import { NavLink } from 'react-router-dom' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { Rundown, getRundownNrcsName } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { ContextMenu, MenuItem, ContextMenuTrigger } from '@jstarpl/react-contextmenu' -import { PieceUi } from '../../SegmentTimeline/SegmentTimelineContainer' -import { RundownSystemStatus } from '../RundownSystemStatus' -import { getHelpMode } from '../../../lib/localStorage' -import { reloadRundownPlaylistClick } from '../RundownNotifier' -import { useRundownViewEventBusListener } from '../../../lib/lib' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownLayoutRundownHeader } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { contextMenuHoldToDisplayTime } from '../../../lib/lib' -import { - ActivateRundownPlaylistEvent, - DeactivateRundownPlaylistEvent, - IEventContext, - RundownViewEvents, -} from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' -import { RundownLayoutsAPI } from '../../../lib/rundownLayouts' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' -import { BucketAdLibItem } from '../../Shelf/RundownViewBuckets' -import { IAdLibListItem } from '../../Shelf/AdLibListItem' -import { ShelfDashboardLayout } from '../../Shelf/ShelfDashboardLayout' import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyles' -import { UserPermissionsContext } from '../../UserPermissions' -import * as RundownResolver from '../../../lib/RundownResolver' import Navbar from 'react-bootstrap/Navbar' -import { WarningDisplay } from '../WarningDisplay' -import { TimingDisplay } from './TimingDisplay' -import { checkRundownTimes, useRundownPlaylistOperations } from './useRundownPlaylistOperations' +import { RundownContextMenu, RundownHeaderContextMenuTrigger, RundownHamburgerButton } from './RundownContextMenu' +import { TimeOfDay } from '../RundownTiming/TimeOfDay' +import { RundownHeaderPartRemaining, RundownHeaderSegmentBudget } from '../RundownHeader/CurrentPartOrSegmentRemaining' +import { RundownHeaderTimers } from './RundownHeaderTimers' + +import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' +import { useTiming } from '../RundownTiming/withTiming' +import { RundownHeaderTimingDisplay } from './RundownHeaderTimingDisplay' +import { RundownHeaderPlannedStart } from './RundownHeaderPlannedStart' +import { RundownHeaderDurations } from './RundownHeaderDurations' +import { RundownHeaderExpectedEnd } from './RundownHeaderExpectedEnd' +import { HeaderFreezeFrameIcon } from './HeaderFreezeFrameIcon' +import './RundownHeader.scss' interface IRundownHeaderProps { playlist: DBRundownPlaylist @@ -44,6 +33,7 @@ interface IRundownHeaderProps { studio: UIStudio rundownIds: RundownId[] firstRundown: Rundown | undefined + rundownCount: number onActivate?: (isRehearsal: boolean) => void inActiveRundownView?: boolean layout: RundownLayoutRundownHeader | undefined @@ -51,110 +41,56 @@ interface IRundownHeaderProps { export function RundownHeader({ playlist, - showStyleBase, - showStyleVariant, - currentRundown, studio, - rundownIds, firstRundown, - inActiveRundownView, - layout, + currentRundown, + rundownCount, }: IRundownHeaderProps): JSX.Element { const { t } = useTranslation() + const timingDurations = useTiming() + const [simplified, setSimplified] = useState(false) + const [isMenuOpen, setIsMenuOpen] = useState(false) + const [isContextMenuOpen, setIsContextMenuOpen] = useState(false) - const userPermissions = useContext(UserPermissionsContext) + const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) + const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) + const expectedEnd = PlaylistTiming.getExpectedEnd(playlist.timing) - const [selectedPiece, setSelectedPiece] = useState(undefined) - const [shouldQueueAdlibs, setShouldQueueAdlibs] = useState(false) + const hasSimple = !!(expectedStart || expectedDuration || expectedEnd) - const operations = useRundownPlaylistOperations() + // Fallback duration for untimed playlists + const fallbackDuration = PlaylistTiming.isPlaylistTimingNone(playlist.timing) + ? Object.values(timingDurations.partExpectedDurations || {}).reduce((a, b) => a + b, 0) + : undefined - const eventActivate = useCallback( - (e: ActivateRundownPlaylistEvent) => { - if (e.rehearsal) { - operations.activateRehearsal(e.context) - } else { - operations.activate(e.context) - } - }, - [operations] - ) - const eventDeactivate = useCallback( - (e: DeactivateRundownPlaylistEvent) => operations.deactivate(e.context), - [operations] + const hasAdvanced = !!( + playlist.startedPlayback || + expectedStart || + timingDurations.remainingPlaylistDuration || + fallbackDuration ) - const eventResync = useCallback((e: IEventContext) => operations.reloadRundownPlaylist(e.context), [operations]) - const eventTake = useCallback((e: IEventContext) => operations.take(e.context), [operations]) - const eventResetRundownPlaylist = useCallback((e: IEventContext) => operations.resetRundown(e.context), [operations]) - const eventCreateSnapshot = useCallback((e: IEventContext) => operations.takeRundownSnapshot(e.context), [operations]) - useRundownViewEventBusListener(RundownViewEvents.ACTIVATE_RUNDOWN_PLAYLIST, eventActivate) - useRundownViewEventBusListener(RundownViewEvents.DEACTIVATE_RUNDOWN_PLAYLIST, eventDeactivate) - useRundownViewEventBusListener(RundownViewEvents.RESYNC_RUNDOWN_PLAYLIST, eventResync) - useRundownViewEventBusListener(RundownViewEvents.TAKE, eventTake) - useRundownViewEventBusListener(RundownViewEvents.RESET_RUNDOWN_PLAYLIST, eventResetRundownPlaylist) - useRundownViewEventBusListener(RundownViewEvents.CREATE_SNAPSHOT_FOR_DEBUG, eventCreateSnapshot) + const canToggle = simplified ? hasAdvanced : hasSimple + const toggleSimplified = useCallback(() => { + if (canToggle) { + setSimplified((s) => !s) + } + }, [canToggle]) - useEffect(() => { - reloadRundownPlaylistClick.set(operations.reloadRundownPlaylist) - }, [operations.reloadRundownPlaylist]) - - const canClearQuickLoop = - !!studio.settings.enableQuickLoop && - !RundownResolver.isLoopLocked(playlist) && - RundownResolver.isAnyLoopMarkerDefined(playlist) - - const rundownTimesInfo = checkRundownTimes(playlist.timing) + const onMenuClose = useCallback(() => setIsMenuOpen(false), [setIsMenuOpen]) return ( <> - - -
    {playlist && playlist.name}
    - {userPermissions.studio ? ( - - {!(playlist.activationId && playlist.rehearsal) ? ( - !rundownTimesInfo.shouldHaveStarted && !playlist.activationId ? ( - - {t('Prepare Studio and Activate (Rehearsal)')} - - ) : ( - {t('Activate (Rehearsal)')} - ) - ) : ( - {t('Activate (On-Air)')} - )} - {rundownTimesInfo.willShortlyStart && !playlist.activationId && ( - {t('Activate (On-Air)')} - )} - {playlist.activationId ? {t('Deactivate')} : null} - {studio.settings.allowAdlibTestingSegment && playlist.activationId ? ( - {t('AdLib Testing')} - ) : null} - {playlist.activationId ? {t('Take')} : null} - {studio.settings.allowHold && playlist.activationId ? ( - {t('Hold')} - ) : null} - {playlist.activationId && canClearQuickLoop ? ( - {t('Clear QuickLoop')} - ) : null} - {!(playlist.activationId && !playlist.rehearsal && !studio.settings.allowRundownResetOnAir) ? ( - {t('Reset Rundown')} - ) : null} - - {t('Reload {{nrcsName}} Data', { - nrcsName: getRundownNrcsName(firstRundown), - })} - - {t('Store Snapshot')} - - ) : ( - - {t('No actions available')} - - )} -
    -
    + setIsContextMenuOpen(true)} + onHide={() => { + setIsMenuOpen(false) + setIsContextMenuOpen(false) + }} + /> - - - noResetOnActivate ? operations.activateRundown(e) : operations.resetAndActivateRundown(e) - } - /> -
    -
    -
    - -
    - + +
    +
    + setIsMenuOpen(true)} + onClose={onMenuClose} + /> +
    + {playlist.currentPartInfo && ( +
    + + + {t('On Air')} + + + +
    + )} +
    - {layout && RundownLayoutsAPI.isDashboardLayout(layout) ? ( - - ) : ( - <> - - - - )} -
    -
    - - - + +
    +
    +
    + + +
    +
    + {rundownCount > 1 ? ( + {playlist.name} + ) : ( + {(currentRundown ?? firstRundown)?.name} + )} +
    + +
    + + + + +
    - + ) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx new file mode 100644 index 00000000000..b49e21f09b7 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx @@ -0,0 +1,39 @@ +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' +import { useTranslation } from 'react-i18next' +import { Countdown } from './Countdown' +import { useTiming } from '../RundownTiming/withTiming' +import { RundownUtils } from '../../../lib/rundown' + +export function RundownHeaderDurations({ + playlist, + simplified, +}: { + readonly playlist: DBRundownPlaylist + readonly simplified?: boolean +}): JSX.Element | null { + const { t } = useTranslation() + const timingDurations = useTiming() + + const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) + + // Use remainingPlaylistDuration which includes current part's remaining time + const estDuration = timingDurations.remainingPlaylistDuration + + if (expectedDuration == undefined && estDuration == undefined) return null + + return ( +
    + {!simplified && expectedDuration ? ( + + {RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, true, true)} + + ) : null} + {estDuration !== undefined ? ( + + {RundownUtils.formatDiffToTimecode(estDuration, false, true, true, true, true)} + + ) : null} +
    + ) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx new file mode 100644 index 00000000000..8ab25709827 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -0,0 +1,36 @@ +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' +import { useTranslation } from 'react-i18next' +import { Countdown } from './Countdown' +import { useTiming } from '../RundownTiming/withTiming' + +export function RundownHeaderExpectedEnd({ + playlist, + simplified, +}: { + readonly playlist: DBRundownPlaylist + readonly simplified?: boolean +}): JSX.Element | null { + const { t } = useTranslation() + const timingDurations = useTiming() + + const expectedEnd = PlaylistTiming.getExpectedEnd(playlist.timing) + const now = timingDurations.currentTime ?? Date.now() + + // Use remainingPlaylistDuration which includes current part's remaining time + const estEnd = + timingDurations.remainingPlaylistDuration !== undefined ? now + timingDurations.remainingPlaylistDuration : null + + if (expectedEnd === undefined && estEnd === null) return null + + return ( +
    + {!simplified && expectedEnd !== undefined ? ( + + ) : null} + {estEnd !== null ? ( + + ) : null} +
    + ) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx new file mode 100644 index 00000000000..9d0324413f9 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx @@ -0,0 +1,39 @@ +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' +import { useTranslation } from 'react-i18next' +import { Countdown } from './Countdown' +import { useTiming } from '../RundownTiming/withTiming' +import { RundownUtils } from '../../../lib/rundown' + +export function RundownHeaderPlannedStart({ + playlist, + simplified, +}: { + playlist: DBRundownPlaylist + simplified?: boolean +}): JSX.Element | null { + const { t } = useTranslation() + const timingDurations = useTiming() + const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) + + if (expectedStart == null) return null + + const now = timingDurations.currentTime ?? Date.now() + const diff = now - expectedStart + + return ( +
    + {!simplified && expectedStart !== undefined ? ( + + ) : null} + {playlist.startedPlayback !== undefined ? ( + + ) : ( + + {diff >= 0 && '+'} + {RundownUtils.formatDiffToTimecode(Math.abs(diff), false, false, true, true, true)} + + )} +
    + ) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx new file mode 100644 index 00000000000..8a699b89957 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -0,0 +1,79 @@ +import React from 'react' +import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { useTiming } from '../RundownTiming/withTiming' +import { RundownUtils } from '../../../lib/rundown' +import { calculateTTimerDiff, calculateTTimerOverUnder } from '../../../lib/tTimerUtils' +import classNames from 'classnames' +import { getCurrentTime } from '../../../lib/systemTime' +import { Countdown } from './Countdown' + +interface IProps { + tTimers: [RundownTTimer, RundownTTimer, RundownTTimer] +} + +export const RundownHeaderTimers: React.FC = ({ tTimers }) => { + useTiming() + + const activeTimers = tTimers.filter((t) => t.mode).slice(0, 2) + if (activeTimers.length == 0) return null + + return ( +
    + {activeTimers.map((timer) => ( +
    + +
    + ))} +
    + ) +} + +interface ISingleTimerProps { + timer: RundownTTimer +} + +function SingleTimer({ timer }: Readonly) { + const now = getCurrentTime() + const mode = timer.mode + if (!mode) return null + const isRunning = !!timer.state && !timer.state.paused + + const diff = calculateTTimerDiff(timer, now) + const overUnder = calculateTTimerOverUnder(timer, now) + const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(diff), false, true, true, false, true) + const isCountingDown = mode.type === 'countdown' && diff < 0 && isRunning + + return ( + 0, + 'rundown-header__clocks-timers__timer__over-under--under': overUnder < 0, + })} + > + {overUnder > 0 ? '+' : '−'} + {RundownUtils.formatDiffToTimecode(Math.abs(overUnder), false, false, true, false, true)} + + ) : undefined + } + > + {timeStr} + + ) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx new file mode 100644 index 00000000000..3fc8e6eb78a --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx @@ -0,0 +1,43 @@ +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' +import { useTranslation } from 'react-i18next' +import { useTiming } from '../RundownTiming/withTiming' +import { getPlaylistTimingDiff } from '../../../lib/rundownTiming' +import { RundownUtils } from '../../../lib/rundown' + +export interface IRundownHeaderTimingDisplayProps { + playlist: DBRundownPlaylist +} + +export function RundownHeaderTimingDisplay({ playlist }: IRundownHeaderTimingDisplayProps): JSX.Element | null { + const { t } = useTranslation() + const timingDurations = useTiming() + + const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) + + if (overUnderClock === undefined) return null + + // Hide diff in untimed mode before first timing take + if (PlaylistTiming.isPlaylistTimingNone(playlist.timing) && !playlist.startedPlayback) { + return null + } + + const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(overUnderClock), false, false, true, true, true) + const isUnder = overUnderClock <= 0 + + return ( +
    + + {isUnder ? t('Under') : t('Over')} + + {isUnder ? '−' : '+'} + {timeStr} + + +
    + ) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/TimingDisplay.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/TimingDisplay.tsx deleted file mode 100644 index 53c91346422..00000000000 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/TimingDisplay.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { DBRundownPlaylist, RundownHoldState } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' -import { RundownLayoutRundownHeader } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { useTranslation } from 'react-i18next' -import * as RundownResolver from '../../../lib/RundownResolver' -import { AutoNextStatus } from '../RundownTiming/AutoNextStatus' -import { CurrentPartOrSegmentRemaining } from '../RundownTiming/CurrentPartOrSegmentRemaining' -import { NextBreakTiming } from '../RundownTiming/NextBreakTiming' -import { PlaylistEndTiming } from '../RundownTiming/PlaylistEndTiming' -import { PlaylistStartTiming } from '../RundownTiming/PlaylistStartTiming' -import { RundownName } from '../RundownTiming/RundownName' -import { TimeOfDay } from '../RundownTiming/TimeOfDay' -import { useTiming } from '../RundownTiming/withTiming' - -interface ITimingDisplayProps { - rundownPlaylist: DBRundownPlaylist - currentRundown: Rundown | undefined - rundownCount: number - layout: RundownLayoutRundownHeader | undefined -} -export function TimingDisplay({ - rundownPlaylist, - currentRundown, - rundownCount, - layout, -}: ITimingDisplayProps): JSX.Element | null { - const { t } = useTranslation() - - const timingDurations = useTiming() - - if (!rundownPlaylist) return null - - const expectedStart = PlaylistTiming.getExpectedStart(rundownPlaylist.timing) - const expectedEnd = PlaylistTiming.getExpectedEnd(rundownPlaylist.timing) - const expectedDuration = PlaylistTiming.getExpectedDuration(rundownPlaylist.timing) - const showEndTiming = - !timingDurations.rundownsBeforeNextBreak || - !layout?.showNextBreakTiming || - (timingDurations.rundownsBeforeNextBreak.length > 0 && - (!layout?.hideExpectedEndBeforeBreak || (timingDurations.breakIsLastRundown && layout?.lastRundownIsNotBreak))) - const showNextBreakTiming = - rundownPlaylist.startedPlayback && - timingDurations.rundownsBeforeNextBreak?.length && - layout?.showNextBreakTiming && - !(timingDurations.breakIsLastRundown && layout.lastRundownIsNotBreak) - - return ( -
    -
    - - -
    -
    - -
    -
    -
    - {rundownPlaylist.currentPartInfo && ( - - - - {rundownPlaylist.holdState && rundownPlaylist.holdState !== RundownHoldState.COMPLETE ? ( -
    {t('Hold')}
    - ) : null} -
    - )} -
    -
    - {showNextBreakTiming ? ( - - ) : null} - {showEndTiming ? ( - - ) : null} -
    -
    -
    - ) -} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/_shared.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/_shared.scss new file mode 100644 index 00000000000..6ae886e67bb --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/_shared.scss @@ -0,0 +1,25 @@ +// Shared placeholder used by both RundownHeader.scss and Countdown.scss. +// Extracted to break the circular @import dependency. + +%hoverable-label { + font-size: 0.75em; + font-variation-settings: + 'wdth' 25, + 'wght' 500, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 14, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; + letter-spacing: 0.01em; + text-transform: uppercase; + opacity: 1; + color: #888; + transition: color 0.2s; +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/useRundownPlaylistOperations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/useRundownPlaylistOperations.tsx index 8f29d6e7ce0..c4d5a3b64db 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/useRundownPlaylistOperations.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/useRundownPlaylistOperations.tsx @@ -15,7 +15,7 @@ import { NoticeLevel, Notification, NotificationCenter } from '../../../lib/noti import { Meteor } from 'meteor/meteor' import { Tracker } from 'meteor/tracker' import RundownViewEventBus, { RundownViewEvents } from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' -import { handleRundownPlaylistReloadResponse } from './RundownReloadResponse' +import { handleRundownPlaylistReloadResponse } from './RundownReloadResponse.js' import { scrollToPartInstance } from '../../../lib/viewPort' import { hashSingleUseToken } from '../../../lib/lib' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' diff --git a/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx b/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx index ba7328a21e3..906a22a3eb2 100644 --- a/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx @@ -21,11 +21,10 @@ import { ReactiveVar } from 'meteor/reactive-var' import { Rundown, getRundownNrcsName } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { doModalDialog } from '../../lib/ModalDialog.js' import { doUserAction, UserAction } from '../../lib/clientUserAction.js' -// import { withTranslation, getI18n, getDefaults } from 'react-i18next' import { i18nTranslator as t } from '../i18n.js' import { PieceStatusCode } from '@sofie-automation/corelib/dist/dataModel/Piece' import { PeripheralDevicesAPI } from '../../lib/clientAPI.js' -import { handleRundownReloadResponse } from '../RundownView/RundownHeader/RundownReloadResponse.js' +import { handleRundownReloadResponse } from './RundownHeader/RundownReloadResponse.js' import { MeteorCall } from '../../lib/meteorApi.js' import { UISegmentPartNote } from '@sofie-automation/meteor-lib/dist/api/rundownNotifications' import { isTranslatableMessage, translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' @@ -535,17 +534,15 @@ class RundownViewNotifier extends WithManagedTracker { const newNotification = new Notification( notificationId, getNoticeLevelForNoteSeverity(itemType), - ( - <> - {name || segmentName ? ( -
    - {segmentName || name} - {segmentName && name ? `${SEGMENT_DELIMITER}${name}` : null} -
    - ) : null} -
    {translatedMessage || t('There is an unknown problem with the part.')}
    - - ), + <> + {name || segmentName ? ( +
    + {segmentName || name} + {segmentName && name ? `${SEGMENT_DELIMITER}${name}` : null} +
    + ) : null} +
    {translatedMessage || t('There is an unknown problem with the part.')}
    + , origin.segmentId || origin.rundownId || 'unknown', getCurrentTime(), true, @@ -613,20 +610,18 @@ class RundownViewNotifier extends WithManagedTracker { newNotification = new Notification( issue.pieceId, getNoticeLevelForPieceStatus(status) || NoticeLevel.WARNING, - ( - <> -
    {messageName}
    -
    - {messages.map((msg, index) => ( - - {translateMessage(msg, t)} -
    -
    - ))} - {messages.length === 0 && t('There is an unspecified problem with the source.')} -
    - - ), + <> +
    {messageName}
    +
    + {messages.map((msg, index) => ( + + {translateMessage(msg, t)} +
    +
    + ))} + {messages.length === 0 && t('There is an unspecified problem with the source.')} +
    + , issue.segmentId ? issue.segmentId : 'line_' + issue.partId, getCurrentTime(), true, diff --git a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx index 775d55c326f..5ebb40591f8 100644 --- a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx @@ -7,11 +7,11 @@ import { } from '@sofie-automation/corelib/dist/dataModel/Studio' import { RewindAllSegmentsIcon } from '../../lib/ui/icons/rewindAllSegmentsIcon.js' -import { Lottie } from '@crello/react-lottie' +import Lottie, { LottieComponentProps } from 'lottie-react' import { NotificationCenterPanelToggle } from '../../lib/notifications/NotificationCenterPanel.js' -import * as On_Air_MouseOut from './On_Air_MouseOut.json' -import * as On_Air_MouseOver from './On_Air_MouseOver.json' +import On_Air_MouseOut from './On_Air_MouseOut.json' +import On_Air_MouseOver from './On_Air_MouseOver.json' import { SupportPopUpToggle } from '../SupportPopUp.js' import classNames from 'classnames' import { NoticeLevel } from '../../lib/notifications/notifications.js' @@ -54,7 +54,7 @@ interface IProps { hideRundownHeader?: boolean } -const ANIMATION_TEMPLATE = { +const ANIMATION_TEMPLATE: LottieComponentProps = { loop: false, autoplay: true, animationData: {}, @@ -63,11 +63,11 @@ const ANIMATION_TEMPLATE = { }, } -const ONAIR_OUT = { +const ONAIR_OUT: LottieComponentProps = { ...ANIMATION_TEMPLATE, animationData: On_Air_MouseOut, } -const ONAIR_OVER = { +const ONAIR_OVER: LottieComponentProps = { ...ANIMATION_TEMPLATE, animationData: On_Air_MouseOver, } @@ -141,7 +141,7 @@ export function RundownRightHandControls(props: Readonly): JSX.Element { )}
    @@ -195,7 +195,7 @@ export function RundownRightHandControls(props: Readonly): JSX.Element { tabIndex={0} aria-label={t('Go to On Air Segment')} > - {onAirHover ? : } + {onAirHover ? : } )}
    diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/CurrentPartOrSegmentRemaining.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/CurrentPartOrSegmentRemaining.tsx deleted file mode 100644 index 1322e9bb32e..00000000000 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/CurrentPartOrSegmentRemaining.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import * as React from 'react' -import ClassNames from 'classnames' -import { TimingDataResolution, TimingTickResolution, withTiming, WithTiming } from './withTiming.js' -import { RundownUtils } from '../../../lib/rundown.js' -import { SpeechSynthesiser } from '../../../lib/speechSynthesis.js' -import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' - -const SPEAK_ADVANCE = 500 - -interface IPartRemainingProps { - currentPartInstanceId: PartInstanceId | null - hideOnZero?: boolean - className?: string - heavyClassName?: string - speaking?: boolean - vibrating?: boolean - /** Use the segment budget instead of the part duration if available */ - preferSegmentTime?: boolean -} - -// global variable for remembering last uttered displayTime -let prevDisplayTime: number | undefined = undefined - -/** - * A presentational component that will render a countdown to the end of the current part or segment, - * depending on the value of segmentTiming.countdownType - * - * @class CurrentPartOrSegmentRemaining - * @extends React.Component> - */ -export const CurrentPartOrSegmentRemaining = withTiming({ - tickResolution: TimingTickResolution.Synced, - dataResolution: TimingDataResolution.Synced, -})( - class CurrentPartOrSegmentRemaining extends React.Component> { - render(): JSX.Element | null { - if (!this.props.timingDurations || !this.props.timingDurations.currentTime) return null - if (this.props.timingDurations.currentPartInstanceId !== this.props.currentPartInstanceId) return null - let displayTimecode = this.props.timingDurations.remainingTimeOnCurrentPart - if (this.props.preferSegmentTime) - displayTimecode = this.props.timingDurations.remainingBudgetOnCurrentSegment ?? displayTimecode - if (displayTimecode === undefined) return null - displayTimecode *= -1 - return ( - 0 ? this.props.heavyClassName : undefined - )} - role="timer" - > - {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} - - ) - } - - speak(displayTime: number) { - let text = '' // Say nothing - - switch (displayTime) { - case -1: - text = 'One' - break - case -2: - text = 'Two' - break - case -3: - text = 'Three' - break - case -4: - text = 'Four' - break - case -5: - text = 'Five' - break - case -6: - text = 'Six' - break - case -7: - text = 'Seven' - break - case -8: - text = 'Eight' - break - case -9: - text = 'Nine' - break - case -10: - text = 'Ten' - break - } - // if (displayTime === 0 && prevDisplayTime !== undefined) { - // text = 'Zero' - // } - - if (text) { - SpeechSynthesiser.speak(text, 'countdown') - } - } - - vibrate(displayTime: number) { - if ('vibrate' in navigator) { - switch (displayTime) { - case 0: - navigator.vibrate([500]) - break - case -1: - case -2: - case -3: - navigator.vibrate([250]) - break - } - } - } - - act() { - // Note that the displayTime is negative when counting down to 0. - let displayTime = (this.props.timingDurations.remainingTimeOnCurrentPart || 0) * -1 - - if (displayTime === 0) { - // do nothing - } else { - displayTime += SPEAK_ADVANCE - displayTime = Math.floor(displayTime / 1000) - } - - if (prevDisplayTime !== displayTime) { - if (this.props.speaking) { - this.speak(displayTime) - } - - if (this.props.vibrating) { - this.vibrate(displayTime) - } - - prevDisplayTime = displayTime - } - } - - componentDidUpdate(prevProps: WithTiming) { - if (this.props.currentPartInstanceId !== prevProps.currentPartInstanceId) { - prevDisplayTime = undefined - } - this.act() - } - } -) diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx index 47f205ffd76..5fcdeb82f25 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx @@ -1,12 +1,15 @@ import { useTiming } from './withTiming.js' import Moment from 'react-moment' +import classNames from 'classnames' -export function TimeOfDay(): JSX.Element { +export function TimeOfDay({ className }: Readonly<{ className?: string }>): JSX.Element { const timingDurations = useTiming() return ( - - + + + + ) } diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/withTiming.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/withTiming.tsx index 4b739604e9d..76d70b0c596 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/withTiming.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/withTiming.tsx @@ -56,6 +56,7 @@ export const RundownTimingProviderContext = React.createContext WithTimingOptions))} [options] The options object or the options object generator * @return (WrappedComponent: IWrappedComponent) => * new (props: IProps, context: any ) => React.Component + * @deprecated use the `useTiming` hook instead */ export function withTiming( options?: Partial | ((props: IProps) => Partial) @@ -166,6 +167,22 @@ function getFilterFunction( return undefined } +/** + * React hook that subscribes to rundown timing events and returns the + * currently selected timing data. + * + * The hook listens for timing update events determined by `tickResolution`. + * It returns timing data selected by `dataResolution` (high-resolution or + * synced). When a `filter` is provided (function, property path string, or + * array of path segments), the hook will only trigger re-renders if the + * value returned by the filter changes between ticks; otherwise it will + * update on every timing event for the chosen `tickResolution`. + * + * @param tickResolution - which timing event resolution to subscribe to + * @param dataResolution - whether to return `High` or `Synced` timing data + * @param filter - optional selector (function | property path | path array) + * @returns the appropriate `RundownTimingContext` for the selected resolution + */ export function useTiming( tickResolution: TimingTickResolution = TimingTickResolution.Synced, dataResolution: TimingDataResolution = TimingDataResolution.Synced, @@ -195,7 +212,7 @@ export function useTiming( setForceUpdate(Date.now()) } } - }, []) + }, [context]) useEffect(() => { window.addEventListener(rundownTimingEventFromTickResolution(tickResolution), refreshComponent) diff --git a/packages/webui/src/client/ui/RundownView/RundownViewContextProviders.tsx b/packages/webui/src/client/ui/RundownView/RundownViewContextProviders.tsx index a5eb33429e9..4dea007a6dc 100644 --- a/packages/webui/src/client/ui/RundownView/RundownViewContextProviders.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownViewContextProviders.tsx @@ -1,7 +1,7 @@ import React from 'react' import { RundownTimingProvider } from './RundownTiming/RundownTimingProvider' import StudioContext from './StudioContext' -import { RundownPlaylistOperationsContextProvider } from './RundownHeader/useRundownPlaylistOperations' +import { RundownPlaylistOperationsContextProvider } from './RundownHeader/useRundownPlaylistOperations.js' import { PreviewPopUpContextProvider } from '../PreviewPopUp/PreviewPopUpContext' import { SelectedElementProvider } from './SelectedElementsContext' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' diff --git a/packages/webui/src/client/ui/SegmentList/LinePart.tsx b/packages/webui/src/client/ui/SegmentList/LinePart.tsx index 0c89801e0da..014618f7267 100644 --- a/packages/webui/src/client/ui/SegmentList/LinePart.tsx +++ b/packages/webui/src/client/ui/SegmentList/LinePart.tsx @@ -7,7 +7,7 @@ import { contextMenuHoldToDisplayTime } from '../../lib/lib.js' import { RundownUtils } from '../../lib/rundown.js' import { getElementDocumentOffset } from '../../utils/positions.js' import { IContextMenuContext } from '../RundownView.js' -import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js' +import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js' import { PieceUi, SegmentUi } from '../SegmentContainer/withResolvedSegment.js' import { SegmentTimelinePartElementId } from '../SegmentTimeline/Parts/SegmentTimelinePart.js' import { LinePartIdentifier } from './LinePartIdentifier.js' diff --git a/packages/webui/src/client/ui/SegmentList/OnAirLine.tsx b/packages/webui/src/client/ui/SegmentList/OnAirLine.tsx index cbbd9b86c0f..f353f6edbb4 100644 --- a/packages/webui/src/client/ui/SegmentList/OnAirLine.tsx +++ b/packages/webui/src/client/ui/SegmentList/OnAirLine.tsx @@ -4,7 +4,7 @@ import { SIMULATED_PLAYBACK_HARD_MARGIN } from '../SegmentTimeline/Constants.js' import { PartInstanceLimited } from '../../lib/RundownResolver.js' import { useTranslation } from 'react-i18next' import { getAllowSpeaking, getAllowVibrating } from '../../lib/localStorage.js' -import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js' +import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js' import { AutoNextStatus } from '../RundownView/RundownTiming/AutoNextStatus.js' import classNames from 'classnames' diff --git a/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx b/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx index f8e65fbe38b..7a791cbe5d9 100644 --- a/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx +++ b/packages/webui/src/client/ui/SegmentList/SegmentListHeader.tsx @@ -197,7 +197,7 @@ export function SegmentListHeader({ 'time-of-day-countdowns': useTimeOfDayCountdowns, - 'no-rundown-header': hideRundownHeader, + 'no-rundown-header_OLD': hideRundownHeader, })} > {contents} diff --git a/packages/webui/src/client/ui/SegmentStoryboard/StoryboardPart.tsx b/packages/webui/src/client/ui/SegmentStoryboard/StoryboardPart.tsx index 3a3b4202a8e..32a94dc407c 100644 --- a/packages/webui/src/client/ui/SegmentStoryboard/StoryboardPart.tsx +++ b/packages/webui/src/client/ui/SegmentStoryboard/StoryboardPart.tsx @@ -11,7 +11,7 @@ import { getElementDocumentOffset } from '../../utils/positions.js' import { IContextMenuContext } from '../RundownView.js' import { literal } from '@sofie-automation/corelib/dist/lib' import { SegmentTimelinePartElementId } from '../SegmentTimeline/Parts/SegmentTimelinePart.js' -import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js' +import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js' import { getAllowSpeaking, getAllowVibrating } from '../../lib/localStorage.js' import { HighlightEvent, RundownViewEvents } from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' import { Meteor } from 'meteor/meteor' diff --git a/packages/webui/src/client/ui/SegmentTimeline/Parts/FlattenedSourceLayers.tsx b/packages/webui/src/client/ui/SegmentTimeline/Parts/FlattenedSourceLayers.tsx index 0c000353d48..0cc49b48829 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/Parts/FlattenedSourceLayers.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/Parts/FlattenedSourceLayers.tsx @@ -19,7 +19,7 @@ export function FlattenedSourceLayers(props: Readonly onMouseDown(e), + onMouseDownCapture: (e) => onMouseDown(e), role: 'log', 'aria-live': 'assertive', 'aria-label': props.outputLayer.name, diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx index 0118421bd10..02f020cc935 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import Escape from './../../lib/Escape.js' -import { withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { ContextMenu, MenuItem } from '@jstarpl/react-contextmenu' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { @@ -8,19 +8,18 @@ import { QuickLoopMarker, QuickLoopMarkerType, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData.js' import { RundownUtils } from '../../lib/rundown.js' import { IContextMenuContext } from '../RundownView.js' -import { PartUi, SegmentUi } from './SegmentTimelineContainer.js' -import { SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartInstanceId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { UserEditOperationMenuItems } from '../UserEditOperations/RenderUserEditOperations.js' +import { CoreUserEditingDefinition } from '@sofie-automation/corelib/dist/dataModel/UserEditingDefinitions' import * as RundownResolver from '../../lib/RundownResolver.js' import { SelectedElement } from '../RundownView/SelectedElementsContext.js' -import { PieceExtended } from '../../lib/RundownResolver.js' +import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance.js' interface IProps { - onSetNext: (part: DBPart | undefined, e: any, offset?: number, take?: boolean) => void + onSetNext: (partInstance: DBPartInstance | DBPart | undefined, e: any, offset?: number, take?: boolean) => void onSetNextSegment: (segmentId: SegmentId, e: any) => void onQueueNextSegment: (segmentId: SegmentId | null, e: any) => void onSetQuickLoopStart: (marker: QuickLoopMarker | null, e: any) => void @@ -33,252 +32,279 @@ interface IProps { enableQuickLoop: boolean enableUserEdits: boolean } -interface IState {} -export const SegmentContextMenu = withTranslation()( - class SegmentContextMenu extends React.Component, IState> { - constructor(props: Translated) { - super(props) +export function SegmentContextMenu({ + onSetNext, + onSetNextSegment, + onQueueNextSegment, + onSetQuickLoopStart, + onSetQuickLoopEnd, + onEditProps, + playlist, + studioMode, + contextMenuContext, + enablePlayFromAnywhere, + enableQuickLoop, + enableUserEdits, +}: IProps): JSX.Element | null { + const { t } = useTranslation() + + if (!studioMode || !playlist || (!enableUserEdits && !playlist.activationId)) return null + + const getTimePosition = (): number | null => { + let offset = 0 + if (contextMenuContext && contextMenuContext.partDocumentOffset) { + const left = contextMenuContext.partDocumentOffset.left || 0 + const timeScale = contextMenuContext.timeScale || 1 + const menuPosition = contextMenuContext.mousePosition || { left } + offset = (menuPosition.left - left) / timeScale + return offset } + return null + } + + const getIsPlayFromHereDisabled = (take: boolean = false): boolean => { + const offset = getTimePosition() ?? 0 + const partInstance = part?.instance + const isSelectedTimeWithinBounds = + (partInstance?.part.expectedDuration ?? + partInstance?.part.displayDuration ?? + partInstance?.part.expectedDurationWithTransition ?? + 0) < offset - render(): JSX.Element | null { - const { t } = this.props + if (playlist && playlist?.activationId && (!take || !!partInstance?.orphaned)) { + if (!partInstance) return true + else { + return ( + (isSelectedTimeWithinBounds && partInstance._id === playlist.currentPartInfo?.partInstanceId) || + (!!partInstance.orphaned && partInstance._id === playlist.currentPartInfo?.partInstanceId) + ) + } + } + return false + } - if ( - !this.props.studioMode || - !this.props.playlist || - (!this.props.enableUserEdits && !this.props.playlist.activationId) - ) - return null + const onSetAsNextFromHere = ( + partInstance: DBPartInstance, + nextPartInstanceId: PartInstanceId | null, + currentPartInstanceId: PartInstanceId | null, + e: React.MouseEvent | React.TouchEvent, + take: boolean = false + ) => { + const partInstanceAvailableForPlayout = partInstance.timings?.take !== undefined + const isCurrentPartInstance = partInstance._id === currentPartInstanceId + const isNextInstance = partInstance._id === nextPartInstanceId + const offset = getTimePosition() + onSetNext( + (partInstanceAvailableForPlayout && !isCurrentPartInstance) || isNextInstance ? partInstance : partInstance.part, + e, + offset || 0, + take + ) + } - const piece = this.getPieceFromContext() - const part = this.getPartFromContext() - const segment = this.getSegmentFromContext() - const timecode = this.getTimePosition() - const startsAt = this.getPartStartsAt() + const piece = contextMenuContext?.piece + const part = contextMenuContext?.part + const segment = contextMenuContext?.segment + const timecode = getTimePosition() + const startsAt = contextMenuContext?.partStartsAt - const isCurrentPart = - (part && this.props.playlist && part.instance._id === this.props.playlist.currentPartInfo?.partInstanceId) || - undefined + const isCurrentPart = + (part && playlist && part.instance._id === playlist.currentPartInfo?.partInstanceId) || undefined - const isSegmentEditAble = segment?._id !== this.props.playlist.queuedSegmentId + const isSegmentEditAble = segment?._id !== playlist.queuedSegmentId - const isPartEditAble = - isSegmentEditAble && - part?.instance._id !== this.props.playlist.currentPartInfo?.partInstanceId && - part?.instance._id !== this.props.playlist.nextPartInfo?.partInstanceId && - part?.instance._id !== this.props.playlist.previousPartInfo?.partInstanceId + const isPartEditAble = + isSegmentEditAble && + part?.instance._id !== playlist.currentPartInfo?.partInstanceId && + part?.instance._id !== playlist.nextPartInfo?.partInstanceId && + part?.instance._id !== playlist.previousPartInfo?.partInstanceId - const canSetAsNext = !!this.props.playlist?.activationId + const isPartOrphaned: boolean | undefined = part ? part.instance.orphaned !== undefined : undefined - return segment?.orphaned !== SegmentOrphanedReason.ADLIB_TESTING ? ( - - - {part && timecode === null && ( + const isPartNext: boolean | undefined = part ? playlist.nextPartInfo?.partInstanceId === part.instance._id : undefined + + const canSetAsNext = !!playlist?.activationId + + return segment?.orphaned !== SegmentOrphanedReason.ADLIB_TESTING ? ( + + + {part && timecode === null && ( + <> + onSetNextSegment(part.instance.segmentId, e)} + disabled={isCurrentPart || !canSetAsNext} + > + Next') }}> + + {part.instance.segmentId !== playlist.queuedSegmentId ? ( + onQueueNextSegment(part.instance.segmentId, e)} disabled={!canSetAsNext}> + {t('Queue segment')} + + ) : ( + onQueueNextSegment(null, e)} disabled={!canSetAsNext}> + {t('Clear queued segment')} + + )} + {segment && ( + + )} + {enableUserEdits && ( <> - this.props.onSetNextSegment(part.instance.segmentId, e)} - disabled={isCurrentPart || !canSetAsNext} - > - Next') }}> +
    + onEditProps({ type: 'segment', elementId: part.instance.segmentId })}> + {t('Edit Segment Properties')} - {part.instance.segmentId !== this.props.playlist.queuedSegmentId ? ( + + )} +
    + + )} + {part && + isPartNext !== undefined && + isPartOrphaned !== undefined && + !part.instance.part.invalid && + timecode !== null && ( + <> + onSetNext(part.instance.part, e)} + disabled={!!part.instance.orphaned || !canSetAsNext} + > + Next`), + }} + > + + {startsAt !== undefined && part && enablePlayFromAnywhere ? ( + <> this.props.onQueueNextSegment(part.instance.segmentId, e)} - disabled={!canSetAsNext} + onClick={(e) => + onSetAsNextFromHere( + part.instance, + playlist?.nextPartInfo?.partInstanceId ?? null, + playlist?.currentPartInfo?.partInstanceId ?? null, + e + ) + } + disabled={getIsPlayFromHereDisabled()} > - {t('Queue segment')} + Next` + ), + }} + > - ) : ( - this.props.onQueueNextSegment(null, e)} disabled={!canSetAsNext}> - {t('Clear queued segment')} + + onSetAsNextFromHere( + part.instance, + playlist?.nextPartInfo?.partInstanceId ?? null, + playlist?.currentPartInfo?.partInstanceId ?? null, + e, + true + ) + } + disabled={getIsPlayFromHereDisabled(true)} + > + + {t(`Play part from ${RundownUtils.formatTimeToShortTime(Math.floor(timecode / 1000) * 1000)}`)} + - )} - {segment && ( - - )} -
    - {this.props.enableUserEdits && ( - <> -
    + + ) : null} + {enableQuickLoop && !RundownResolver.isLoopLocked(playlist) && ( + <> + {RundownResolver.isQuickLoopStart(part.partId, playlist) ? ( + onSetQuickLoopStart(null, e)}> + {t('Clear QuickLoop Start')} + + ) : ( this.props.onEditProps({ type: 'segment', elementId: part.instance.segmentId })} + onClick={(e) => + onSetQuickLoopStart({ type: QuickLoopMarkerType.PART, id: part.instance.part._id }, e) + } + disabled={!!part.instance.orphaned || !canSetAsNext} > - {t('Edit Segment Properties')} + {t('Set as QuickLoop Start')} - - )} - - )} - {part && !part.instance.part.invalid && timecode !== null && ( - <> - this.props.onSetNext(part.instance.part, e)} - disabled={!!part.instance.orphaned || !canSetAsNext} - > - Next') }}> - {startsAt !== null && - '\u00a0(' + RundownUtils.formatTimeToShortTime(Math.floor(startsAt / 1000) * 1000) + ')'} - - {startsAt !== null && part && this.props.enablePlayFromAnywhere ? ( - <> - {/* this.onSetAsNextFromHere(part.instance.part, e)} - disabled={isCurrentPart || !!part.instance.orphaned || !canSetAsNext} - > - Next Here') }}> ( - {RundownUtils.formatTimeToShortTime(Math.floor((startsAt + timecode) / 1000) * 1000)}) - */} + )} + {RundownResolver.isQuickLoopEnd(part.partId, playlist) ? ( + onSetQuickLoopEnd(null, e)}> + {t('Clear QuickLoop End')} + + ) : ( this.onPlayFromHere(part.instance.part, e)} + onClick={(e) => + onSetQuickLoopEnd({ type: QuickLoopMarkerType.PART, id: part.instance.part._id }, e) + } disabled={!!part.instance.orphaned || !canSetAsNext} > - {t('Play from Here')} ( - {RundownUtils.formatTimeToShortTime(Math.floor((startsAt + timecode) / 1000) * 1000)}) + {t('Set as QuickLoop End')} - - ) : null} - {this.props.enableQuickLoop && !RundownResolver.isLoopLocked(this.props.playlist) && ( - <> - {RundownResolver.isQuickLoopStart(part.partId, this.props.playlist) ? ( - this.props.onSetQuickLoopStart(null, e)}> - {t('Clear QuickLoop Start')} - - ) : ( - - this.props.onSetQuickLoopStart( - { type: QuickLoopMarkerType.PART, id: part.instance.part._id }, - e - ) - } - disabled={!!part.instance.orphaned || !canSetAsNext} - > - {t('Set as QuickLoop Start')} - - )} - {RundownResolver.isQuickLoopEnd(part.partId, this.props.playlist) ? ( - this.props.onSetQuickLoopEnd(null, e)}> - {t('Clear QuickLoop End')} - - ) : ( - - this.props.onSetQuickLoopEnd( - { type: QuickLoopMarkerType.PART, id: part.instance.part._id }, - e - ) - } - disabled={!!part.instance.orphaned || !canSetAsNext} - > - {t('Set as QuickLoop End')} - - )} - - )} + )} + + )} + + + {piece && piece.instance.piece.userEditOperations && ( + )} - {this.props.enableUserEdits && ( - <> -
    - this.props.onEditProps({ type: 'segment', elementId: part.instance.segmentId })} - > - {t('Edit Segment Properties')} - - this.props.onEditProps({ type: 'part', elementId: part.instance.part._id })} - > - {t('Edit Part Properties')} + {enableUserEdits && ( + <> +
    + onEditProps({ type: 'segment', elementId: part.instance.segmentId })}> + {t('Edit Segment Properties')} + + onEditProps({ type: 'part', elementId: part.instance.part._id })}> + {t('Edit Part Properties')} + + {piece && piece.instance.piece.userEditProperties && ( + onEditProps({ type: 'piece', elementId: piece.instance.piece._id })}> + {t('Edit Piece Properties')} - {piece && piece.instance.piece.userEditProperties && ( - this.props.onEditProps({ type: 'piece', elementId: piece.instance.piece._id })} - > - {t('Edit Piece Properties')} - - )} - - )} - - )} -
    -
    - ) : null - } - - getSegmentFromContext = (): SegmentUi | null => { - if (this.props.contextMenuContext && this.props.contextMenuContext.segment) { - return this.props.contextMenuContext.segment - } - - return null - } - - getPartFromContext = (): PartUi | null => { - if (this.props.contextMenuContext && this.props.contextMenuContext.part) { - return this.props.contextMenuContext.part - } else { - return null - } - } - - getPieceFromContext = (): PieceExtended | null => { - if (this.props.contextMenuContext && this.props.contextMenuContext.piece) { - return this.props.contextMenuContext.piece - } else { - return null - } - } - - // private onSetAsNextFromHere = (part: DBPart, e) => { - // const offset = this.getTimePosition() - // this.props.onSetNext(part, e, offset || 0) - // } - - private onPlayFromHere = (part: DBPart, e: React.MouseEvent | React.TouchEvent) => { - const offset = this.getTimePosition() - this.props.onSetNext(part, e, offset || 0, true) - } - - private getPartStartsAt = (): number | null => { - if (this.props.contextMenuContext && this.props.contextMenuContext.partStartsAt !== undefined) { - return this.props.contextMenuContext.partStartsAt - } - return null - } - - private getTimePosition = (): number | null => { - let offset = 0 - if (this.props.contextMenuContext && this.props.contextMenuContext.partDocumentOffset) { - const left = this.props.contextMenuContext.partDocumentOffset.left || 0 - const timeScale = this.props.contextMenuContext.timeScale || 1 - const menuPosition = this.props.contextMenuContext.mousePosition || { left } - offset = (menuPosition.left - left) / timeScale - return offset - } - return null - } - } -) + )} + + )} + + )} +
    +
    + ) : null +} diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index 5c419819d5f..d851610ec7c 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import React, { useState, useRef, useEffect } from 'react' import { WithTranslation, withTranslation } from 'react-i18next' import ClassNames from 'classnames' @@ -12,7 +12,7 @@ import { SegmentTimelineZoomControls } from './SegmentTimelineZoomControls.js' import { SegmentDuration } from '../RundownView/RundownTiming/SegmentDuration.js' import { PartCountdown } from '../RundownView/RundownTiming/PartCountdown.js' import { RundownTiming } from '../RundownView/RundownTiming/RundownTiming.js' -import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js' +import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js' import { RundownUtils } from '../../lib/rundown.js' import { Translated } from '../../lib/ReactMeteorData/ReactMeteorData.js' @@ -36,7 +36,7 @@ import { wrapPartToTemporaryInstance } from '@sofie-automation/meteor-lib/dist/c import { SegmentTimelineSmallPartFlag } from './SmallParts/SegmentTimelineSmallPartFlag.js' import { UIStateStorage } from '../../lib/UIStateStorage.js' -import { getPartInstanceTimingId, RundownTimingContext } from '../../lib/rundownTiming.js' +import { computeSegmentDuration, getPartInstanceTimingId, RundownTimingContext } from '../../lib/rundownTiming.js' import { IOutputLayer, ISourceLayer, NoteSeverity, UserEditingType } from '@sofie-automation/blueprints-integration' import { SegmentTimelineZoomButtons } from './SegmentTimelineZoomButtons.js' import { SegmentViewMode } from '../SegmentContainer/SegmentViewModes.js' @@ -51,7 +51,6 @@ import { TimingTickResolution, TimingDataResolution, WithTiming, - RundownTimingProviderContext, } from '../RundownView/RundownTiming/withTiming.js' import { SegmentTimeAnchorTime } from '../RundownView/RundownTiming/SegmentTimeAnchorTime.js' import { logger } from '../../lib/logging.js' @@ -121,100 +120,61 @@ interface IStateHeader { // isSelected: boolean } -interface IZoomPropsHeader { +interface SegmentTimelineZoomProps extends IProps { onZoomDblClick: (e: React.MouseEvent) => void timelineWidth: number + timingDurations: RundownTimingContext } -interface IZoomStateHeader { - totalSegmentDuration: number + +function computeSegmentDurationFromProps(props: SegmentTimelineZoomProps): number { + return computeSegmentDuration(props.timingDurations, props.parts, true) } -const SegmentTimelineZoom = class SegmentTimelineZoom extends React.Component< - IProps & IZoomPropsHeader, - IZoomStateHeader -> { - static contextType = RundownTimingProviderContext - declare context: React.ContextType +function SegmentTimelineZoom(props: SegmentTimelineZoomProps): JSX.Element { + const [totalSegmentDuration, setTotalSegmentDuration] = useState(() => computeSegmentDurationFromProps(props)) - constructor(props: IProps & IZoomPropsHeader, context: any) { - super(props, context) - this.state = { - totalSegmentDuration: 10, - } - } + // Store the props into a ref so that the checkTimingChange can access the latest props without needing to be re-created on every render + const propsRef = useRef(props) + propsRef.current = props - componentDidMount(): void { - this.checkTimingChange() - window.addEventListener(RundownTiming.Events.timeupdateHighResolution, this.onTimeupdate) - } - - componentWillUnmount(): void { - window.removeEventListener(RundownTiming.Events.timeupdateHighResolution, this.onTimeupdate) - } - - onTimeupdate = () => { - if (!this.props.isLiveSegment) { - this.checkTimingChange() - } - } - - checkTimingChange = () => { - const total = this.calculateSegmentDuration() - if (total !== this.state.totalSegmentDuration) { - this.setState({ - totalSegmentDuration: total, - }) + useEffect(() => { + const onTimeupdate = () => { + if (!propsRef.current.isLiveSegment) { + setTotalSegmentDuration(computeSegmentDurationFromProps(propsRef.current)) + } } - } - calculateSegmentDuration(): number { - let total = 0 - if (this.context?.durations) { - const durations = this.context.durations - this.props.parts.forEach((partExtended) => { - // total += durations.partDurations ? durations.partDurations[item._id] : (item.duration || item.renderedDuration || 1) - const partInstanceTimingId = getPartInstanceTimingId(partExtended.instance) - const duration = Math.max( - partExtended.instance.timings?.duration || partExtended.renderedDuration || 0, - durations.partDisplayDurations?.[partInstanceTimingId] || Settings.defaultDisplayDuration - ) - total += duration - }) - } else { - total = RundownUtils.getSegmentDuration(this.props.parts, true) + window.addEventListener(RundownTiming.Events.timeupdateHighResolution, onTimeupdate) + return () => { + window.removeEventListener(RundownTiming.Events.timeupdateHighResolution, onTimeupdate) } - return total - } + }, []) - getSegmentDuration(): number { - return this.props.isLiveSegment ? this.calculateSegmentDuration() : this.state.totalSegmentDuration - } + const segmentDuration = props.isLiveSegment ? computeSegmentDurationFromProps(props) : totalSegmentDuration - render(): JSX.Element { - return ( -
    -
    this.props.onZoomDblClick(e)}> - -
    + return ( +
    +
    +
    - ) - } +
    + ) } export const SEGMENT_TIMELINE_ELEMENT_ID = 'rundown__segment__' @@ -760,7 +720,6 @@ export class SegmentTimelineClass extends React.Component i.instance.part._id), - true - ) || 1) - + (computeSegmentDuration(this.context.durations, this.props.parts, true) || 1) - LIVELINE_HISTORY_SIZE / this.state.timeScale ) ), diff --git a/packages/webui/src/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx b/packages/webui/src/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx index 35b3e6df908..dbb969e26d5 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SmallParts/SegmentTimelineSmallPartFlag.tsx @@ -8,49 +8,40 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { SegmentTimelinePartHoverPreview } from './SegmentTimelinePartHoverPreview.js' import RundownViewEventBus, { RundownViewEvents } from '@sofie-automation/meteor-lib/dist/triggers/RundownViewEventBus' import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' -import { TimingDataResolution, TimingTickResolution, withTiming } from '../../RundownView/RundownTiming/withTiming.js' +import { TimingDataResolution, TimingTickResolution, useTiming } from '../../RundownView/RundownTiming/withTiming.js' import { SegmentTimelinePartClass } from '../Parts/SegmentTimelinePart.js' import { PartExtended } from '../../../lib/RundownResolver.js' import { getPartInstanceTimingId } from '../../../lib/rundownTiming.js' -export const SegmentTimelineSmallPartFlag = withTiming< - { - parts: [PartUi, number, number][] - followingPart: PartUi | undefined - firstPartInSegment: PartExtended - sourceLayers: { - [key: string]: ISourceLayer - } - timeToPixelRatio: number +interface ISegmentTimelineSmallPartFlagProps { + parts: [PartUi, number, number][] + followingPart: PartUi | undefined + firstPartInSegment: PartExtended + sourceLayers: { + [key: string]: ISourceLayer + } + timeToPixelRatio: number - segment: SegmentUi - playlist: DBRundownPlaylist - studio: UIStudio - collapsedOutputs: { - [key: string]: boolean - } - autoNextPart: boolean - liveLineHistorySize: number - isLastSegment: boolean - isLastInSegment: boolean - timelineWidth: number - showDurationSourceLayers?: Set - - livePosition: number - isLiveSegment: boolean | undefined - anyPriorPartWasLive: boolean | undefined - livePartStartsAt: number | undefined - livePartDisplayDuration: number | undefined - }, - {} ->((props) => ({ - dataResolution: TimingDataResolution.High, - tickResolution: TimingTickResolution.High, - filter: (timings) => [ - timings?.partDisplayStartsAt?.[getPartInstanceTimingId(props.firstPartInSegment.instance)], - timings?.partDisplayStartsAt?.[getPartInstanceTimingId(props.parts[0][0].instance)], - ], -}))(({ + segment: SegmentUi + playlist: DBRundownPlaylist + studio: UIStudio + collapsedOutputs: { + [key: string]: boolean + } + autoNextPart: boolean + liveLineHistorySize: number + isLastSegment: boolean + isLastInSegment: boolean + showDurationSourceLayers?: Set + + livePosition: number + isLiveSegment: boolean | undefined + anyPriorPartWasLive: boolean | undefined + livePartStartsAt: number | undefined + livePartDisplayDuration: number | undefined +} + +export const SegmentTimelineSmallPartFlag = ({ parts, followingPart, sourceLayers, @@ -72,9 +63,12 @@ export const SegmentTimelineSmallPartFlag = withTiming< anyPriorPartWasLive, livePartStartsAt, livePartDisplayDuration, +}: ISegmentTimelineSmallPartFlagProps): JSX.Element => { + const timingDurations = useTiming(TimingTickResolution.High, TimingDataResolution.High, (timings) => [ + timings?.partDisplayStartsAt?.[getPartInstanceTimingId(firstPartInSegment.instance)], + timings?.partDisplayStartsAt?.[getPartInstanceTimingId(parts[0][0].instance)], + ]) - timingDurations, -}): JSX.Element => { const flagRef = useRef(null) const futureShadePaddingTime = useMemo(() => { @@ -199,4 +193,4 @@ export const SegmentTimelineSmallPartFlag = withTiming< />
    ) -}) +} diff --git a/packages/webui/src/client/ui/Settings/BlueprintConfigSchema/index.tsx b/packages/webui/src/client/ui/Settings/BlueprintConfigSchema/index.tsx index 50b8febc0d0..26c5a93321e 100644 --- a/packages/webui/src/client/ui/Settings/BlueprintConfigSchema/index.tsx +++ b/packages/webui/src/client/ui/Settings/BlueprintConfigSchema/index.tsx @@ -3,12 +3,8 @@ import { MappingExt, MappingsExt } from '@sofie-automation/corelib/dist/dataMode import { IBlueprintConfig, ISourceLayer, SchemaFormUIField } from '@sofie-automation/blueprints-integration' import { groupByToMapFunc, literal } from '@sofie-automation/corelib/dist/lib' import { useTranslation } from 'react-i18next' -import { - applyAndValidateOverrides, - ObjectWithOverrides, - SomeObjectOverrideOp, -} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { useOverrideOpHelper, WrappedOverridableItemNormal } from '../util/OverrideOpHelper.js' +import { ObjectWithOverrides, SomeObjectOverrideOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { useOverrideOpHelperForSimpleObject } from '../util/OverrideOpHelper.js' import { JSONSchema } from '@sofie-automation/shared-lib/dist/lib/JSONSchemaTypes' import deepmerge from 'deepmerge' import { SchemaFormSofieEnumDefinition, translateStringIfHasNamespaces } from '../../../lib/forms/schemaFormUtil.js' @@ -87,41 +83,25 @@ export function BlueprintConfigSchemaSettings({ } }, [layerMappings, sourceLayers]) - const [wrappedItem, wrappedConfigObject] = useMemo(() => { + const combinedObject = useMemo>(() => { + // TODO - replace based around a custom implementation of OverrideOpHelperForItemContents? + const combinedDefaults: IBlueprintConfig = alternateConfig ? deepmerge(alternateConfig, rawConfigObject.defaults, { arrayMerge: (_destinationArray, sourceArray, _options) => sourceArray, }) : rawConfigObject.defaults - const prefixedOps = rawConfigObject.overrides.map((op) => ({ - ...op, - // TODO: can we avoid doing this hack? - path: `0.${op.path}`, - })) - - const computedValue = applyAndValidateOverrides({ + return { defaults: combinedDefaults, overrides: rawConfigObject.overrides, - }).obj - - const wrappedItem = literal>({ - type: 'normal', - id: '0', - computed: computedValue, - defaults: combinedDefaults, - overrideOps: prefixedOps, - }) - - const wrappedConfigObject: ObjectWithOverrides = { - defaults: combinedDefaults, - overrides: prefixedOps, } + }, [alternateConfig, rawConfigObject]) - return [wrappedItem, wrappedConfigObject] - }, [rawConfigObject]) - - const overrideHelper = useOverrideOpHelper(saveOverridesStrippingPrefix, wrappedConfigObject) // TODO - replace based around a custom implementation of OverrideOpHelperForItemContents? + const { overrideHelper, wrappedItem } = useOverrideOpHelperForSimpleObject( + saveOverridesStrippingPrefix, + combinedObject + ) const groupedSchema = useMemo(() => { if (schema?.type === 'object' && schema.properties) { diff --git a/packages/webui/src/client/ui/Settings/BlueprintSettings.tsx b/packages/webui/src/client/ui/Settings/BlueprintSettings.tsx index 24aec466c77..45d8bafe264 100644 --- a/packages/webui/src/client/ui/Settings/BlueprintSettings.tsx +++ b/packages/webui/src/client/ui/Settings/BlueprintSettings.tsx @@ -1,14 +1,10 @@ import * as React from 'react' import { EditAttribute } from '../../lib/EditAttribute.js' -import { Translated, translateWithTracker } from '../../lib/ReactMeteorData/react-meteor-data.js' +import { useTracker } from '../../lib/ReactMeteorData/react-meteor-data.js' import { Spinner } from '../../lib/Spinner.js' import { doModalDialog } from '../../lib/ModalDialog.js' -import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint' import Moment from 'react-moment' import { Link } from 'react-router-dom' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase' -import { ICoreSystem } from '@sofie-automation/meteor-lib/dist/collections/CoreSystem' import { BlueprintManifestType } from '@sofie-automation/blueprints-integration' import { NotificationCenter, Notification, NoticeLevel } from '../../lib/notifications/notifications.js' import { catchError, fetchFrom } from '../../lib/lib.js' @@ -24,198 +20,239 @@ import Button from 'react-bootstrap/esm/Button' import { useTranslation } from 'react-i18next' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { createPrivateApiPath } from '../../url.js' +import { DBShowStyleBase } from '@sofie-automation/corelib/dist/dataModel/ShowStyleBase.js' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio.js' +import { assertNever } from '@sofie-automation/corelib/dist/lib' interface IProps { blueprintId: BlueprintId } -interface IState {} -interface ITrackedProps { - blueprint?: Blueprint - assignedStudios: DBStudio[] - assignedShowStyles: DBShowStyleBase[] - assignedSystem: ICoreSystem | undefined -} -export default translateWithTracker((props: IProps) => { - const id = props.blueprintId - return { - blueprint: Blueprints.findOne(id), - assignedStudios: Studios.find({ blueprintId: id }).fetch(), - assignedShowStyles: ShowStyleBases.find({ blueprintId: id }).fetch(), - assignedSystem: CoreSystem.findOne({ blueprintId: id }), +export default function BlueprintSettings({ blueprintId }: IProps): JSX.Element { + const { t } = useTranslation() + + const blueprint = useTracker(() => Blueprints.findOne(blueprintId), [blueprintId]) + + if (!blueprint) { + return } -})( - class BlueprintSettings extends React.Component, IState> { - constructor(props: Translated) { - super(props) - this.state = {} - } - assignSystemBlueprint(id: BlueprintId | undefined) { - MeteorCall.blueprint.assignSystemBlueprint(id).catch(catchError('blueprint.assignSystemBlueprint')) - } + return ( +
    +
    + - renderAssignment(blueprint: Blueprint) { - const { t } = this.props +
    - ) - } - } -) + }, + [showStyleBase._id] + ) + + const { toggleExpanded, isExpanded } = useToggleExpandHelper() + + return ( +
    +

    {t('Custom Hotkey Labels')}

    + + + {(showStyleBase.hotkeyLegend || []).map((item, index) => { + return ( + + + + + + + + {isExpanded(item._id) && ( + + + + )} + + ) + })} + +
    + {hotkeyHelper.shortcutLabel(item.key)} + {item.label} + + +
    +
    + + +
    +
    + +
    +
    +
    + + + + +
    +
    + ) +} function ImportHotkeyLegendButton({ showStyleBaseId }: { showStyleBaseId: ShowStyleBaseId }) { const { t } = useTranslation() diff --git a/packages/webui/src/client/ui/Settings/SnapshotsView.tsx b/packages/webui/src/client/ui/Settings/SnapshotsView.tsx index fa429930dbe..6e67fe0de82 100644 --- a/packages/webui/src/client/ui/Settings/SnapshotsView.tsx +++ b/packages/webui/src/client/ui/Settings/SnapshotsView.tsx @@ -1,15 +1,12 @@ import * as React from 'react' -import { Translated, useSubscription, useTracker } from '../../lib/ReactMeteorData/react-meteor-data.js' +import { useSubscription, useTracker } from '../../lib/ReactMeteorData/react-meteor-data.js' import { doModalDialog } from '../../lib/ModalDialog.js' -import { SnapshotItem } from '@sofie-automation/meteor-lib/dist/collections/Snapshots' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' -import _ from 'underscore' import { logger } from '../../lib/logging.js' import { EditAttribute } from '../../lib/EditAttribute.js' import { faWindowClose, faUpload } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { multilineText, fetchFrom } from '../../lib/lib.js' +import { fetchFrom } from '../../lib/lib.js' import { NotificationCenter, Notification, NoticeLevel } from '../../lib/notifications/notifications.js' import { UploadButton } from '../../lib/uploadButton.js' import { MeteorPubSub } from '@sofie-automation/meteor-lib/dist/api/pubsub' @@ -19,32 +16,23 @@ import { Snapshots, Studios } from '../../collections/index.js' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { hashSingleUseToken } from '../../lib/lib.js' import { CorelibPubSub } from '@sofie-automation/corelib/dist/pubsub' -import { useTranslation, withTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import Button from 'react-bootstrap/esm/Button' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { createPrivateApiPath } from '../../url.js' import { UserError } from '@sofie-automation/corelib/dist/error' +import { SnapshotItem, SnapshotType } from '@sofie-automation/meteor-lib/dist/collections/Snapshots' +import { useState } from 'react' +import { MomentFromNow } from '../../lib/Moment.js' +import { assertNever } from '@sofie-automation/corelib/dist/lib' -interface IProps { - match: { - params: { - showStyleId: string - } - } -} -interface IState { - uploadFileKey: string // Used to force clear the input after use - uploadFileKey2: string // Used to force clear the input after use - editSnapshotId: SnapshotId | null - removeSnapshots: boolean -} -interface ITrackedProps { - snapshots: Array - studios: Array -} +export default function SnapshotsView(): JSX.Element { + const { t } = useTranslation() -export default function SnapshotsView(props: Readonly): JSX.Element { - // // Subscribe to data: + const [removeSnapshots, setRemoveSnapshots] = React.useState(false) + const toggleRemoveView = React.useCallback(() => setRemoveSnapshots((old) => !old), []) + + // Subscribe to data: useSubscription(MeteorPubSub.snapshots) useSubscription(CorelibPubSub.studios, null) @@ -63,309 +51,182 @@ export default function SnapshotsView(props: Readonly): JSX.Element { ) const studios = useTracker(() => Studios.find({}, {}).fetch(), [], []) - return -} - -const SnapshotsViewContent = withTranslation()( - class SnapshotsViewContent extends React.Component, IState> { - constructor(props: Translated) { - super(props) - this.state = { - uploadFileKey: `${Date.now()}_1`, - uploadFileKey2: `${Date.now()}_2`, - editSnapshotId: null, - removeSnapshots: false, - } - } + return ( +
    +

    {t('Take a Snapshot')}

    +
    +

    {t('Full System Snapshot')}

    +

    + + {t('A Full System Snapshot contains all system settings (studios, showstyles, blueprints, devices, etc.)')} + +

    - restoreStoredSnapshot = (snapshotId: SnapshotId) => { - const snapshot = Snapshots.findOne(snapshotId) - if (snapshot) { - doModalDialog({ - title: 'Restore Snapshot', - message: `Do you really want to restore the snapshot ${snapshot.name}?`, - onAccept: () => { - MeteorCall.snapshot - .restoreSnapshot(snapshotId, false) - .then(() => { - // todo: replace this with something else - doModalDialog({ - title: 'Restore Snapshot', - message: `Snapshot restored!`, - acceptOnly: true, - onAccept: () => { - // nothing - }, - }) - }) - .catch((err) => { - logger.error(err) - doModalDialog({ - title: 'Restore Snapshot', - message: `Error: ${err.toString()}`, - acceptOnly: true, - onAccept: () => { - // nothing - }, - }) - }) - }, - }) - } - } - takeSystemSnapshot = (studioId: StudioId | null) => { - MeteorCall.system - .generateSingleUseToken() - .then((tokenResponse) => { - if (ClientAPI.isClientResponseError(tokenResponse)) throw UserError.fromSerialized(tokenResponse.error) - if (!tokenResponse.result) throw new Error('Failed to generate token') - return MeteorCall.snapshot.storeSystemSnapshot( - hashSingleUseToken(tokenResponse.result), - studioId, - `Requested by user` - ) - }) - .catch((err) => { - logger.error(err) - doModalDialog({ - title: 'Restore Snapshot', - message: `Error: ${err.toString()}`, - acceptOnly: true, - onAccept: () => { - // nothing - }, - }) - }) - } - takeDebugSnapshot = (studioId: StudioId) => { - MeteorCall.system - .generateSingleUseToken() - .then((tokenResponse) => { - if (ClientAPI.isClientResponseError(tokenResponse)) throw UserError.fromSerialized(tokenResponse.error) - if (!tokenResponse.result) throw new Error('Failed to generate token') - return MeteorCall.snapshot.storeDebugSnapshot( - hashSingleUseToken(tokenResponse.result), - studioId, - `Requested by user` - ) - }) - .catch((err) => { - logger.error(err) - doModalDialog({ - title: 'Restore Snapshot', - message: `Error: ${err.toString()}`, - acceptOnly: true, - onAccept: () => { - // nothing - }, - }) - }) - } - editSnapshot = (snapshotId: SnapshotId) => { - if (this.state.editSnapshotId === snapshotId) { - this.setState({ - editSnapshotId: null, - }) - } else { - this.setState({ - editSnapshotId: snapshotId, - }) - } - } - toggleRemoveView = () => { - this.setState({ - removeSnapshots: !this.state.removeSnapshots, - }) - } - removeStoredSnapshot = (snapshotId: SnapshotId) => { - const snapshot = Snapshots.findOne(snapshotId) - if (snapshot) { - doModalDialog({ - title: 'Remove Snapshot', - message: `Are you sure, do you really want to REMOVE the Snapshot ${snapshot.name}?\r\nThis cannot be undone!!`, - onAccept: () => { - MeteorCall.snapshot.removeSnapshot(snapshotId).catch((err) => { - logger.error(err) - doModalDialog({ - title: 'Remove Snapshot', - message: `Error: ${err.toString()}`, - acceptOnly: true, - onAccept: () => { - // nothing - }, - }) - }) - }, - }) - } - } - render(): JSX.Element { - const { t } = this.props +
    + +
    - return ( -
    -

    {t('Take a Snapshot')}

    + {studios.length > 1 ? (
    -

    {t('Full System Snapshot')}

    -

    - - {t( - 'A Full System Snapshot contains all system settings (studios, showstyles, blueprints, devices, etc.)' - )} - +

    {t('Studio Snapshot')}

    +

    + {t('A Studio Snapshot contains all system settings related to that studio')}

    + {studios.map((studio) => { + return ( +
    + +
    + ) + })} +
    + ) : null} +
    -
    - -
    +

    {t('Restore from Snapshot File')}

    - {this.props.studios.length > 1 ? ( -
    -

    {t('Studio Snapshot')}

    -

    - {t('A Studio Snapshot contains all system settings related to that studio')} -

    - {_.map(this.props.studios, (studio) => { - return ( -
    - -
    - ) - })} -
    - ) : null} -
    +

    + + {t('Upload Snapshot')} + + {t('Upload a snapshot file')} +

    +

    + + {t('Upload Snapshot (for debugging)')} + + + {t( + 'Upload a snapshot file (restores additional info not directly related to a Playlist / Rundown, such as Packages, PackageWorkStatuses etc' + )} + +

    +

    + + {t('Ingest from Snapshot')} + + + {t('Reads the ingest (NRCS) data, and pipes it through the blueprints')} + +

    -

    {t('Restore from Snapshot File')}

    +

    {t('Restore from Stored Snapshots')}

    +
    + + + + + + + {removeSnapshots ? : null} + + {snapshots.map((snapshot) => ( + + ))} + +
    {t('Name')}{t('When')}
    + +
    +
    + ) +} -

    - - {t('Upload Snapshot')} - - {t('Upload a snapshot file')} -

    -

    - - {t('Upload Snapshot (for debugging)')} - - - {t( - 'Upload a snapshot file (restores additional info not directly related to a Playlist / Rundown, such as Packages, PackageWorkStatuses etc' - )} - -

    -

    - - {t('Ingest from Snapshot')} - - - {t('Reads the ingest (NRCS) data, and pipes it throught the blueprints')} - -

    +function SnapshotRowItem({ + snapshot, + removeSnapshots, +}: { + snapshot: SnapshotItem + removeSnapshots: boolean +}): JSX.Element { + const [isExpanded, setIsExpanded] = useState(false) -

    {t('Restore from Stored Snapshots')}

    -
    - - - - - - - - {this.state.removeSnapshots ? : null} - - {_.map(this.props.snapshots, (snapshot) => { - return ( - - - - - + + - {this.state.removeSnapshots ? ( - - ) : null} - - ) - })} - -
    TypeNameComment
    - - {snapshot.type} - - {snapshot.name} - - - {this.state.editSnapshotId === snapshot._id ? ( -
    - + return ( +
    + + + + {isExpanded ? ( +
    + - -
    - ) : ( - { - e.preventDefault() - this.editSnapshot(snapshot._id) - }} - > - {multilineText(snapshot.comment)} - - )} -
    - -
    - +
    -
    - ) - } + ) : ( + { + e.preventDefault() + setIsExpanded(true) + }} + > + + {(snapshot.comment || '').split('\n').map((line: string, i, arr) => { + return ( +

    + {line} +

    + ) + })} +
    +
    + )} + + + + + {removeSnapshots ? ( + + + + ) : null} + + ) +} + +function SnapshotTypeIndicator({ snapshotType }: { snapshotType: SnapshotType }): JSX.Element { + const { t } = useTranslation() + + switch (snapshotType) { + case SnapshotType.RUNDOWNPLAYLIST: + return {t('Playlist')} + case SnapshotType.SYSTEM: + return {t('System')} + case SnapshotType.DEBUG: + return {t('Debug')} + default: + assertNever(snapshotType) + return {snapshotType} } -) +} function SnapshotImportButton({ restoreVariant, @@ -441,3 +302,124 @@ function SnapshotImportButton({ ) } + +function RestoreStoredSnapshotButton({ snapshotId }: { snapshotId: SnapshotId }) { + const { t } = useTranslation() + + const restoreStoredSnapshot = React.useCallback(() => { + const snapshot = Snapshots.findOne(snapshotId) + if (snapshot) { + doModalDialog({ + title: t('Restore Snapshot'), + message: t('Do you really want to restore the snapshot "{{snapshotName}}"?', { snapshotName: snapshot.name }), + onAccept: () => { + MeteorCall.snapshot + .restoreSnapshot(snapshotId, false) + .then(() => { + // todo: replace this with something else + doModalDialog({ + title: t('Restore Snapshot'), + message: t('Snapshot restored!'), + acceptOnly: true, + onAccept: () => { + // nothing + }, + }) + }) + .catch((err) => { + logger.error(err) + doModalDialog({ + title: t('Restore Snapshot'), + message: t('Snapshot restore failed: {{errorMessage}}', { errorMessage: stringifyError(err) }), + acceptOnly: true, + onAccept: () => { + // nothing + }, + }) + }) + }, + }) + } + }, [t, snapshotId]) + + return ( + + ) +} + +function TakeSystemSnapshotButton({ studioId }: { studioId: StudioId | null }) { + const { t } = useTranslation() + + const takeSystemSnapshot = React.useCallback(() => { + MeteorCall.system + .generateSingleUseToken() + .then((tokenResponse) => { + if (ClientAPI.isClientResponseError(tokenResponse)) throw UserError.fromSerialized(tokenResponse.error) + if (!tokenResponse.result) throw new Error('Failed to generate token') + return MeteorCall.snapshot.storeSystemSnapshot( + hashSingleUseToken(tokenResponse.result), + studioId, + `Requested by user` + ) + }) + .catch((err) => { + logger.error(err) + doModalDialog({ + title: t('Take System Snapshot'), + message: t('Take System Snapshot failed: {{errorMessage}}', { errorMessage: stringifyError(err) }), + acceptOnly: true, + onAccept: () => { + // nothing + }, + }) + }) + }, [t, studioId]) + + const studioName = useTracker(() => (studioId ? Studios.findOne(studioId)?.name : null), [studioId]) + + return ( + + ) +} + +function RemoveSnapshotButton({ snapshotId }: { snapshotId: SnapshotId }) { + const { t } = useTranslation() + + const removeStoredSnapshot = React.useCallback(() => { + const snapshot = Snapshots.findOne(snapshotId) + if (snapshot) { + doModalDialog({ + title: t('Remove Snapshot'), + message: t( + 'Are you sure, do you really want to REMOVE the Snapshot "{{snapshotName}}"?\r\nThis cannot be undone!!', + { snapshotName: snapshot.name } + ), + onAccept: () => { + MeteorCall.snapshot.removeSnapshot(snapshotId).catch((err) => { + logger.error(err) + doModalDialog({ + title: t('Remove Snapshot'), + message: t('Snapshot remove failed: {{errorMessage}}', { errorMessage: stringifyError(err) }), + acceptOnly: true, + onAccept: () => { + // nothing + }, + }) + }) + }, + }) + } + }, [t, snapshotId]) + + return ( + + ) +} diff --git a/packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx b/packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx index 80cfda1c82e..7794c15d1be 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Devices/IngestSubDevices.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next' import { getAllCurrentAndDeletedItemsFromOverrides, useOverrideOpHelper } from '../../util/OverrideOpHelper.js' import { ObjectOverrideSetOp, + ObjectWithOverrides, SomeObjectOverrideOp, wrapDefaultObject, } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' @@ -43,7 +44,7 @@ export function StudioIngestSubDevices({ [studio?._id] ) - const baseSettings = useMemo( + const baseSettings = useMemo>>( () => studio?.peripheralDeviceSettings?.ingestDevices ?? wrapDefaultObject({}), [studio?.peripheralDeviceSettings?.ingestDevices] ) diff --git a/packages/webui/src/client/ui/Settings/Studio/Devices/InputSubDevices.tsx b/packages/webui/src/client/ui/Settings/Studio/Devices/InputSubDevices.tsx index 825d21ed966..8d7059e5201 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Devices/InputSubDevices.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Devices/InputSubDevices.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next' import { getAllCurrentAndDeletedItemsFromOverrides, useOverrideOpHelper } from '../../util/OverrideOpHelper.js' import { ObjectOverrideSetOp, + ObjectWithOverrides, SomeObjectOverrideOp, wrapDefaultObject, } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' @@ -40,7 +41,7 @@ export function StudioInputSubDevices({ studioId, studioDevices }: Readonly>>( () => studio?.peripheralDeviceSettings?.inputDevices ?? wrapDefaultObject({}), [studio?.peripheralDeviceSettings?.inputDevices] ) diff --git a/packages/webui/src/client/ui/Settings/Studio/Devices/PlayoutSubDevices.tsx b/packages/webui/src/client/ui/Settings/Studio/Devices/PlayoutSubDevices.tsx index b015c8a8bef..b1b58994806 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Devices/PlayoutSubDevices.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Devices/PlayoutSubDevices.tsx @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next' import { getAllCurrentAndDeletedItemsFromOverrides, useOverrideOpHelper } from '../../util/OverrideOpHelper.js' import { ObjectOverrideSetOp, + ObjectWithOverrides, SomeObjectOverrideOp, wrapDefaultObject, } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' @@ -44,7 +45,7 @@ export function StudioPlayoutSubDevices({ [studio?._id] ) - const baseSettings = useMemo( + const baseSettings = useMemo>>( () => studio?.peripheralDeviceSettings?.playoutDevices ?? wrapDefaultObject({}), [studio?.peripheralDeviceSettings?.playoutDevices] ) diff --git a/packages/webui/src/client/ui/Settings/Studio/Generic.tsx b/packages/webui/src/client/ui/Settings/Studio/Generic.tsx index 32d81caceec..61ff9433a04 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Generic.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Generic.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio' +import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons' import { useTranslation } from 'react-i18next' @@ -18,14 +18,9 @@ import { } from '../../../lib/Components/LabelAndOverrides.js' import { catchError } from '../../../lib/lib.js' import { ForceQuickLoopAutoNext } from '@sofie-automation/shared-lib/dist/core/model/StudioSettings' -import { - applyAndValidateOverrides, - ObjectWithOverrides, - SomeObjectOverrideOp, -} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { useOverrideOpHelper, WrappedOverridableItemNormal } from '../util/OverrideOpHelper.js' +import { SomeObjectOverrideOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { useOverrideOpHelperForSimpleObject } from '../util/OverrideOpHelper.js' import { IntInputControl } from '../../../lib/Components/IntInput.js' -import { literal } from '@sofie-automation/corelib/dist/lib' import { useMemo } from 'react' import { CheckboxControl } from '../../../lib/Components/Checkbox.js' import { TextInputControl } from '../../../lib/Components/TextInput.js' @@ -161,32 +156,10 @@ function StudioSettings({ studio }: { studio: DBStudio }): JSX.Element { [studio._id] ) - const [wrappedItem, wrappedConfigObject] = useMemo(() => { - const prefixedOps = studio.settingsWithOverrides.overrides.map((op) => ({ - ...op, - // TODO: can we avoid doing this hack? - path: `0.${op.path}`, - })) - - const computedValue = applyAndValidateOverrides(studio.settingsWithOverrides).obj - - const wrappedItem = literal>({ - type: 'normal', - id: '0', - computed: computedValue, - defaults: studio.settingsWithOverrides.defaults, - overrideOps: prefixedOps, - }) - - const wrappedConfigObject: ObjectWithOverrides = { - defaults: studio.settingsWithOverrides.defaults, - overrides: prefixedOps, - } - - return [wrappedItem, wrappedConfigObject] - }, [studio.settingsWithOverrides]) - - const overrideHelper = useOverrideOpHelper(saveOverrides, wrappedConfigObject) + const { overrideHelper, wrappedItem } = useOverrideOpHelperForSimpleObject( + saveOverrides, + studio.settingsWithOverrides + ) const autoNextOptions: DropdownInputOption[] = useMemo( () => [ diff --git a/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx b/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx index 4ee6ba40ff2..727c6b69f1f 100644 --- a/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/Mappings.tsx @@ -44,6 +44,7 @@ import { translateStringIfHasNamespaces, } from '../../../lib/forms/schemaFormUtil.js' import { Studios } from '../../../collections/index.js' +import { ReadonlyDeep } from 'type-fest' export interface MappingsSettingsManifest { displayName: string @@ -189,7 +190,7 @@ interface DeletedEntryProps { manifestNames: Record translationNamespaces: string[] - mapping: MappingExt + mapping: ReadonlyDeep layerId: string doUndelete: (itemId: string) => void } @@ -531,7 +532,7 @@ function StudioMappingsEntry({ interface MappingSummaryProps { translationNamespaces: string[] fields: SchemaSummaryField[] - mapping: MappingExt + mapping: ReadonlyDeep } function MappingSummary({ translationNamespaces, fields, mapping }: Readonly) { if (fields.length > 0) { diff --git a/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainerPickers.tsx b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainerPickers.tsx index 8c107c0c0f8..5486fd8d619 100644 --- a/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainerPickers.tsx +++ b/packages/webui/src/client/ui/Settings/Studio/PackageManager/PackageContainerPickers.tsx @@ -1,12 +1,23 @@ import * as React from 'react' import { DBStudio, StudioPackageContainer } from '@sofie-automation/corelib/dist/dataModel/Studio' -import { EditAttribute } from '../../../../lib/EditAttribute.js' import { useTranslation } from 'react-i18next' import { Accessor } from '@sofie-automation/blueprints-integration' import { Studios } from '../../../../collections/index.js' import { DropdownInputOption } from '../../../../lib/Components/DropdownInput.js' -import { WrappedOverridableItem } from '../../util/OverrideOpHelper.js' -import { LabelActual } from '../../../../lib/Components/LabelAndOverrides.js' +import { + useOverrideOpHelper, + WrappedOverridableItem, + WrappedOverridableItemNormal, +} from '../../util/OverrideOpHelper.js' +import { LabelAndOverridesForMultiSelect } from '../../../../lib/Components/LabelAndOverrides' +import { + applyAndValidateOverrides, + ObjectWithOverrides, + SomeObjectOverrideOp, +} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { MultiSelectInputControl } from '../../../../lib/Components/MultiSelectInput' +import { useMemo } from 'react' +import { StudioPackageContainerSettings } from '@sofie-automation/shared-lib/dist/core/model/PackageContainer' interface PackageContainersPickersProps { studio: DBStudio @@ -19,6 +30,46 @@ export function PackageContainersPickers({ }: PackageContainersPickersProps): JSX.Element { const { t } = useTranslation() + const [wrappedItem, wrappedConfigObject] = useMemo(() => { + const prefixedOps = studio.packageContainerSettingsWithOverrides.overrides.map((op) => ({ + ...op, + // TODO: can we avoid doing this hack? + path: `0.${op.path}`, + })) + + const computedValue = applyAndValidateOverrides(studio.packageContainerSettingsWithOverrides).obj + + const wrappedItem: WrappedOverridableItemNormal = { + type: 'normal', + id: '0', + computed: computedValue, + defaults: studio.packageContainerSettingsWithOverrides.defaults, + overrideOps: prefixedOps, + } + + const wrappedConfigObject: ObjectWithOverrides = { + defaults: studio.packageContainerSettingsWithOverrides.defaults, + overrides: prefixedOps, + } + + return [wrappedItem, wrappedConfigObject] + }, [studio.packageContainerSettingsWithOverrides]) + + const saveOverrides = React.useCallback( + (newOps: SomeObjectOverrideOp[]) => { + Studios.update(studio._id, { + $set: { + 'packageContainerSettingsWithOverrides.overrides': newOps.map((op) => ({ + ...op, + path: op.path.startsWith('0.') ? op.path.slice(2) : op.path, + })), + }, + }) + }, + [studio._id] + ) + const overrideHelper = useOverrideOpHelper(saveOverrides, wrappedConfigObject) + const availablePackageContainerOptions = React.useMemo(() => { const arr: DropdownInputOption[] = [] @@ -45,32 +96,40 @@ export function PackageContainersPickers({ return (
    -
    - -
    - + {(value, handleUpdate, options) => ( + -
    -
    -
    - -
    - + + {(value, handleUpdate, options) => ( + -
    -
    + )} +
    ) } diff --git a/packages/webui/src/client/ui/Settings/SystemManagement.tsx b/packages/webui/src/client/ui/Settings/SystemManagement.tsx index 072fc5444f1..6c30261859e 100644 --- a/packages/webui/src/client/ui/Settings/SystemManagement.tsx +++ b/packages/webui/src/client/ui/Settings/SystemManagement.tsx @@ -9,7 +9,6 @@ import { languageAnd } from '../../lib/language.js' import { TriggeredActionsEditor } from './components/triggeredActions/TriggeredActionsEditor.js' import { TFunction, useTranslation } from 'react-i18next' import { Meteor } from 'meteor/meteor' -import { literal } from '@sofie-automation/corelib/dist/lib' import { LogLevel } from '@sofie-automation/meteor-lib/dist/lib' import { CoreSystem } from '../../collections/index.js' import { CollectionCleanupResult } from '@sofie-automation/meteor-lib/dist/api/system' @@ -21,13 +20,8 @@ import { } from '../../lib/Components/LabelAndOverrides.js' import { catchError } from '../../lib/lib.js' import { SystemManagementBlueprint } from './SystemManagement/Blueprint.js' -import { - applyAndValidateOverrides, - ObjectWithOverrides, - SomeObjectOverrideOp, -} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { ICoreSystemSettings } from '@sofie-automation/blueprints-integration' -import { WrappedOverridableItemNormal, useOverrideOpHelper } from './util/OverrideOpHelper.js' +import { SomeObjectOverrideOp } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { useOverrideOpHelperForSimpleObject } from './util/OverrideOpHelper.js' import { CheckboxControl } from '../../lib/Components/Checkbox.js' import { CombinedMultiLineTextInputControl, @@ -531,35 +525,5 @@ function useCoreSystemSettingsWithOverrides(coreSystem: ICoreSystem) { [coreSystem._id] ) - const [wrappedItem, wrappedConfigObject] = useMemo(() => { - const prefixedOps = coreSystem.settingsWithOverrides.overrides.map((op) => ({ - ...op, - // TODO: can we avoid doing this hack? - path: `0.${op.path}`, - })) - - const computedValue = applyAndValidateOverrides(coreSystem.settingsWithOverrides).obj - - const wrappedItem = literal>({ - type: 'normal', - id: '0', - computed: computedValue, - defaults: coreSystem.settingsWithOverrides.defaults, - overrideOps: prefixedOps, - }) - - const wrappedConfigObject: ObjectWithOverrides = { - defaults: coreSystem.settingsWithOverrides.defaults, - overrides: prefixedOps, - } - - return [wrappedItem, wrappedConfigObject] - }, [coreSystem.settingsWithOverrides]) - - const overrideHelper = useOverrideOpHelper(saveOverrides, wrappedConfigObject) - - return { - wrappedItem, - overrideHelper, - } + return useOverrideOpHelperForSimpleObject(saveOverrides, coreSystem.settingsWithOverrides) } diff --git a/packages/webui/src/client/ui/Settings/components/GenericDeviceSettingsComponent.tsx b/packages/webui/src/client/ui/Settings/components/GenericDeviceSettingsComponent.tsx index 80ea8d660b3..89cef9035e1 100644 --- a/packages/webui/src/client/ui/Settings/components/GenericDeviceSettingsComponent.tsx +++ b/packages/webui/src/client/ui/Settings/components/GenericDeviceSettingsComponent.tsx @@ -4,6 +4,8 @@ import { DeviceItem } from '../../Status/SystemStatus/DeviceItem.js' import { ConfigManifestOAuthFlowComponent } from './ConfigManifestOAuthFlow.js' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { useDebugStatesForPlayoutDevice } from './useDebugStatesForPlayoutDevice.js' +import { PeripheralDevices } from '../../../collections/index.js' +import { useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData.js' interface IGenericDeviceSettingsComponentProps { device: PeripheralDevice @@ -38,17 +40,17 @@ export function GenericDeviceSettingsComponent({ } } -interface GenericAttahcedSubDeviceSettingsComponentProps { +interface GenericAttachedSubDeviceSettingsComponentProps { device: PeripheralDevice - subDevices: PeripheralDevice[] | undefined } -export function GenericAttahcedSubDeviceSettingsComponent({ +export function GenericAttachedSubDeviceSettingsComponent({ device, - subDevices, -}: Readonly): JSX.Element { +}: Readonly): JSX.Element { const { t } = useTranslation() + const subDevices = useTracker(() => PeripheralDevices.find({ parentDeviceId: device._id }).fetch(), [device._id], []) + const debugStates = useDebugStatesForPlayoutDevice(device) return ( @@ -57,9 +59,9 @@ export function GenericAttahcedSubDeviceSettingsComponent({ <>

    {t('Attached Subdevices')}

    - {(!subDevices || subDevices.length === 0) &&

    {t('There are no sub-devices for this gateway')}

    } + {subDevices.length === 0 &&

    {t('There are no sub-devices for this gateway')}

    } - {subDevices?.map((subDevice) => ( + {subDevices.map((subDevice) => ( = function ActionEditor({ [action, overrideHelper] ) + const onChangeType = useCallback( + (filterIndex: number, newType: FilterType) => { + action.filterChain[filterIndex].object = newType + + overrideHelper().replaceItem(actionId, action).commit() + }, + [action, overrideHelper] + ) + function onFilterInsertNext(filterIndex: number) { if (action.filterChain.length === filterIndex + 1) { const obj = @@ -154,6 +163,7 @@ export const ActionEditor: React.FC = function ActionEditor({ index={chainIndex} opened={openFilterIndex === chainIndex} onChange={onFilterChange} + onChangeType={onChangeType} sourceLayers={sourceLayers} outputLayers={outputLayers} onFocus={onFilterFocus} @@ -173,9 +183,23 @@ export const ActionEditor: React.FC = function ActionEditor({ final={action.filterChain.length === 1 && isFinal(action, chainLink)} onInsertNext={onFilterInsertNext} onRemove={onFilterRemove} + onChangeType={onChangeType} /> ) : chainLink.object === 'rundownPlaylist' ? ( - + ) : (
    {(chainLink as any).object}
    diff --git a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/AdLibFilter.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/AdLibFilter.tsx index 0c2c47f6465..286ddc15c18 100644 --- a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/AdLibFilter.tsx +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/AdLibFilter.tsx @@ -1,6 +1,12 @@ import React from 'react' import _ from 'underscore' -import { IAdLibFilterLink, IOutputLayer, ISourceLayer, SourceLayerType } from '@sofie-automation/blueprints-integration' +import { + FilterType, + IAdLibFilterLink, + IOutputLayer, + ISourceLayer, + SourceLayerType, +} from '@sofie-automation/blueprints-integration' import { useTranslation } from 'react-i18next' import { TFunction } from 'i18next' import { assertNever } from '@sofie-automation/corelib/dist/lib' @@ -22,6 +28,7 @@ interface IProps { outputLayers: OutputLayers | undefined readonly?: boolean opened: boolean + onChangeType: (index: number, newType: FilterType) => void onChange: (index: number, newVal: IAdLibFilterLink, oldVal: IAdLibFilterLink) => void onFocus?: (index: number) => void onInsertNext?: (index: number) => void @@ -92,7 +99,7 @@ function fieldToLabel(t: TFunction, field: IAdLibFilterLink['field']): string { return t('Type') default: assertNever(field) - return field + return t('AdLib filter') } } @@ -285,6 +292,7 @@ export const AdLibFilter: React.FC = function AdLibFilter({ onFocus, onInsertNext, onRemove, + onChangeType, }: IProps) { const { t } = useTranslation() @@ -329,6 +337,7 @@ export const AdLibFilter: React.FC = function AdLibFilter({ return ( = function AdLibFilter({ onClose={onClose} onInsertNext={onInsertNext} onRemove={onRemove} + onChangeType={(newType) => onChangeType(index, newType)} /> ) } diff --git a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/FilterEditor.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/FilterEditor.tsx index 0dedcaba2a3..a1e9b2b6519 100644 --- a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/FilterEditor.tsx +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/FilterEditor.tsx @@ -9,6 +9,8 @@ import { catchError } from '../../../../../../lib/lib.js' import { preventOverflow } from '@popperjs/core' import { DropdownInputControl, getDropdownInputOptions } from '../../../../../../lib/Components/DropdownInput.js' import Button from 'react-bootstrap/esm/Button' +import { SwitchFilterType } from './SwitchFilterType' +import { FilterType } from '@sofie-automation/blueprints-integration' interface IProps { fieldLabel: string @@ -23,6 +25,8 @@ interface IProps { type: EditAttributeType values?: Record index: number + filterType: FilterType + onChangeType: (newType: FilterType) => void onChangeField: (newField: any) => void onChange: (newValue: any) => void onFocus?: (index: number) => void @@ -32,7 +36,7 @@ interface IProps { } export const FilterEditor: React.FC = function FilterEditor(props: IProps): React.ReactElement | null { - const { index, opened, onClose, onFocus } = props + const { index, opened, onClose, onFocus, filterType, onChangeType } = props const [referenceElement, setReferenceElement] = useState(null) const [popperElement, setPopperElement] = useState(null) const { styles, attributes, update } = usePopper(referenceElement, popperElement, { @@ -98,6 +102,13 @@ export const FilterEditor: React.FC = function FilterEditor(props: IProp style={styles.popper} {...attributes.popper} > + + {props.description &&

    {props.description}

    } void + onChange: (index: number, newVal: IRundownPlaylistFilterLink, oldVal: IRundownPlaylistFilterLink) => void + onFocus?: (index: number) => void + onInsertNext?: (index: number) => void + onRemove?: (index: number) => void + onClose: (index: number) => void +} + +function fieldToType(field: IRundownPlaylistFilterLink['field']): EditAttributeType { + switch (field) { + case 'activationId': + return 'dropdown' + case 'name': + return 'text' + case 'rehearsal': + return 'dropdown' + case 'studioId': + return 'dropdown' + default: + assertNever(field) + return field + } +} + +function fieldToOptions(t: TFunction, field: IRundownPlaylistFilterLink['field']): Record { + switch (field) { + case 'activationId': + return { + [t('Active')]: true, + } + case 'name': + return {} + case 'rehearsal': + return { + [t('In rehearsal')]: true, + [t('Not in rehearsal')]: false, + } + case 'studioId': + return Studios.find() + .fetch() + .map((studio) => ({ name: `${studio.name} (${studio._id})`, value: studio._id })) + default: + assertNever(field) + return field + } +} + +function fieldValueToValueLabel(t: TFunction, link: IRundownPlaylistFilterLink) { + if (link.value === undefined || (Array.isArray(link.value) && link.value.length === 0)) { + return '' + } + + switch (link.field) { + case 'activationId': + return link.value === true ? t('Active') : t('Not Active') + case 'name': + case 'studioId': + return String(link.value) + case 'rehearsal': + return link.value === true ? t('In rehearsal') : t('Not in rehearsal') + default: + assertNever(link) + //@ts-expect-error fallback + return String(link.value) + } +} + +function fieldValueMutate(link: IRundownPlaylistFilterLink, newValue: any) { + switch (link.field) { + case 'activationId': + case 'rehearsal': + return Boolean(newValue) + case 'name': + case 'studioId': + return String(newValue) + default: + assertNever(link) + return String(newValue) + } +} + +function fieldValueToEditorValue(link: IRundownPlaylistFilterLink) { + if (link.value === undefined || (Array.isArray(link.value) && link.value.length === 0)) { + return undefined + } + + switch (link.field) { + case 'activationId': + case 'rehearsal': + case 'name': + case 'studioId': + return link.value + default: + assertNever(link) + //@ts-expect-error fallback + return String(link.value) + } +} + +function getAvailableFields(t: TFunction, fields: IRundownPlaylistFilterLink['field'][]): Record { + const result: Record = {} + fields.forEach((key) => { + result[fieldToLabel(t, key)] = key + }) + + return result } function fieldToLabel(t: TFunction, field: IRundownPlaylistFilterLink['field']): string { switch (field) { case 'activationId': - return t('Now active rundown') + return t('Now Active Rundown') case 'name': - return t('Name') + return t('Rundown Name') case 'studioId': return t('Studio') + case 'rehearsal': + return t('Rehearsal State') default: assertNever(field) - return field + return t('Rundown filter') } } -export const RundownPlaylistFilter: React.FC = function RundownPlaylistFilter({ link, final }: IProps) { +export const RundownPlaylistFilter: React.FC = function RundownPlaylistFilter({ + index, + link, + readonly, + opened, + onClose, + onChange, + onFocus, + onInsertNext, + onRemove, + onChangeType, +}: IProps) { const { t } = useTranslation() + const fields: IRundownPlaylistFilterLink['field'][] = ['activationId', 'name', 'studioId', 'rehearsal'] + + const availableOptions = useTracker | string[]>( + () => { + return fieldToOptions(t, link.field) + }, + [link.field], + fieldToOptions(t, link.field) + ) + return ( -
    -
    {fieldToLabel(t, link.field)}
    -
    {link.value}
    -
    + { + onChange( + index, + { + ...link, + value: fieldValueMutate(link, newValue) as any, + }, + link + ) + }} + onChangeField={(newValue) => { + onChange( + index, + { + ...link, + field: newValue, + value: '', + }, + link + ) + }} + onFocus={onFocus} + onClose={onClose} + onInsertNext={onInsertNext} + onRemove={onRemove} + onChangeType={(newType) => onChangeType(index, newType)} + /> ) } diff --git a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/SwitchFilterType.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/SwitchFilterType.tsx new file mode 100644 index 00000000000..3eb9c296f87 --- /dev/null +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/SwitchFilterType.tsx @@ -0,0 +1,47 @@ +import { FilterType } from '@sofie-automation/blueprints-integration' +import Button from 'react-bootstrap/Button' +import ButtonGroup from 'react-bootstrap/ButtonGroup' +import { useTranslation } from 'react-i18next' + +export function SwitchFilterType({ + className, + allowedTypes, + selectedType, + onChangeType, +}: { + className?: string + allowedTypes: FilterType[] + selectedType: FilterType + onChangeType: (newType: FilterType) => void +}): JSX.Element { + const { t } = useTranslation() + + return ( + + {allowedTypes.includes('view') ? ( + + ) : null} + {allowedTypes.includes('rundownPlaylist') ? ( + + ) : null} + {allowedTypes.includes('adLib') ? ( + + ) : null} + + ) +} diff --git a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/ViewFilter.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/ViewFilter.tsx index fc74057b5cd..d568717c32a 100644 --- a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/ViewFilter.tsx +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/ViewFilter.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useLayoutEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { IGUIContextFilterLink } from '@sofie-automation/blueprints-integration' +import { FilterType, IGUIContextFilterLink } from '@sofie-automation/blueprints-integration' import classNames from 'classnames' import { usePopper } from 'react-popper' import { sameWidth } from '../../../../../../lib/popperUtils.js' @@ -8,6 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faAngleRight, faCheck, faTrash } from '@fortawesome/free-solid-svg-icons' import { catchError } from '../../../../../../lib/lib.js' import Button from 'react-bootstrap/Button' +import { SwitchFilterType } from './SwitchFilterType.js' interface IProps { index: number @@ -15,6 +16,7 @@ interface IProps { final?: boolean opened: boolean readonly?: boolean + onChangeType: (index: number, newType: FilterType) => void onClose: (index: number) => void onFocus: (index: number) => void onInsertNext: (index: number) => void @@ -27,6 +29,7 @@ export const ViewFilter: React.FC = function ViewFilter({ readonly, final, opened, + onChangeType, onClose, onFocus, onInsertNext, @@ -95,6 +98,13 @@ export const ViewFilter: React.FC = function ViewFilter({ style={styles.popper} {...attributes.popper} > + onChangeType(index, newType)} + /> +

    {t('Executes within the currently open Rundown, requires a Client-side trigger.')}

    diff --git a/packages/webui/src/client/ui/Settings/util/OverrideOpHelper.tsx b/packages/webui/src/client/ui/Settings/util/OverrideOpHelper.tsx index 23b002232b7..2e953931919 100644 --- a/packages/webui/src/client/ui/Settings/util/OverrideOpHelper.tsx +++ b/packages/webui/src/client/ui/Settings/util/OverrideOpHelper.tsx @@ -1,6 +1,16 @@ -import { SomeObjectOverrideOp, ObjectWithOverrides } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' -import { useRef, useEffect, useCallback } from 'react' -import { OverrideOpHelper, OverrideOpHelperImpl } from '@sofie-automation/corelib/dist/overrideOpHelper' +import { + SomeObjectOverrideOp, + ObjectWithOverrides, + applyAndValidateOverrides, +} from '@sofie-automation/corelib/dist/settings/objectWithOverrides' +import { useRef, useEffect, useCallback, useMemo } from 'react' +import { + OverrideOpHelper, + OverrideOpHelperImpl, + WrappedOverridableItemNormal, +} from '@sofie-automation/corelib/dist/overrideOpHelper' +import { ReadonlyDeep } from 'type-fest/source/readonly-deep' +import { literal } from '@sofie-automation/corelib/dist/lib' export type * from '@sofie-automation/corelib/dist/overrideOpHelper' export { @@ -27,3 +37,46 @@ export function useOverrideOpHelper( return new OverrideOpHelperImpl(saveOverrides, objectWithOverridesRef.current) }, [saveOverrides, objectWithOverridesRef]) } + +/** + * A helper to work with modifying an ObjectWithOverrides where T is a simple object (not an array of items) + */ +export function useOverrideOpHelperForSimpleObject( + saveOverrides: (newOps: SomeObjectOverrideOp[]) => void, + rawConfigObject: ReadonlyDeep> +): { + wrappedItem: WrappedOverridableItemNormal + overrideHelper: OverrideOpHelper +} { + const [wrappedItem, wrappedConfigObject] = useMemo(() => { + const prefixedOps = rawConfigObject.overrides.map((op) => ({ + ...op, + // Fixup the paths to match the wrappedItem produced below + path: `0.${op.path}`, + })) + + const computedValue = applyAndValidateOverrides(rawConfigObject).obj + + const wrappedItem = literal>({ + type: 'normal', + id: '0', + computed: computedValue, + defaults: rawConfigObject.defaults, + overrideOps: prefixedOps, + }) + + const wrappedConfigObject: ObjectWithOverrides = { + defaults: rawConfigObject.defaults as T, + overrides: prefixedOps, + } + + return [wrappedItem, wrappedConfigObject] + }, [rawConfigObject]) + + const overrideHelper = useOverrideOpHelper(saveOverrides, wrappedConfigObject) + + return { + wrappedItem, + overrideHelper, + } +} diff --git a/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx b/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx index 3288f3b2e21..cd1987a7230 100644 --- a/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/PartTimingPanel.tsx @@ -8,7 +8,7 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund import { dashboardElementStyle } from './DashboardPanel.js' import { RundownLayoutsAPI } from '../../lib/rundownLayouts.js' import { getAllowSpeaking, getAllowVibrating } from '../../lib/localStorage.js' -import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownTiming/CurrentPartOrSegmentRemaining.js' +import { CurrentPartOrSegmentRemaining } from '../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js' import { CurrentPartElapsed } from '../RundownView/RundownTiming/CurrentPartElapsed.js' import { getIsFilterActive } from '../../lib/rundownLayouts.js' import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyles' diff --git a/packages/webui/src/client/ui/Shelf/PieceCountdownPanel.tsx b/packages/webui/src/client/ui/Shelf/PieceCountdownPanel.tsx index 4827e12aa0c..46de9b9eaa2 100644 --- a/packages/webui/src/client/ui/Shelf/PieceCountdownPanel.tsx +++ b/packages/webui/src/client/ui/Shelf/PieceCountdownPanel.tsx @@ -1,5 +1,4 @@ -import * as React from 'react' -import _ from 'underscore' +import { useEffect, useState } from 'react' import ClassNames from 'classnames' import { RundownLayoutBase, @@ -8,7 +7,7 @@ import { } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' import { RundownLayoutsAPI } from '../../lib/rundownLayouts.js' import { dashboardElementStyle } from './DashboardPanel.js' -import { withTracker } from '../../lib/ReactMeteorData/ReactMeteorData.js' +import { useTracker } from '../../lib/ReactMeteorData/ReactMeteorData.js' import { RundownUtils } from '../../lib/rundown.js' import { RundownTiming, TimingEvent } from '../RundownView/RundownTiming/RundownTiming.js' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' @@ -20,104 +19,73 @@ import { ReadonlyDeep } from 'type-fest' interface IPieceCountdownPanelProps { visible?: boolean layout: RundownLayoutBase + panel: RundownLayoutPieceCountdown playlist: DBRundownPlaylist showStyleBase: UIShowStyleBase } -interface IPieceCountdownPanelTrackedProps { - livePieceInstance?: ReadonlyDeep -} +export function PieceCountdownPanel({ + visible, + layout, + panel, + playlist, + showStyleBase, +}: IPieceCountdownPanelProps): JSX.Element { + const [displayTimecode, setDisplayTimecode] = useState(0) -interface IState { - displayTimecode: number -} + const livePieceInstance = useTracker(() => { + const unfinishedPieces = getUnfinishedPieceInstancesReactive(playlist, showStyleBase) + const livePieceInstance: ReadonlyDeep | undefined = + panel.sourceLayerIds && panel.sourceLayerIds.length + ? unfinishedPieces.find((piece: ReadonlyDeep) => { + return ( + (panel.sourceLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 && + piece.partInstanceId === playlist.currentPartInfo?.partInstanceId + ) + }) + : undefined + return livePieceInstance + }, [playlist, showStyleBase, panel.sourceLayerIds]) -export class PieceCountdownPanelInner extends React.Component< - IPieceCountdownPanelProps & IPieceCountdownPanelTrackedProps, - IState -> { - constructor(props: IPieceCountdownPanelProps & IPieceCountdownPanelTrackedProps) { - super(props) - this.state = { - displayTimecode: 0, + useEffect(() => { + const updateTimecode = (e: TimingEvent) => { + let timecode = 0 + if (livePieceInstance && livePieceInstance.plannedStartedPlayback) { + const vtContent = livePieceInstance.piece.content as VTContent | undefined + const sourceDuration = vtContent?.sourceDuration || 0 + const seek = vtContent?.seek || 0 + const startedPlayback = livePieceInstance.plannedStartedPlayback + if (startedPlayback && sourceDuration > 0) { + timecode = e.detail.currentTime - (startedPlayback + sourceDuration - seek) + } + } + setDisplayTimecode(timecode) } - this.updateTimecode = this.updateTimecode.bind(this) - } - componentDidMount(): void { - window.addEventListener(RundownTiming.Events.timeupdateLowResolution, this.updateTimecode) - } + window.addEventListener(RundownTiming.Events.timeupdateLowResolution, updateTimecode) - componentWillUnmount(): void { - window.removeEventListener(RundownTiming.Events.timeupdateLowResolution, this.updateTimecode) - } - - private updateTimecode(e: TimingEvent) { - let timecode = 0 - if (this.props.livePieceInstance && this.props.livePieceInstance.plannedStartedPlayback) { - const vtContent = this.props.livePieceInstance.piece.content as VTContent | undefined - const sourceDuration = vtContent?.sourceDuration || 0 - const seek = vtContent?.seek || 0 - const startedPlayback = this.props.livePieceInstance.plannedStartedPlayback - if (startedPlayback && sourceDuration > 0) { - timecode = e.detail.currentTime - (startedPlayback + sourceDuration - seek) - } - } - if (this.state.displayTimecode != timecode) { - this.setState({ - displayTimecode: timecode, - }) + return () => { + window.removeEventListener(RundownTiming.Events.timeupdateLowResolution, updateTimecode) } - } + }, [livePieceInstance]) - render(): JSX.Element { - const isDashboardLayout = RundownLayoutsAPI.isDashboardLayout(this.props.layout) - return ( -
    + 0, + })} > - 0, - })} - > - {RundownUtils.formatDiffToTimecode( - this.state.displayTimecode || 0, - true, - false, - true, - false, - true, - '', - false, - true - )} - -
    - ) - } + {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} + +
    + ) } - -export const PieceCountdownPanel = withTracker( - (props: IPieceCountdownPanelProps & IPieceCountdownPanelTrackedProps) => { - const unfinishedPieces = getUnfinishedPieceInstancesReactive(props.playlist, props.showStyleBase) - const livePieceInstance: ReadonlyDeep | undefined = - props.panel.sourceLayerIds && props.panel.sourceLayerIds.length - ? unfinishedPieces.find((piece: ReadonlyDeep) => { - return ( - (props.panel.sourceLayerIds || []).indexOf(piece.piece.sourceLayerId) !== -1 && - piece.partInstanceId === props.playlist.currentPartInfo?.partInstanceId - ) - }) - : undefined - return { livePieceInstance } - }, - (_data, props: IPieceCountdownPanelProps, nextProps: IPieceCountdownPanelProps) => { - return !_.isEqual(props, nextProps) - } -)(PieceCountdownPanelInner) diff --git a/packages/webui/src/client/ui/Status/package-status/PackageContainerStatus.tsx b/packages/webui/src/client/ui/Status/package-status/PackageContainerStatus.tsx index 5b2b1f7a7a6..f62ef3772b1 100644 --- a/packages/webui/src/client/ui/Status/package-status/PackageContainerStatus.tsx +++ b/packages/webui/src/client/ui/Status/package-status/PackageContainerStatus.tsx @@ -83,57 +83,3 @@ export const PackageContainerStatus: React.FC<{ ) } - -// export const OLDPackageContainerStatus = withTranslation()( -// class PackageContainerStatus extends React.Component, {}> { -// constructor(props) { -// super(props) - -// this.state = {} -// } - -// render(): JSX.Element { -// const { t } = this.props -// const packageContainerStatus = this.props.packageContainerStatus - -// return ( -// <> -// -// -// {packageContainerStatus.containerId} -// -// -// -// -// -// {packageContainerStatus.status.statusReason.user} -// -// -// -// -// -// -// {Object.entries(packageContainerStatus.status.monitors).map(([monitorId, monitor]) => { -// return ( -// -// -// {monitorId} -// -// -// -// -// -// {monitor.statusReason.user} -// -// -// -// -// ) -// })} -// -// ) -// } -// } -// ) diff --git a/packages/webui/src/client/ui/Status/package-status/index.tsx b/packages/webui/src/client/ui/Status/package-status/index.tsx index 4fea26d793b..3242a9b09dd 100644 --- a/packages/webui/src/client/ui/Status/package-status/index.tsx +++ b/packages/webui/src/client/ui/Status/package-status/index.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from 'react' -import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/react-meteor-data.js' +import { useSubscriptionIfEnabled, useTracker } from '../../../lib/ReactMeteorData/react-meteor-data.js' import { ExpectedPackageWorkStatus } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackageWorkStatuses' import { normalizeArrayToMap } from '@sofie-automation/corelib/dist/lib' import { unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' @@ -34,15 +34,16 @@ export const ExpectedPackagesStatus: React.FC<{}> = function ExpectedPackagesSta UIStudios.find() .fetch() .map((studio) => studio._id), + [], [] ) const allSubsReady: boolean = [ - useSubscription(CorelibPubSub.expectedPackageWorkStatuses, studioIds ?? []), - useSubscription(CorelibPubSub.expectedPackages, studioIds ?? []), - useSubscription(CorelibPubSub.packageContainerStatuses, studioIds ?? []), - studioIds && studioIds.length > 0, + useSubscriptionIfEnabled(CorelibPubSub.expectedPackageWorkStatuses, studioIds.length > 0, studioIds), + useSubscriptionIfEnabled(CorelibPubSub.expectedPackages, studioIds.length > 0, studioIds), + useSubscriptionIfEnabled(CorelibPubSub.packageContainerStatuses, studioIds.length > 0, studioIds), + studioIds.length > 0, ].reduce((memo, value) => memo && value, true) || false const expectedPackageWorkStatuses = useTracker(() => ExpectedPackageWorkStatuses.find({}).fetch(), [], []) @@ -55,7 +56,11 @@ export const ExpectedPackagesStatus: React.FC<{}> = function ExpectedPackagesSta expectedPackageWorkStatuses.forEach((epws) => devices.add(epws.deviceId)) return Array.from(devices) }, [packageContainerStatuses, expectedPackageWorkStatuses]) - const peripheralDeviceSubReady = useSubscription(CorelibPubSub.peripheralDevices, deviceIds) + const peripheralDeviceSubReady = useSubscriptionIfEnabled( + CorelibPubSub.peripheralDevices, + deviceIds.length > 0, + deviceIds + ) const peripheralDevices = useTracker(() => PeripheralDevices.find().fetch(), [], []) const peripheralDevicesMap = normalizeArrayToMap(peripheralDevices, '_id') diff --git a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx index 835f0554bc6..f48ee562228 100644 --- a/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx +++ b/packages/webui/src/client/ui/UserEditOperations/PropertiesPanel.tsx @@ -17,7 +17,7 @@ import classNames from 'classnames' import { useTranslation } from 'react-i18next' import { useSelectedElements, useSelectedElementsContext } from '../RundownView/SelectedElementsContext.js' import { RundownUtils } from '../../lib/rundown.js' -import * as CoreIcon from '@nrk/core-icons/jsx' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useCallback, useMemo, useState } from 'react' import { SchemaFormWithState } from '../../lib/forms/SchemaFormWithState.js' import { translateMessage } from '@sofie-automation/corelib/dist/TranslatableMessage' @@ -154,7 +154,7 @@ export function PropertiesPanel(): JSX.Element { title={t('Close Properties Panel')} onClick={clearSelections} > - +
    diff --git a/packages/webui/src/client/ui/util/useToggleExpandHelper.tsx b/packages/webui/src/client/ui/util/useToggleExpandHelper.tsx index b41f298eca9..ef5f6fb9d88 100644 --- a/packages/webui/src/client/ui/util/useToggleExpandHelper.tsx +++ b/packages/webui/src/client/ui/util/useToggleExpandHelper.tsx @@ -1,9 +1,12 @@ import { ProtectedString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { useState, useCallback } from 'react' +export type ToggleSetExpanded = (id: ProtectedString | string | number, forceState?: boolean) => void +export type ToggleIsExpanded = (id: ProtectedString | string | number) => boolean + export function useToggleExpandHelper(): { - toggleExpanded: (id: ProtectedString | string | number, forceState?: boolean) => void - isExpanded: (id: ProtectedString | string | number) => boolean + toggleExpanded: ToggleSetExpanded + isExpanded: ToggleIsExpanded } { const [expandedItemIds, setExpandedItemIds] = useState>({}) diff --git a/packages/webui/src/client/utils/__tests__/dimensions.test.ts b/packages/webui/src/client/utils/__tests__/dimensions.test.ts index 4b13ee191a5..508492cc271 100644 --- a/packages/webui/src/client/utils/__tests__/dimensions.test.ts +++ b/packages/webui/src/client/utils/__tests__/dimensions.test.ts @@ -1,25 +1,23 @@ import { getElementWidth, getElementHeight } from '../dimensions.js' -import { createSandbox, SinonStub } from 'sinon' - -const sandbox = createSandbox() describe('client/utils/dimensions', () => { - type getComputedStyleType = (typeof window)['getComputedStyle'] - let getComputedStyle: SinonStub, any> //ReturnType> + let getComputedStyle: jest.SpyInstance beforeEach(() => { - getComputedStyle = sandbox.stub(window, 'getComputedStyle') + getComputedStyle = jest.spyOn(window, 'getComputedStyle') }) afterEach(() => { - sandbox.restore() + jest.restoreAllMocks() }) describe('getElementWidth', () => { test('returns width from getComputedStyle when it has a numeric value', () => { const expected = 20 const element = document.createElement('div') - getComputedStyle.withArgs(element).returns({ width: expected }) + getComputedStyle.mockImplementation((el: Element) => + el === element ? ({ width: expected } as any) : ({} as any) + ) const actual = getElementWidth(element) @@ -34,7 +32,9 @@ describe('client/utils/dimensions', () => { const element = document.createElement('div') Object.defineProperty(element, 'offsetWidth', { value: offsetWidth }) - getComputedStyle.withArgs(element).returns({ width: 'auto', paddingLeft, paddingRight }) + getComputedStyle.mockImplementation((el: Element) => + el === element ? ({ width: 'auto', paddingLeft, paddingRight } as any) : ({} as any) + ) const actual = getElementWidth(element) @@ -46,7 +46,9 @@ describe('client/utils/dimensions', () => { test('returns height from getComputedStyle when it has a numeric value', () => { const expected = 20 const element = document.createElement('div') - getComputedStyle.withArgs(element).returns({ height: expected }) + getComputedStyle.mockImplementation((el: Element) => + el === element ? ({ height: expected } as any) : ({} as any) + ) const actual = getElementHeight(element) @@ -61,7 +63,9 @@ describe('client/utils/dimensions', () => { const element = document.createElement('div') Object.defineProperty(element, 'scrollHeight', { value: scrollHeight }) - getComputedStyle.withArgs(element).returns({ height: 'auto', paddingTop, paddingBottom }) + getComputedStyle.mockImplementation((el: Element) => + el === element ? ({ height: 'auto', paddingTop, paddingBottom } as any) : ({} as any) + ) const actual = getElementHeight(element) diff --git a/packages/webui/vite.config.mts b/packages/webui/vite.config.mts index 17c1524bd55..d7a38968a76 100644 --- a/packages/webui/vite.config.mts +++ b/packages/webui/vite.config.mts @@ -59,6 +59,17 @@ export default defineConfig(({ command }) => ({ }, }, + css: { + preprocessorOptions: { + scss: { + // Silence deprecation warnings from Bootstrap and other dependencies + // This hides warnings from dependencies but still shows warnings from our own code + // Bootstrap 5.x not yet fully supporting Dart Sass 2.x causes many warnings + quietDeps: true, + }, + }, + }, + define: { __APP_VERSION__: JSON.stringify(process.env.npm_package_version), }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 477f1ce8d35..6d704d11fb6 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -275,16 +275,6 @@ __metadata: languageName: node linkType: hard -"@ampproject/remapping@npm:^2.2.0": - version: 2.2.1 - resolution: "@ampproject/remapping@npm:2.2.1" - dependencies: - "@jridgewell/gen-mapping": "npm:^0.3.0" - "@jridgewell/trace-mapping": "npm:^0.3.9" - checksum: 10/e15fecbf3b54c988c8b4fdea8ef514ab482537e8a080b2978cc4b47ccca7140577ca7b65ad3322dcce65bc73ee6e5b90cbfe0bbd8c766dad04d5c62ec9634c42 - languageName: node - linkType: hard - "@antfu/install-pkg@npm:^1.1.0": version: 1.1.0 resolution: "@antfu/install-pkg@npm:1.1.0" @@ -313,37 +303,25 @@ __metadata: languageName: node linkType: hard -"@apidevtools/json-schema-ref-parser@npm:9.0.9": - version: 9.0.9 - resolution: "@apidevtools/json-schema-ref-parser@npm:9.0.9" - dependencies: - "@jsdevtools/ono": "npm:^7.1.3" - "@types/json-schema": "npm:^7.0.6" - call-me-maybe: "npm:^1.0.1" - js-yaml: "npm:^4.1.0" - checksum: 10/4b73ebbb3a3c1d7620c993a7a7067d71897d9c8be32bf5cf5bee1d2fdab594b2ef32074cbd55464f28dc6930fa715e420fda2a06b23f8889559eedb4422e074e - languageName: node - linkType: hard - -"@apidevtools/json-schema-ref-parser@npm:^11.1.0": - version: 11.7.2 - resolution: "@apidevtools/json-schema-ref-parser@npm:11.7.2" +"@apidevtools/json-schema-ref-parser@npm:^11.1.0, @apidevtools/json-schema-ref-parser@npm:^11.5.5": + version: 11.9.3 + resolution: "@apidevtools/json-schema-ref-parser@npm:11.9.3" dependencies: "@jsdevtools/ono": "npm:^7.1.3" "@types/json-schema": "npm:^7.0.15" js-yaml: "npm:^4.1.0" - checksum: 10/8e80207c28aad234d3710fcfcf307691000bfbda40edb2ea4fdaf8158d026eb2b15a6471076490c2f40304df5b7bdd4be33d9979acef6cbfaf459b8bd1d79bf2 + checksum: 10/3d3618dbb611d1296b99bdee4ff0dde664dad47632d30e0310c6d10de8081f6378ccb58329ea4e03103eca9347d5143671d03f0527b1c3f0916d95f8c09215e2 languageName: node linkType: hard -"@apidevtools/json-schema-ref-parser@npm:^14.2.1": - version: 14.2.1 - resolution: "@apidevtools/json-schema-ref-parser@npm:14.2.1" +"@apidevtools/json-schema-ref-parser@npm:^15.2.2": + version: 15.2.2 + resolution: "@apidevtools/json-schema-ref-parser@npm:15.2.2" dependencies: - js-yaml: "npm:^4.1.0" + js-yaml: "npm:^4.1.1" peerDependencies: "@types/json-schema": ^7.0.15 - checksum: 10/c3f6d97c0e885f9543b0654258ee16b2dd75463c8496499563c278089043317f89010e89eb51699c7fb38dfb83cc8592f0b0c4983b764b56789dc3329b25ebfd + checksum: 10/9ed13cda5bd7cd5cc71b7e1cfcc8a7f4fa5f064ff03ba2d884cb0d2262b630690f50e7e7839f121795160049038e66b6c2da409b0a3e55c319a5f9924a084831 languageName: node linkType: hard @@ -402,6 +380,16 @@ __metadata: languageName: node linkType: hard +"@asyncapi/generator-components@npm:*": + version: 1.0.0 + resolution: "@asyncapi/generator-components@npm:1.0.0" + dependencies: + "@asyncapi/generator-react-sdk": "npm:^1.1.2" + "@asyncapi/modelina": "npm:^4.0.0-next.62" + checksum: 10/7d03ef95234c98e756155219064a27472cfbdce3aedc77336ad602de740e670e79a231efdc4927b5f0ebb817fbc47248f95991af813b74dfdec5c27f6172b007 + languageName: node + linkType: hard + "@asyncapi/generator-filters@npm:^2.1.0": version: 2.1.0 resolution: "@asyncapi/generator-filters@npm:2.1.0" @@ -413,6 +401,13 @@ __metadata: languageName: node linkType: hard +"@asyncapi/generator-helpers@npm:*": + version: 1.1.0 + resolution: "@asyncapi/generator-helpers@npm:1.1.0" + checksum: 10/d186699c7893aed7f76852a1c3848a4ac080b6ce36b7a757d1a5b8ab8bcc837469fbda595d94ed9afa38400c3fe2d45fa7e5e3eb62d367b36fa62acf2056eee3 + languageName: node + linkType: hard + "@asyncapi/generator-hooks@npm:*, @asyncapi/generator-hooks@npm:^0.1.0": version: 0.1.0 resolution: "@asyncapi/generator-hooks@npm:0.1.0" @@ -422,6 +417,24 @@ __metadata: languageName: node linkType: hard +"@asyncapi/generator-react-sdk@npm:*": + version: 1.1.3 + resolution: "@asyncapi/generator-react-sdk@npm:1.1.3" + dependencies: + "@asyncapi/parser": "npm:^3.1.0" + "@babel/core": "npm:7.12.9" + "@babel/preset-env": "npm:^7.12.7" + "@babel/preset-react": "npm:^7.12.7" + "@rollup/plugin-babel": "npm:^5.2.1" + babel-plugin-source-map-support: "npm:^2.1.3" + prop-types: "npm:^15.7.2" + react: "npm:^17.0.1" + rollup: "npm:^2.60.1" + source-map-support: "npm:^0.5.19" + checksum: 10/98d31b8083f4740f86b1301fcf551fb7865c840552052cf9d5c441a03ccdf63f3d0179cbaac3e954bbf354672b9807f8e6658d63eee624dfdd13aed93c4a6957 + languageName: node + linkType: hard + "@asyncapi/generator-react-sdk@npm:^1.1.2": version: 1.1.2 resolution: "@asyncapi/generator-react-sdk@npm:1.1.2" @@ -440,12 +453,14 @@ __metadata: languageName: node linkType: hard -"@asyncapi/generator@npm:^2.6.0": - version: 2.6.0 - resolution: "@asyncapi/generator@npm:2.6.0" +"@asyncapi/generator@npm:^2.11.0": + version: 2.11.0 + resolution: "@asyncapi/generator@npm:2.11.0" dependencies: + "@asyncapi/generator-components": "npm:*" + "@asyncapi/generator-helpers": "npm:*" "@asyncapi/generator-hooks": "npm:*" - "@asyncapi/generator-react-sdk": "npm:^1.1.2" + "@asyncapi/generator-react-sdk": "npm:*" "@asyncapi/multi-parser": "npm:^2.1.1" "@asyncapi/nunjucks-filters": "npm:*" "@asyncapi/parser": "npm:^3.0.14" @@ -458,7 +473,7 @@ __metadata: fs.extra: "npm:^1.3.2" global-dirs: "npm:^3.0.0" jmespath: "npm:^0.15.0" - js-yaml: "npm:^3.13.1" + js-yaml: "npm:^4.1.1" levenshtein-edit-distance: "npm:^2.0.5" loglevel: "npm:^1.6.8" minimatch: "npm:^3.0.4" @@ -474,30 +489,30 @@ __metadata: bin: ag: cli.js asyncapi-generator: cli.js - checksum: 10/0c7518061b811644b26129f6e62bdaceb263f2787b6248abcfabd47ecb2cb83accfc0fedf9492e0b5a1271881e6028f9f80159a7378bc7adc278810930dbf1b1 + checksum: 10/d97482aa86d89ab3990d384ea3d1c49c54d1888dbee4018d868d8371092fdfc61714bc2ba820e81212b57160a69ec1c629ba0b6f164444baefc94dd84f88ca97 languageName: node linkType: hard -"@asyncapi/html-template@npm:^3.2.0": - version: 3.2.0 - resolution: "@asyncapi/html-template@npm:3.2.0" +"@asyncapi/html-template@npm:^3.5.4": + version: 3.5.4 + resolution: "@asyncapi/html-template@npm:3.5.4" dependencies: "@asyncapi/generator-react-sdk": "npm:^1.1.2" - "@asyncapi/parser": "npm:^3.1.0" - "@asyncapi/react-component": "npm:^2.5.1" + "@asyncapi/parser": "npm:^3.6.0" + "@asyncapi/react-component": "npm:^3.0.1" highlight.js: "npm:10.7.3" - puppeteer: "npm:^14.1.0" + puppeteer: "npm:^24.4.0" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" rimraf: "npm:^3.0.2" sync-fetch: "npm:^0.5.2" - checksum: 10/638d9f9243e6f0e30f4a4bca32a50ad096f7e16910c68417d37ae42c78025739795c83ec2d1a8e4727bd43cc8aba1911554b3b0a4185579de7ae8497753eb789 + checksum: 10/a6168ff6534c62aea6dbe1b6e3d798500248c355f9c330e53846fa4e9cc1a8f465186a29c5f3ab4bdfb4ab0a2f92d9f8ca0a9a1dc911daa932b4ea2ccf30eddc languageName: node linkType: hard -"@asyncapi/modelina@npm:^4.0.4": - version: 4.0.4 - resolution: "@asyncapi/modelina@npm:4.0.4" +"@asyncapi/modelina@npm:^4.0.0-next.62": + version: 4.4.3 + resolution: "@asyncapi/modelina@npm:4.4.3" dependencies: "@apidevtools/json-schema-ref-parser": "npm:^11.1.0" "@apidevtools/swagger-parser": "npm:^10.1.0" @@ -506,9 +521,27 @@ __metadata: alterschema: "npm:^1.1.2" change-case: "npm:^4.1.2" js-yaml: "npm:^4.1.0" - openapi-types: "npm:9.3.0" + openapi-types: "npm:^12.1.3" typescript-json-schema: "npm:^0.58.1" - checksum: 10/de1599288c6741bb240fd5c8cb9410d215ea0e8198e0a7598da2fd82b84969365f74fd9dece9bcd90c2d9ee461e45444185706b55006f617bf373c08f6880e07 + checksum: 10/0ecb0d63c81374e03a0b440122d7bcf016eb908fb62014c31644f0cbf45dc45e258b22a1fbe60f0431c1292dbffa7c2043b0fdf74602e8d35183dca96e5827da + languageName: node + linkType: hard + +"@asyncapi/modelina@npm:^5.10.1": + version: 5.10.1 + resolution: "@asyncapi/modelina@npm:5.10.1" + dependencies: + "@apidevtools/json-schema-ref-parser": "npm:^11.1.0" + "@apidevtools/swagger-parser": "npm:^10.1.0" + "@asyncapi/multi-parser": "npm:^2.2.0" + "@asyncapi/parser": "npm:^3.4.0" + alterschema: "npm:^1.1.2" + change-case: "npm:^4.1.2" + fast-xml-parser: "npm:^5.3.0" + js-yaml: "npm:^4.1.0" + openapi-types: "npm:^12.1.3" + typescript-json-schema: "npm:^0.58.1" + checksum: 10/0176d873504e97a914fb48e0bcbb2a3bad0b009ef0e86e526f3d6cc847ff51d4d37cf6f309f93c932fa7c1bbd65806f27cedafb80286727df5bf8383a6bce72d languageName: node linkType: hard @@ -561,11 +594,11 @@ __metadata: languageName: node linkType: hard -"@asyncapi/parser@npm:*, @asyncapi/parser@npm:^3.0.14, @asyncapi/parser@npm:^3.1.0, @asyncapi/parser@npm:^3.3.0, @asyncapi/parser@npm:^3.4.0": - version: 3.4.0 - resolution: "@asyncapi/parser@npm:3.4.0" +"@asyncapi/parser@npm:*, @asyncapi/parser@npm:^3.0.14, @asyncapi/parser@npm:^3.1.0, @asyncapi/parser@npm:^3.4.0, @asyncapi/parser@npm:^3.6.0": + version: 3.6.0 + resolution: "@asyncapi/parser@npm:3.6.0" dependencies: - "@asyncapi/specs": "npm:^6.8.0" + "@asyncapi/specs": "npm:^6.11.1" "@openapi-contrib/openapi-schema-to-json-schema": "npm:~3.2.0" "@stoplight/json": "npm:3.21.0" "@stoplight/json-ref-readers": "npm:^1.2.2" @@ -581,21 +614,21 @@ __metadata: ajv-errors: "npm:^3.0.0" ajv-formats: "npm:^2.1.1" avsc: "npm:^5.7.5" - js-yaml: "npm:^4.1.0" - jsonpath-plus: "npm:^10.0.0" + js-yaml: "npm:^4.1.1" + jsonpath-plus: "npm:^10.0.7" node-fetch: "npm:2.6.7" - checksum: 10/67de9ca4a5257b9fb39e16349d9e3aa0f5d34b0343e5d9c0ea05f35171b604f356ab54ac769783caf580f5f3ef90914267aa60b17783ffbfa07af6af071f9f64 + checksum: 10/adc0db543c72f5ddb674fe4f2bbc070de03dffb69749cff49bdfcb202959e089588edf4371733cd55de4194adc2b485dd1002ff37ca69065a484209e135d7f95 languageName: node linkType: hard -"@asyncapi/protobuf-schema-parser@npm:^3.0.0, @asyncapi/protobuf-schema-parser@npm:^3.5.1": - version: 3.5.1 - resolution: "@asyncapi/protobuf-schema-parser@npm:3.5.1" +"@asyncapi/protobuf-schema-parser@npm:^3.0.0, @asyncapi/protobuf-schema-parser@npm:^3.6.0": + version: 3.6.0 + resolution: "@asyncapi/protobuf-schema-parser@npm:3.6.0" dependencies: "@asyncapi/parser": "npm:^3.4.0" "@types/protocol-buffers-schema": "npm:^3.4.3" protobufjs: "npm:^7.4.0" - checksum: 10/dbef0c14080f0894e2d2ca1f5f233485e3cce3f37bf82e3412be50322bd16366812ab933f35e38c35ee453a29ae20e2d8a811a6921fc5631cb5caf0b59fd839a + checksum: 10/595b5daf8a6162a5c67ad86b95657064b29a2a2f34223b825a22496969d2cebf64ba1c23336cfc323e1e0ae6a42e8418aa66eea06d0479bfc6b679250fdb5833 languageName: node linkType: hard @@ -611,14 +644,14 @@ __metadata: languageName: node linkType: hard -"@asyncapi/react-component@npm:^2.5.1": - version: 2.6.3 - resolution: "@asyncapi/react-component@npm:2.6.3" +"@asyncapi/react-component@npm:^3.0.1": + version: 3.0.1 + resolution: "@asyncapi/react-component@npm:3.0.1" dependencies: "@asyncapi/avro-schema-parser": "npm:^3.0.24" "@asyncapi/openapi-schema-parser": "npm:^3.0.24" - "@asyncapi/parser": "npm:^3.3.0" - "@asyncapi/protobuf-schema-parser": "npm:^3.5.1" + "@asyncapi/parser": "npm:^3.6.0" + "@asyncapi/protobuf-schema-parser": "npm:^3.6.0" highlight.js: "npm:^10.7.2" isomorphic-dompurify: "npm:^2.14.0" marked: "npm:^4.0.14" @@ -628,7 +661,7 @@ __metadata: peerDependencies: react: ">=18.0.0" react-dom: ">=18.0.0" - checksum: 10/7105385f8f806200638f10b799ff1a5d1838041d20d14c6e64b1e1a411933727b91cd3957c2057bc7946524be01c8ab7bb03946886534b15f4767c277da38445 + checksum: 10/d74c2264134801be020dadd2e226e0d9ce7531848551c532129bae9a73ebefef8da7045cc2130bf57e141e5966353bd23df666b7271f5b44c30af5bcf1e78d6f languageName: node linkType: hard @@ -641,7 +674,7 @@ __metadata: languageName: node linkType: hard -"@asyncapi/specs@npm:^6.0.0-next-major-spec.9, @asyncapi/specs@npm:^6.8.0": +"@asyncapi/specs@npm:^6.0.0-next-major-spec.9": version: 6.8.1 resolution: "@asyncapi/specs@npm:6.8.1" dependencies: @@ -650,21 +683,30 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.26.2, @babel/code-frame@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/code-frame@npm:7.28.6" +"@asyncapi/specs@npm:^6.11.1": + version: 6.11.1 + resolution: "@asyncapi/specs@npm:6.11.1" + dependencies: + "@types/json-schema": "npm:^7.0.11" + checksum: 10/51a9f8e61b85c519baee392641c83f021419f64c68e508732d6461c6b6a60d9891d84e53489a916ef0903a80dd736b0a0631bb97567488b7cc4383437806b84d + languageName: node + linkType: hard + +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.27.1, @babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/code-frame@npm:7.29.0" dependencies: "@babel/helper-validator-identifier": "npm:^7.28.5" js-tokens: "npm:^4.0.0" picocolors: "npm:^1.1.1" - checksum: 10/93e7ed9e039e3cb661bdb97c26feebafacc6ec13d745881dae5c7e2708f579475daebe7a3b5d23b183bb940b30744f52f4a5bcb65b4df03b79d82fcb38495784 + checksum: 10/199e15ff89007dd30675655eec52481cb245c9fdf4f81e4dc1f866603b0217b57aff25f5ffa0a95bbc8e31eb861695330cd7869ad52cc211aa63016320ef72c5 languageName: node linkType: hard -"@babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.26.5": - version: 7.26.5 - resolution: "@babel/compat-data@npm:7.26.5" - checksum: 10/afe35751f27bda80390fa221d5e37be55b7fc42cec80de9896086e20394f2306936c4296fcb4d62b683e3b49ba2934661ea7e06196ca2dacdc2e779fbea4a1a9 +"@babel/compat-data@npm:^7.28.6, @babel/compat-data@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/compat-data@npm:7.29.0" + checksum: 10/7f21beedb930ed8fbf7eabafc60e6e6521c1d905646bf1317a61b2163339157fe797efeb85962bf55136e166b01fd1a6b526a15974b92a8b877d564dcb6c9580 languageName: node linkType: hard @@ -692,191 +734,198 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.21.3, @babel/core@npm:^7.25.9, @babel/core@npm:^7.26.0, @babel/core@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/core@npm:7.26.7" - dependencies: - "@ampproject/remapping": "npm:^2.2.0" - "@babel/code-frame": "npm:^7.26.2" - "@babel/generator": "npm:^7.26.5" - "@babel/helper-compilation-targets": "npm:^7.26.5" - "@babel/helper-module-transforms": "npm:^7.26.0" - "@babel/helpers": "npm:^7.26.7" - "@babel/parser": "npm:^7.26.7" - "@babel/template": "npm:^7.25.9" - "@babel/traverse": "npm:^7.26.7" - "@babel/types": "npm:^7.26.7" +"@babel/core@npm:^7.21.3, @babel/core@npm:^7.23.9, @babel/core@npm:^7.25.9, @babel/core@npm:^7.27.4, @babel/core@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/core@npm:7.29.0" + dependencies: + "@babel/code-frame": "npm:^7.29.0" + "@babel/generator": "npm:^7.29.0" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-module-transforms": "npm:^7.28.6" + "@babel/helpers": "npm:^7.28.6" + "@babel/parser": "npm:^7.29.0" + "@babel/template": "npm:^7.28.6" + "@babel/traverse": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" + "@jridgewell/remapping": "npm:^2.3.5" convert-source-map: "npm:^2.0.0" debug: "npm:^4.1.0" gensync: "npm:^1.0.0-beta.2" json5: "npm:^2.2.3" semver: "npm:^6.3.1" - checksum: 10/1ca1c9b1366a1ee77ade9c72302f288b2b148e4190e0f36bc032d09c686b2c7973d3309e4eec2c57243508c16cf907c17dec4e34ba95e7a18badd57c61bbcb7c + checksum: 10/25f4e91688cdfbaf1365831f4f245b436cdaabe63d59389b75752013b8d61819ee4257101b52fc328b0546159fd7d0e74457ed7cf12c365fea54be4fb0a40229 languageName: node linkType: hard -"@babel/generator@npm:^7.12.5, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.26.5, @babel/generator@npm:^7.7.2": - version: 7.26.5 - resolution: "@babel/generator@npm:7.26.5" +"@babel/generator@npm:^7.12.5, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.27.5, @babel/generator@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/generator@npm:7.29.0" dependencies: - "@babel/parser": "npm:^7.26.5" - "@babel/types": "npm:^7.26.5" - "@jridgewell/gen-mapping": "npm:^0.3.5" - "@jridgewell/trace-mapping": "npm:^0.3.25" + "@babel/parser": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" jsesc: "npm:^3.0.2" - checksum: 10/aa5f176155431d1fb541ca11a7deddec0fc021f20992ced17dc2f688a0a9584e4ff4280f92e8a39302627345cd325762f70f032764806c579c6fd69432542bcb + checksum: 10/e144a5d3db43207e0909702c60a01928be8751c3df12cb99e94249a618358acd773c99d33c2209a9049142034e13591ba0a7ce938da49d9f7709dc3814020d1e languageName: node linkType: hard -"@babel/helper-annotate-as-pure@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-annotate-as-pure@npm:7.25.9" +"@babel/helper-annotate-as-pure@npm:^7.25.9, @babel/helper-annotate-as-pure@npm:^7.27.1, @babel/helper-annotate-as-pure@npm:^7.27.3": + version: 7.27.3 + resolution: "@babel/helper-annotate-as-pure@npm:7.27.3" dependencies: - "@babel/types": "npm:^7.25.9" - checksum: 10/41edda10df1ae106a9b4fe617bf7c6df77db992992afd46192534f5cff29f9e49a303231733782dd65c5f9409714a529f215325569f14282046e9d3b7a1ffb6c + "@babel/types": "npm:^7.27.3" + checksum: 10/63863a5c936ef82b546ca289c9d1b18fabfc24da5c4ee382830b124e2e79b68d626207febc8d4bffc720f50b2ee65691d7d12cc0308679dee2cd6bdc926b7190 languageName: node linkType: hard -"@babel/helper-compilation-targets@npm:^7.22.6, @babel/helper-compilation-targets@npm:^7.25.9, @babel/helper-compilation-targets@npm:^7.26.5": - version: 7.26.5 - resolution: "@babel/helper-compilation-targets@npm:7.26.5" +"@babel/helper-compilation-targets@npm:^7.27.1, @babel/helper-compilation-targets@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-compilation-targets@npm:7.28.6" dependencies: - "@babel/compat-data": "npm:^7.26.5" - "@babel/helper-validator-option": "npm:^7.25.9" + "@babel/compat-data": "npm:^7.28.6" + "@babel/helper-validator-option": "npm:^7.27.1" browserslist: "npm:^4.24.0" lru-cache: "npm:^5.1.1" semver: "npm:^6.3.1" - checksum: 10/f3b5f0bfcd7b6adf03be1a494b269782531c6e415afab2b958c077d570371cf1bfe001c442508092c50ed3711475f244c05b8f04457d8dea9c34df2b741522bf + checksum: 10/f512a5aeee4dfc6ea8807f521d085fdca8d66a7d068a6dd5e5b37da10a6081d648c0bbf66791a081e4e8e6556758da44831b331540965dfbf4f5275f3d0a8788 languageName: node linkType: hard -"@babel/helper-create-class-features-plugin@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-create-class-features-plugin@npm:7.25.9" - dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.25.9" - "@babel/helper-member-expression-to-functions": "npm:^7.25.9" - "@babel/helper-optimise-call-expression": "npm:^7.25.9" - "@babel/helper-replace-supers": "npm:^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" +"@babel/helper-create-class-features-plugin@npm:^7.25.9, @babel/helper-create-class-features-plugin@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-create-class-features-plugin@npm:7.28.6" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-member-expression-to-functions": "npm:^7.28.5" + "@babel/helper-optimise-call-expression": "npm:^7.27.1" + "@babel/helper-replace-supers": "npm:^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.6" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/d1d47a7b5fd317c6cb1446b0e4f4892c19ddaa69ea0229f04ba8bea5f273fc8168441e7114ad36ff919f2d310f97310cec51adc79002e22039a7e1640ccaf248 + checksum: 10/11f55607fcf66827ade745c0616aa3c6086aa655c0fab665dd3c4961829752e4c94c942262db30c4831ef9bce37ad444722e85ef1b7136587e28c6b1ef8ad43c languageName: node linkType: hard -"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.25.9": - version: 7.26.3 - resolution: "@babel/helper-create-regexp-features-plugin@npm:7.26.3" +"@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.27.1, @babel/helper-create-regexp-features-plugin@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.28.5" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.25.9" - regexpu-core: "npm:^6.2.0" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + regexpu-core: "npm:^6.3.1" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/4c44122ea11c4253ee78a9c083b7fbce96c725e2cb43cc864f0e8ea2749f7b6658617239c6278df9f132d09a7545c8fe0336ed2895ad7c80c71507828a7bc8ba + checksum: 10/d8791350fe0479af0909aa5efb6dfd3bacda743c7c3f8fa1b0bb18fe014c206505834102ee24382df1cfe5a83b4e4083220e97f420a48b2cec15bb1ad6c7c9d3 languageName: node linkType: hard -"@babel/helper-define-polyfill-provider@npm:^0.6.1, @babel/helper-define-polyfill-provider@npm:^0.6.2": - version: 0.6.3 - resolution: "@babel/helper-define-polyfill-provider@npm:0.6.3" +"@babel/helper-define-polyfill-provider@npm:^0.6.2, @babel/helper-define-polyfill-provider@npm:^0.6.6": + version: 0.6.6 + resolution: "@babel/helper-define-polyfill-provider@npm:0.6.6" dependencies: - "@babel/helper-compilation-targets": "npm:^7.22.6" - "@babel/helper-plugin-utils": "npm:^7.22.5" - debug: "npm:^4.1.1" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + debug: "npm:^4.4.3" lodash.debounce: "npm:^4.0.8" - resolve: "npm:^1.14.2" + resolve: "npm:^1.22.11" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/b79a77ac8fbf1aaf6c7f99191871760508e87d75a374ff3c39c6599a17d9bb82284797cd451769305764e504546caf22ae63367b22d6e45e32d0a8f4a34aab53 + checksum: 10/1c725c47bafb10ae4527aff6741b44ca49b18bf7005ae4583b15f992783e7c1d7687eab1a5583a373b5494160d46e91e29145280bd850e97d36b8b01bc5fef99 languageName: node linkType: hard -"@babel/helper-member-expression-to-functions@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-member-expression-to-functions@npm:7.25.9" +"@babel/helper-globals@npm:^7.28.0": + version: 7.28.0 + resolution: "@babel/helper-globals@npm:7.28.0" + checksum: 10/91445f7edfde9b65dcac47f4f858f68dc1661bf73332060ab67ad7cc7b313421099a2bfc4bda30c3db3842cfa1e86fffbb0d7b2c5205a177d91b22c8d7d9cb47 + languageName: node + linkType: hard + +"@babel/helper-member-expression-to-functions@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/helper-member-expression-to-functions@npm:7.28.5" dependencies: - "@babel/traverse": "npm:^7.25.9" - "@babel/types": "npm:^7.25.9" - checksum: 10/ef8cc1c1e600b012b312315f843226545a1a89f25d2f474ce2503fd939ca3f8585180f291a3a13efc56cf13eddc1d41a3a040eae9a521838fd59a6d04cc82490 + "@babel/traverse": "npm:^7.28.5" + "@babel/types": "npm:^7.28.5" + checksum: 10/05e0857cf7913f03d88ca62952d3888693c21a4f4d7cfc141c630983f71fc0a64393e05cecceb7701dfe98298f7cc38fcb735d892e3c8c6f56f112c85ee1b154 languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-module-imports@npm:7.25.9" +"@babel/helper-module-imports@npm:^7.10.4, @babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.25.9, @babel/helper-module-imports@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-module-imports@npm:7.28.6" dependencies: - "@babel/traverse": "npm:^7.25.9" - "@babel/types": "npm:^7.25.9" - checksum: 10/e090be5dee94dda6cd769972231b21ddfae988acd76b703a480ac0c96f3334557d70a965bf41245d6ee43891e7571a8b400ccf2b2be5803351375d0f4e5bcf08 + "@babel/traverse": "npm:^7.28.6" + "@babel/types": "npm:^7.28.6" + checksum: 10/64b1380d74425566a3c288074d7ce4dea56d775d2d3325a3d4a6df1dca702916c1d268133b6f385de9ba5b822b3c6e2af5d3b11ac88e5453d5698d77264f0ec0 languageName: node linkType: hard -"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.25.9, @babel/helper-module-transforms@npm:^7.26.0": - version: 7.26.0 - resolution: "@babel/helper-module-transforms@npm:7.26.0" +"@babel/helper-module-transforms@npm:^7.12.1, @babel/helper-module-transforms@npm:^7.27.1, @babel/helper-module-transforms@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-module-transforms@npm:7.28.6" dependencies: - "@babel/helper-module-imports": "npm:^7.25.9" - "@babel/helper-validator-identifier": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" + "@babel/helper-module-imports": "npm:^7.28.6" + "@babel/helper-validator-identifier": "npm:^7.28.5" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/9841d2a62f61ad52b66a72d08264f23052d533afc4ce07aec2a6202adac0bfe43014c312f94feacb3291f4c5aafe681955610041ece2c276271adce3f570f2f5 + checksum: 10/2e421c7db743249819ee51e83054952709dc2e197c7d5d415b4bdddc718580195704bfcdf38544b3f674efc2eccd4d29a65d38678fc827ed3934a7690984cd8b languageName: node linkType: hard -"@babel/helper-optimise-call-expression@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-optimise-call-expression@npm:7.25.9" +"@babel/helper-optimise-call-expression@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-optimise-call-expression@npm:7.27.1" dependencies: - "@babel/types": "npm:^7.25.9" - checksum: 10/f09d0ad60c0715b9a60c31841b3246b47d67650c512ce85bbe24a3124f1a4d66377df793af393273bc6e1015b0a9c799626c48e53747581c1582b99167cc65dc + "@babel/types": "npm:^7.27.1" + checksum: 10/0fb7ee824a384529d6b74f8a58279f9b56bfe3cce332168067dddeab2552d8eeb56dc8eaf86c04a3a09166a316cb92dfc79c4c623cd034ad4c563952c98b464f languageName: node linkType: hard -"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.8.0": - version: 7.26.5 - resolution: "@babel/helper-plugin-utils@npm:7.26.5" - checksum: 10/1cc0fd8514da3bb249bed6c27227696ab5e84289749d7258098701cffc0c599b7f61ec40dd332f8613030564b79899d9826813c96f966330bcfc7145a8377857 +"@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.25.9, @babel/helper-plugin-utils@npm:^7.26.5, @babel/helper-plugin-utils@npm:^7.27.1, @babel/helper-plugin-utils@npm:^7.28.6, @babel/helper-plugin-utils@npm:^7.8.0": + version: 7.28.6 + resolution: "@babel/helper-plugin-utils@npm:7.28.6" + checksum: 10/21c853bbc13dbdddf03309c9a0477270124ad48989e1ad6524b83e83a77524b333f92edd2caae645c5a7ecf264ec6d04a9ebe15aeb54c7f33c037b71ec521e4a languageName: node linkType: hard -"@babel/helper-remap-async-to-generator@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-remap-async-to-generator@npm:7.25.9" +"@babel/helper-remap-async-to-generator@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-remap-async-to-generator@npm:7.27.1" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.25.9" - "@babel/helper-wrap-function": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" + "@babel/helper-annotate-as-pure": "npm:^7.27.1" + "@babel/helper-wrap-function": "npm:^7.27.1" + "@babel/traverse": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/ea37ad9f8f7bcc27c109963b8ebb9d22bac7a5db2a51de199cb560e251d5593fe721e46aab2ca7d3e7a24b0aa4aff0eaf9c7307af9c2fd3a1d84268579073052 + checksum: 10/0747397ba013f87dbf575454a76c18210d61c7c9af0f697546b4bcac670b54ddc156330234407b397f0c948738c304c228e0223039bc45eab4fbf46966a5e8cc languageName: node linkType: hard -"@babel/helper-replace-supers@npm:^7.25.9": - version: 7.26.5 - resolution: "@babel/helper-replace-supers@npm:7.26.5" +"@babel/helper-replace-supers@npm:^7.27.1, @babel/helper-replace-supers@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/helper-replace-supers@npm:7.28.6" dependencies: - "@babel/helper-member-expression-to-functions": "npm:^7.25.9" - "@babel/helper-optimise-call-expression": "npm:^7.25.9" - "@babel/traverse": "npm:^7.26.5" + "@babel/helper-member-expression-to-functions": "npm:^7.28.5" + "@babel/helper-optimise-call-expression": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/cfb911d001a8c3d2675077dbb74ee8d7d5533b22d74f8d775cefabf19c604f6cbc22cfeb94544fe8efa626710d920f04acb22923017e68f46f5fdb1cb08b32ad + checksum: 10/ad2724713a4d983208f509e9607e8f950855f11bd97518a700057eb8bec69d687a8f90dc2da0c3c47281d2e3b79cf1d14ecf1fe3e1ee0a8e90b61aee6759c9a7 languageName: node linkType: hard -"@babel/helper-skip-transparent-expression-wrappers@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.25.9" +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.25.9, @babel/helper-skip-transparent-expression-wrappers@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.27.1" dependencies: - "@babel/traverse": "npm:^7.25.9" - "@babel/types": "npm:^7.25.9" - checksum: 10/fdbb5248932198bc26daa6abf0d2ac42cab9c2dbb75b7e9f40d425c8f28f09620b886d40e7f9e4e08ffc7aaa2cefe6fc2c44be7c20e81f7526634702fb615bdc + "@babel/traverse": "npm:^7.27.1" + "@babel/types": "npm:^7.27.1" + checksum: 10/4f380c5d0e0769fa6942a468b0c2d7c8f0c438f941aaa88f785f8752c103631d0904c7b4e76207a3b0e6588b2dec376595370d92ca8f8f1b422c14a69aa146d4 languageName: node linkType: hard @@ -887,32 +936,32 @@ __metadata: languageName: node linkType: hard -"@babel/helper-validator-identifier@npm:^7.25.9, @babel/helper-validator-identifier@npm:^7.28.5": +"@babel/helper-validator-identifier@npm:^7.28.5": version: 7.28.5 resolution: "@babel/helper-validator-identifier@npm:7.28.5" checksum: 10/8e5d9b0133702cfacc7f368bf792f0f8ac0483794877c6dca5fcb73810ee138e27527701826fb58a40a004f3a5ec0a2f3c3dd5e326d262530b119918f3132ba7 languageName: node linkType: hard -"@babel/helper-validator-option@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-validator-option@npm:7.25.9" - checksum: 10/9491b2755948ebbdd68f87da907283698e663b5af2d2b1b02a2765761974b1120d5d8d49e9175b167f16f72748ffceec8c9cf62acfbee73f4904507b246e2b3d +"@babel/helper-validator-option@npm:^7.25.9, @babel/helper-validator-option@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/helper-validator-option@npm:7.27.1" + checksum: 10/db73e6a308092531c629ee5de7f0d04390835b21a263be2644276cb27da2384b64676cab9f22cd8d8dbd854c92b1d7d56fc8517cf0070c35d1c14a8c828b0903 languageName: node linkType: hard -"@babel/helper-wrap-function@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-wrap-function@npm:7.25.9" +"@babel/helper-wrap-function@npm:^7.27.1": + version: 7.28.6 + resolution: "@babel/helper-wrap-function@npm:7.28.6" dependencies: - "@babel/template": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" - "@babel/types": "npm:^7.25.9" - checksum: 10/988dcf49159f1c920d6b9486762a93767a6e84b5e593a6342bc235f3e47cc1cb0c048d8fca531a48143e6b7fce1ff12ddbf735cf5f62cb2f07192cf7c27b89cf + "@babel/template": "npm:^7.28.6" + "@babel/traverse": "npm:^7.28.6" + "@babel/types": "npm:^7.28.6" + checksum: 10/d8a895a75399904746f4127db33593a20021fc55d1a5b5dfeb060b87cc13a8dceea91e70a4951bcd376ba9bd8232b0c04bff9a86c1dab83d691e01852c3b5bcd languageName: node linkType: hard -"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.26.7": +"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helpers@npm:7.28.6" dependencies: @@ -922,73 +971,73 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.26.5, @babel/parser@npm:^7.26.7, @babel/parser@npm:^7.28.6": - version: 7.28.6 - resolution: "@babel/parser@npm:7.28.6" +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/parser@npm:7.29.0" dependencies: - "@babel/types": "npm:^7.28.6" + "@babel/types": "npm:^7.29.0" bin: parser: ./bin/babel-parser.js - checksum: 10/483a6fb5f9876ec9cbbb98816f2c94f39ae4d1158d35f87e1c4bf19a1f56027c96a1a3962ff0c8c46e8322a6d9e1c80d26b7f9668410df13d5b5769d9447b010 + checksum: 10/b1576dca41074997a33ee740d87b330ae2e647f4b7da9e8d2abd3772b18385d303b0cee962b9b88425e0f30d58358dbb8d63792c1a2d005c823d335f6a029747 languageName: node linkType: hard -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.9" +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.28.5" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.5" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/3c23ef34e3fd7da3578428cb488180ab6b7b96c9c141438374b6d87fa814d87de099f28098e5fc64726c19193a1da397e4d2351d40b459bcd2489993557e2c74 + checksum: 10/750de98b34e6d09b545ded6e635b43cbab02fe319622964175259b98f41b16052e5931c4fbd45bad8cd0a37ebdd381233edecec9ee395b8ec51f47f47d1dbcd4 languageName: node linkType: hard -"@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:7.25.9" +"@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/d3e14ab1cb9cb50246d20cab9539f2fbd1e7ef1ded73980c8ad7c0561b4d5e0b144d362225f0976d47898e04cbd40f2000e208b0913bd788346cf7791b96af91 + checksum: 10/eb7f4146dc01f1198ce559a90b077e58b951a07521ec414e3c7d4593bf6c4ab5c2af22242a7e9fec085e20299e0ba6ea97f44a45e84ab148141bf9eb959ad25e languageName: node linkType: hard -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.25.9" +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/a9d1ee3fd100d3eb6799a2f2bbd785296f356c531d75c9369f71541811fa324270258a374db103ce159156d006da2f33370330558d0133e6f7584152c34997ca + checksum: 10/621cfddfcc99a81e74f8b6f9101fd260b27500cb1a568e3ceae9cc8afe9aee45ac3bca3900a2b66c612b1a2366d29ef67d4df5a1c975be727eaad6906f98c2c6 languageName: node linkType: hard -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.25.9" +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" - "@babel/plugin-transform-optional-chaining": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" + "@babel/plugin-transform-optional-chaining": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.13.0 - checksum: 10/5b298b28e156f64de51cdb03a2c5b80c7f978815ef1026f3ae8b9fc48d28bf0a83817d8fbecb61ef8fb94a7201f62cca5103cc6e7b9e8f28e38f766d7905b378 + checksum: 10/f07aa80272bd7a46b7ba11a4644da6c9b6a5a64e848dfaffdad6f02663adefd512e1aaebe664c4dd95f7ed4f80c872c7f8db8d8e34b47aae0930b412a28711a0 languageName: node linkType: hard -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.25.9" +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/cb893e5deb9312a0120a399835b6614a016c036714de7123c8edabccc56a09c4455016e083c5c4dd485248546d4e5e55fc0e9132b3c3a9bd16abf534138fe3f2 + checksum: 10/9377897aa7cba3a0b78a7c6015799ff71504b2b203329357e42ab3185d44aab07344ba33f5dd53f14d5340c1dc5a2587346343e0859538947bbab0484e72b914 languageName: node linkType: hard @@ -1023,7 +1072,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-class-properties@npm:^7.8.3": +"@babel/plugin-syntax-class-properties@npm:^7.12.13": version: 7.12.13 resolution: "@babel/plugin-syntax-class-properties@npm:7.12.13" dependencies: @@ -1034,6 +1083,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-class-static-block@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-class-static-block@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/3e80814b5b6d4fe17826093918680a351c2d34398a914ce6e55d8083d72a9bdde4fbaf6a2dcea0e23a03de26dc2917ae3efd603d27099e2b98380345703bf948 + languageName: node + linkType: hard + "@babel/plugin-syntax-dynamic-import@npm:^7.8.3": version: 7.8.3 resolution: "@babel/plugin-syntax-dynamic-import@npm:7.8.3" @@ -1045,29 +1105,29 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-assertions@npm:^7.26.0": - version: 7.26.0 - resolution: "@babel/plugin-syntax-import-assertions@npm:7.26.0" +"@babel/plugin-syntax-import-assertions@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/b58f2306df4a690ca90b763d832ec05202c50af787158ff8b50cdf3354359710bce2e1eb2b5135fcabf284756ac8eadf09ca74764aa7e76d12a5cac5f6b21e67 + checksum: 10/25017235e1e2c4ed892aa327a3fa10f4209cc618c6dd7806fc40c07d8d7d24a39743d3d5568b8d1c8f416cffe03c174e78874ded513c9338b07a7ab1dcbab050 languageName: node linkType: hard -"@babel/plugin-syntax-import-attributes@npm:^7.26.0": - version: 7.26.0 - resolution: "@babel/plugin-syntax-import-attributes@npm:7.26.0" +"@babel/plugin-syntax-import-attributes@npm:^7.24.7, @babel/plugin-syntax-import-attributes@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/c122aa577166c80ee67f75aebebeef4150a132c4d3109d25d7fc058bf802946f883e330f20b78c1d3e3a5ada631c8780c263d2d01b5dbaecc69efefeedd42916 + checksum: 10/6c8c6a5988dbb9799d6027360d1a5ba64faabf551f2ef11ba4eade0c62253b5c85d44ddc8eb643c74b9acb2bcaa664a950bd5de9a5d4aef291c4f2a48223bb4b languageName: node linkType: hard -"@babel/plugin-syntax-import-meta@npm:^7.8.3": +"@babel/plugin-syntax-import-meta@npm:^7.10.4": version: 7.10.4 resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" dependencies: @@ -1089,7 +1149,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.25.9, @babel/plugin-syntax-jsx@npm:^7.7.2": +"@babel/plugin-syntax-jsx@npm:^7.25.9": version: 7.25.9 resolution: "@babel/plugin-syntax-jsx@npm:7.25.9" dependencies: @@ -1100,7 +1160,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": +"@babel/plugin-syntax-jsx@npm:^7.27.1": + version: 7.28.6 + resolution: "@babel/plugin-syntax-jsx@npm:7.28.6" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.28.6" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/572e38f5c1bb4b8124300e7e3dd13e82ae84a21f90d3f0786c98cd05e63c78ca1f32d1cfe462dfbaf5e7d5102fa7cd8fd741dfe4f3afc2e01a3b2877dcc8c866 + languageName: node + linkType: hard + +"@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4": version: 7.10.4 resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" dependencies: @@ -1122,7 +1193,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-numeric-separator@npm:^7.8.3": +"@babel/plugin-syntax-numeric-separator@npm:^7.10.4": version: 7.10.4 resolution: "@babel/plugin-syntax-numeric-separator@npm:7.10.4" dependencies: @@ -1166,7 +1237,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-top-level-await@npm:^7.8.3": +"@babel/plugin-syntax-private-property-in-object@npm:^7.14.5": + version: 7.14.5 + resolution: "@babel/plugin-syntax-private-property-in-object@npm:7.14.5" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.14.5" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/b317174783e6e96029b743ccff2a67d63d38756876e7e5d0ba53a322e38d9ca452c13354a57de1ad476b4c066dbae699e0ca157441da611117a47af88985ecda + languageName: node + linkType: hard + +"@babel/plugin-syntax-top-level-await@npm:^7.14.5": version: 7.14.5 resolution: "@babel/plugin-syntax-top-level-await@npm:7.14.5" dependencies: @@ -1177,7 +1259,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.25.9, @babel/plugin-syntax-typescript@npm:^7.7.2": +"@babel/plugin-syntax-typescript@npm:^7.25.9": version: 7.25.9 resolution: "@babel/plugin-syntax-typescript@npm:7.25.9" dependencies: @@ -1188,6 +1270,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-typescript@npm:^7.27.1": + version: 7.28.6 + resolution: "@babel/plugin-syntax-typescript@npm:7.28.6" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.28.6" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/5c55f9c63bd36cf3d7e8db892294c8f85000f9c1526c3a1cc310d47d1e174f5c6f6605e5cc902c4636d885faba7a9f3d5e5edc6b35e4f3b1fd4c2d58d0304fa5 + languageName: node + linkType: hard + "@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6" @@ -1200,452 +1293,467 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-arrow-functions@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-arrow-functions@npm:7.25.9" +"@babel/plugin-transform-arrow-functions@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/c29f081224859483accf55fb4d091db2aac0dcd0d7954bac5ca889030cc498d3f771aa20eb2e9cd8310084ec394d85fa084b97faf09298b6bc9541182b3eb5bb + checksum: 10/62c2cc0ae2093336b1aa1376741c5ed245c0987d9e4b4c5313da4a38155509a7098b5acce582b6781cc0699381420010da2e3086353344abe0a6a0ec38961eb7 languageName: node linkType: hard -"@babel/plugin-transform-async-generator-functions@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-async-generator-functions@npm:7.25.9" +"@babel/plugin-transform-async-generator-functions@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.29.0" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/helper-remap-async-to-generator": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-remap-async-to-generator": "npm:^7.27.1" + "@babel/traverse": "npm:^7.29.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/99306c44a4a791abd51a56d89fa61c4cfe805a58e070c7fb1cbf950886778a6c8c4f25a92d231f91da1746d14a338436073fd83038e607f03a2a98ac5340406b + checksum: 10/e2c064a5eb212cbdf14f7c0113e069b845ca0f0ba431c1cc04607d3fc4f3bf1ed70f5c375fe7c61338a45db88bc1a79d270c8d633ce12256e1fce3666c1e6b93 languageName: node linkType: hard -"@babel/plugin-transform-async-to-generator@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-async-to-generator@npm:7.25.9" +"@babel/plugin-transform-async-to-generator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.28.6" dependencies: - "@babel/helper-module-imports": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/helper-remap-async-to-generator": "npm:^7.25.9" + "@babel/helper-module-imports": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-remap-async-to-generator": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/b3ad50fb93c171644d501864620ed23952a46648c4df10dc9c62cc9ad08031b66bd272cfdd708faeee07c23b6251b16f29ce0350473e4c79f0c32178d38ce3a6 + checksum: 10/bca5774263ec01dd2bf71c74bbaf7baa183bf03576636b7826c3346be70c8c8cb15cff549112f2983c36885131a0afde6c443591278c281f733ee17f455aa9b1 languageName: node linkType: hard -"@babel/plugin-transform-block-scoped-functions@npm:^7.26.5": - version: 7.26.5 - resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.26.5" +"@babel/plugin-transform-block-scoped-functions@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.26.5" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/f2046c09bf8e588bfb1a6342d0eee733189102cf663ade27adb0130f3865123af5816b40a55ec8d8fa09271b54dfdaf977cd2f8e0b3dc97f18e690188d5a2174 + checksum: 10/7fb4988ca80cf1fc8345310d5edfe38e86b3a72a302675cdd09404d5064fe1d1fe1283ebe658ad2b71445ecef857bfb29a748064306b5f6c628e0084759c2201 languageName: node linkType: hard -"@babel/plugin-transform-block-scoping@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-block-scoping@npm:7.25.9" +"@babel/plugin-transform-block-scoping@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-block-scoping@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/89dcdd7edb1e0c2f44e3c568a8ad8202e2574a8a8308248550a9391540bc3f5c9fbd8352c60ae90769d46f58d3ab36f2c3a0fbc1c3620813d92ff6fccdfa79c8 + checksum: 10/7ab8a0856024a5360ba16c3569b739385e939bc5a15ad7d811bec8459361a9aa5ee7c5f154a4e2ce79f5d66779c19464e7532600c31a1b6f681db4eb7e1c7bde languageName: node linkType: hard -"@babel/plugin-transform-class-properties@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-class-properties@npm:7.25.9" +"@babel/plugin-transform-class-properties@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-class-properties@npm:7.28.6" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/a8d69e2c285486b63f49193cbcf7a15e1d3a5f632c1c07d7a97f65306df7f554b30270b7378dde143f8b557d1f8f6336c643377943dec8ec405e4cd11e90b9ea + checksum: 10/200f30d44b36a768fa3a8cf690db9e333996af2ad14d9fa1b4c91a427ed9302907873b219b4ce87517ca1014a810eb2e929a6a66be68473f72b546fc64d04fbc languageName: node linkType: hard -"@babel/plugin-transform-class-static-block@npm:^7.26.0": - version: 7.26.0 - resolution: "@babel/plugin-transform-class-static-block@npm:7.26.0" +"@babel/plugin-transform-class-static-block@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-class-static-block@npm:7.28.6" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.12.0 - checksum: 10/60cba3f125a7bc4f90706af0a011697c7ffd2eddfba336ed6f84c5f358c44c3161af18b0202475241a96dee7964d96dd3a342f46dbf85b75b38bb789326e1766 + checksum: 10/bea7836846deefd02d9976ad1b30b5ade0d6329ecd92866db789dcf6aacfaf900b7a77031e25680f8de5ad636a771a5bdca8961361e6218d45d538ec5d9b71cc languageName: node linkType: hard -"@babel/plugin-transform-classes@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-classes@npm:7.25.9" +"@babel/plugin-transform-classes@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-classes@npm:7.28.6" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.25.9" - "@babel/helper-compilation-targets": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/helper-replace-supers": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" - globals: "npm:^11.1.0" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-replace-supers": "npm:^7.28.6" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/1914ebe152f35c667fba7bf17ce0d9d0f33df2fb4491990ce9bb1f9ec5ae8cbd11d95b0dc371f7a4cc5e7ce4cf89467c3e34857302911fc6bfb6494a77f7b37e + checksum: 10/9c3278a314d1c4bcda792bb22aced20e30c735557daf9bcc56397c0f3eb54761b21c770219e4581036a10dabda3e597321ed093bc245d5f4d561e19ceff66a6d languageName: node linkType: hard -"@babel/plugin-transform-computed-properties@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-computed-properties@npm:7.25.9" +"@babel/plugin-transform-computed-properties@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-computed-properties@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/template": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/template": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/aa1a9064d6a9d3b569b8cae6972437315a38a8f6553ee618406da5122500a06c2f20b9fa93aeed04dd895923bf6f529c09fc79d4be987ec41785ceb7d2203122 + checksum: 10/4a5e270f7e1f1e9787cf7cf133d48e3c1e38eb935d29a90331a1324d7c720f589b7b626b2e6485cd5521a7a13f2dbdc89a3e46ecbe7213d5bbb631175267c4aa languageName: node linkType: hard -"@babel/plugin-transform-destructuring@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-destructuring@npm:7.25.9" +"@babel/plugin-transform-destructuring@npm:^7.28.5": + version: 7.28.5 + resolution: "@babel/plugin-transform-destructuring@npm:7.28.5" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/traverse": "npm:^7.28.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/51b24fbead910ad0547463b2d214dd08076b22a66234b9f878b8bac117603dd23e05090ff86e9ffc373214de23d3e5bf1b095fe54cce2ca16b010264d90cf4f5 + checksum: 10/9cc67d3377bc5d8063599f2eb4588f5f9a8ab3abc9b64a40c24501fb3c1f91f4d5cf281ea9f208fd6b2ef8d9d8b018dacf1bed9493334577c966cd32370a7036 languageName: node linkType: hard -"@babel/plugin-transform-dotall-regex@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-dotall-regex@npm:7.25.9" +"@babel/plugin-transform-dotall-regex@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-dotall-regex@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/8bdf1bb9e6e3a2cc8154ae88a3872faa6dc346d6901994505fb43ac85f858728781f1219f40b67f7bb0687c507450236cb7838ac68d457e65637f98500aa161b + checksum: 10/866ffbbdee77fa955063b37c75593db8dbbe46b1ebb64cc788ea437e3a9aa41cb7b9afcee617c678a32b6705baa0892ec8e5d4b8af3bbb0ab1b254514ccdbd37 languageName: node linkType: hard -"@babel/plugin-transform-duplicate-keys@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-duplicate-keys@npm:7.25.9" +"@babel/plugin-transform-duplicate-keys@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-duplicate-keys@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/10dbb87bc09582416f9f97ca6c40563655abf33e3fd0fee25eeaeff28e946a06651192112a2bc2b18c314a638fa15c55b8365a677ef67aa490848cefdc57e1d8 + checksum: 10/987b718d2fab7626f61b72325c8121ead42341d6f46ad3a9b5e5f67f3ec558c903f1b8336277ffc43caac504ce00dd23a5456b5d1da23913333e1da77751f08d languageName: node linkType: hard -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.25.9" +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.29.0" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/f7233cf596be8c6843d31951afaf2464a62a610cb89c72c818c044765827fab78403ab8a7d3a6386f838c8df574668e2a48f6c206b1d7da965aff9c6886cb8e6 + checksum: 10/7fa7b773259a578c9e01c80946f75ecc074520064aa7a87a65db06c7df70766e2fa6be78cda55fa9418a14e30b2b9d595484a46db48074d495d9f877a4276065 languageName: node linkType: hard -"@babel/plugin-transform-dynamic-import@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-dynamic-import@npm:7.25.9" +"@babel/plugin-transform-dynamic-import@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/aaca1ccda819be9b2b85af47ba08ddd2210ff2dbea222f26e4cd33f97ab020884bf81a66197e50872721e9daf36ceb5659502c82199884ea74d5d75ecda5c58b + checksum: 10/7a9fbc8d17148b7f11a1d1ca3990d2c2cd44bd08a45dcaf14f20a017721235b9044b20e6168b6940282bb1b48fb78e6afbdfb9dd9d82fde614e15baa7d579932 languageName: node linkType: hard -"@babel/plugin-transform-exponentiation-operator@npm:^7.26.3": - version: 7.26.3 - resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.26.3" +"@babel/plugin-transform-explicit-resource-management@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-explicit-resource-management@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/plugin-transform-destructuring": "npm:^7.28.5" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0d8da2e552a50a775fe8e6e3c32621d20d3c5d1af7ab40ca2f5c7603de057b57b1b5850f74040e4ecbe36c09ac86d92173ad1e223a2a3b3df3cc359ca4349738 + checksum: 10/36d638a253dbdaee5548b4ddd21c04ee4e39914b207437bb64cf79bb41c2caadb4321768d3dba308c1016702649bc44efe751e2052de393004563c7376210d86 languageName: node linkType: hard -"@babel/plugin-transform-export-namespace-from@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-export-namespace-from@npm:7.25.9" +"@babel/plugin-transform-exponentiation-operator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/4dfe8df86c5b1d085d591290874bb2d78a9063090d71567ed657a418010ad333c3f48af2c974b865f53bbb718987a065f89828d43279a7751db1a56c9229078d + checksum: 10/b232152499370435c7cd4bf3321f58e189150e35ca3722ea16533d33434b97294df1342f5499671ec48e62b71c34cdea0ca8cf317ad12594a10f6fc670315e62 languageName: node linkType: hard -"@babel/plugin-transform-for-of@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-for-of@npm:7.25.9" +"@babel/plugin-transform-export-namespace-from@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/63a2db7fe06c2e3f5fc1926f478dac66a5f7b3eaeb4a0ffae577e6f3cb3d822cb1ed2ed3798f70f5cb1aa06bc2ad8bcd1f557342f5c425fd83c37a8fc1cfd2ba + checksum: 10/85082923eca317094f08f4953d8ea2a6558b3117826c0b740676983902b7236df1f4213ad844cb38c2dae104753dbe8f1cc51f01567835d476d32f5f544a4385 languageName: node linkType: hard -"@babel/plugin-transform-function-name@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-function-name@npm:7.25.9" +"@babel/plugin-transform-for-of@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-for-of@npm:7.27.1" dependencies: - "@babel/helper-compilation-targets": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/a8d7c8d019a6eb57eab5ca1be3e3236f175557d55b1f3b11f8ad7999e3fbb1cf37905fd8cb3a349bffb4163a558e9f33b63f631597fdc97c858757deac1b2fd7 + checksum: 10/705c591d17ef263c309bba8c38e20655e8e74ff7fd21883a9cdaf5bf1df42d724383ad3d88ac01f42926e15b1e1e66f2f7f8c4e87de955afffa290d52314b019 languageName: node linkType: hard -"@babel/plugin-transform-json-strings@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-json-strings@npm:7.25.9" +"@babel/plugin-transform-function-name@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-function-name@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-compilation-targets": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/traverse": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/e2498d84761cfd05aaea53799933d55af309c9d6204e66b38778792d171e4d1311ad34f334259a3aa3407dd0446f6bd3e390a1fcb8ce2e42fe5aabed0e41bee1 + checksum: 10/26a2a183c3c52a96495967420a64afc5a09f743a230272a131668abf23001e393afa6371e6f8e6c60f4182bea210ed31d1caf866452d91009c1daac345a52f23 languageName: node linkType: hard -"@babel/plugin-transform-literals@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-literals@npm:7.25.9" +"@babel/plugin-transform-json-strings@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-json-strings@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/3cca75823a38aab599bc151b0fa4d816b5e1b62d6e49c156aa90436deb6e13649f5505973151a10418b64f3f9d1c3da53e38a186402e0ed7ad98e482e70c0c14 + checksum: 10/69d82a1a0a72ed6e6f7969e09cf330516599d79b2b4e680e9dd3c57616a8c6af049b5103456e370ab56642815e80e46ed88bb81e9e059304a85c5fe0bf137c29 languageName: node linkType: hard -"@babel/plugin-transform-logical-assignment-operators@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.25.9" +"@babel/plugin-transform-literals@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-literals@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/8c6febb4ac53852314d28b5e2c23d5dbbff7bf1e57d61f9672e0d97531ef7778b3f0ad698dcf1179f5486e626c77127508916a65eb846a89e98a92f70ed3537b + checksum: 10/0a76d12ab19f32dd139964aea7da48cecdb7de0b75e207e576f0f700121fe92367d788f328bf4fb44b8261a0f605c97b44e62ae61cddbb67b14e94c88b411f95 languageName: node linkType: hard -"@babel/plugin-transform-member-expression-literals@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-member-expression-literals@npm:7.25.9" +"@babel/plugin-transform-logical-assignment-operators@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/db92041ae87b8f59f98b50359e0bb172480f6ba22e5e76b13bdfe07122cbf0daa9cd8ad2e78dcb47939938fed88ad57ab5989346f64b3a16953fc73dea3a9b1f + checksum: 10/36095d5d1cfc680e95298b5389a16016da800ae3379b130dabf557e94652c47b06610407e9fa44aaa03e9b0a5aa7b4b93348123985d44a45e369bf5f3497d149 languageName: node linkType: hard -"@babel/plugin-transform-modules-amd@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-modules-amd@npm:7.25.9" +"@babel/plugin-transform-member-expression-literals@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-member-expression-literals@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.27.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/804121430a6dcd431e6ffe99c6d1fbbc44b43478113b79c677629e7f877b4f78a06b69c6bfb2747fd84ee91879fe2eb32e4620b53124603086cf5b727593ebe8 + languageName: node + linkType: hard + +"@babel/plugin-transform-modules-amd@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-modules-amd@npm:7.27.1" dependencies: - "@babel/helper-module-transforms": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-module-transforms": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/75d34c6e709a23bcfa0e06f722c9a72b1d9ac3e7d72a07ef54a943d32f65f97cbbf0e387d874eb9d9b4c8d33045edfa8e8441d0f8794f3c2b9f1d71b928acf2c + checksum: 10/5ca9257981f2bbddd9dccf9126f1368de1cb335e7a5ff5cca9282266825af5b18b5f06c144320dcf5d2a200d2b53b6d22d9b801a55dc0509ab5a5838af7e61b7 languageName: node linkType: hard -"@babel/plugin-transform-modules-commonjs@npm:^7.25.9, @babel/plugin-transform-modules-commonjs@npm:^7.26.3": - version: 7.26.3 - resolution: "@babel/plugin-transform-modules-commonjs@npm:7.26.3" +"@babel/plugin-transform-modules-commonjs@npm:^7.25.9, @babel/plugin-transform-modules-commonjs@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.28.6" dependencies: - "@babel/helper-module-transforms": "npm:^7.26.0" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-module-transforms": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/f817f02fa04d13f1578f3026239b57f1003bebcf9f9b8d854714bed76a0e4986c79bd6d2e0ac14282c5d309454a8dab683c179709ca753b0152a69c69f3a78e3 + checksum: 10/ec6ea2958e778a7e0220f4a75cb5816cecddc6bd98efa10499fff7baabaa29a594d50d787a4ebf8a8ba66fefcf76ca2ded602be0b4554ae3317e53b3b3375b37 languageName: node linkType: hard -"@babel/plugin-transform-modules-systemjs@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-modules-systemjs@npm:7.25.9" +"@babel/plugin-transform-modules-systemjs@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.29.0" dependencies: - "@babel/helper-module-transforms": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/helper-validator-identifier": "npm:^7.25.9" - "@babel/traverse": "npm:^7.25.9" + "@babel/helper-module-transforms": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-validator-identifier": "npm:^7.28.5" + "@babel/traverse": "npm:^7.29.0" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/03145aa89b7c867941a03755216cfb503df6d475a78df84849a157fa5f2fcc17ba114a968d0579ae34e7c61403f35d1ba5d188fdfb9ad05f19354eb7605792f9 + checksum: 10/b3e64728eef02d829510778226da4c06be740fe52e0d45d4aa68b24083096d8ad7df67f2e9e67198b2e85f3237d42bd66f5771f85846f7a746105d05ca2e0cae languageName: node linkType: hard -"@babel/plugin-transform-modules-umd@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-modules-umd@npm:7.25.9" +"@babel/plugin-transform-modules-umd@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-modules-umd@npm:7.27.1" dependencies: - "@babel/helper-module-transforms": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-module-transforms": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/47d03485fedac828832d9fee33b3b982a6db8197e8651ceb5d001890e276150b5a7ee3e9780749e1ba76453c471af907a159108832c24f93453dd45221788e97 + checksum: 10/7388932863b4ee01f177eb6c2e2df9e2312005e43ada99897624d5565db4b9cef1e30aa7ad2c79bbe5373f284cfcddea98d8fe212714a24c6aba223272163058 languageName: node linkType: hard -"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.25.9" +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.29.0" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/434346ba05cf74e3f4704b3bdd439287b95cd2a8676afcdc607810b8c38b6f4798cd69c1419726b2e4c7204e62e4a04d31b0360e91ca57a930521c9211e07789 + checksum: 10/ed8c27699ca82a6c01cbfd39f3de16b90cfea4f8146a358057f76df290d308a66a8bd2e6734e6a87f68c18576e15d2d70548a84cd474d26fdf256c3f5ae44d8c languageName: node linkType: hard -"@babel/plugin-transform-new-target@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-new-target@npm:7.25.9" +"@babel/plugin-transform-new-target@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-new-target@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/07bb3a09028ee7b8e8ede6e6390e3b3aecc5cf9adb2fc5475ff58036c552b8a3f8e63d4c43211a60545f3307cdc15919f0e54cb5455d9546daed162dc54ff94e + checksum: 10/620d78ee476ae70960989e477dc86031ffa3d554b1b1999e6ec95261629f7a13e5a7b98579c63a009f9fdf14def027db57de1f0ae1f06fb6eaed8908ff65cf68 languageName: node linkType: hard -"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.26.6": - version: 7.26.6 - resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.26.6" +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.26.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/3832609f043dd1cd8076ab6a00a201573ef3f95bb2144d57787e4a973b3189884c16b4e77ff8e84a6ca47bc3b65bb7df10dca2f6163dfffc316ac96c37b0b5a6 + checksum: 10/88106952ca4f4fea8f97222a25f9595c6859d458d76905845dfa54f54e7d345e3dc338932e8c84a9c57a6c88b2f6d9ebff47130ce508a49c2b6e6a9f03858750 languageName: node linkType: hard -"@babel/plugin-transform-numeric-separator@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-numeric-separator@npm:7.25.9" +"@babel/plugin-transform-numeric-separator@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0528ef041ed88e8c3f51624ee87b8182a7f246fe4013f0572788e0727d20795b558f2b82e3989b5dd416cbd339500f0d88857de41b6d3b6fdacb1d5344bcc5b1 + checksum: 10/4b5ca60e481e22f0842761a3badca17376a230b5a7e5482338604eb95836c2d0c9c9bde53bdc5c2de1c6a12ae6c12de7464d098bf74b0943f85905ca358f0b68 languageName: node linkType: hard -"@babel/plugin-transform-object-rest-spread@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-object-rest-spread@npm:7.25.9" +"@babel/plugin-transform-object-rest-spread@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.28.6" dependencies: - "@babel/helper-compilation-targets": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/plugin-transform-parameters": "npm:^7.25.9" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/plugin-transform-destructuring": "npm:^7.28.5" + "@babel/plugin-transform-parameters": "npm:^7.27.7" + "@babel/traverse": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/a157ac5af2721090150858f301d9c0a3a0efb8ef66b90fce326d6cc0ae45ab97b6219b3e441bf8d72a2287e95eb04dd6c12544da88ea2345e70b3fac2c0ac9e2 + checksum: 10/9c8c51a515a5ec98a33a715e82d49f873e58b04b53fa1e826f3c2009f7133cd396d6730553a53d265e096dbfbea17dd100ae38815d0b506c094cb316a7a5519e languageName: node linkType: hard -"@babel/plugin-transform-object-super@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-object-super@npm:7.25.9" +"@babel/plugin-transform-object-super@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-object-super@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/helper-replace-supers": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/helper-replace-supers": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/1817b5d8b80e451ae1ad9080cca884f4f16df75880a158947df76a2ed8ab404d567a7dce71dd8051ef95f90fbe3513154086a32aba55cc76027f6cbabfbd7f98 + checksum: 10/46b819cb9a6cd3cfefe42d07875fee414f18d5e66040366ae856116db560ad4e16f3899a0a7fddd6773e0d1458444f94b208b67c0e3b6977a27ea17a5c13dbf6 languageName: node linkType: hard -"@babel/plugin-transform-optional-catch-binding@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.25.9" +"@babel/plugin-transform-optional-catch-binding@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/b46a8d1e91829f3db5c252583eb00d05a779b4660abeea5500fda0f8ffa3584fd18299443c22f7fddf0ed9dfdb73c782c43b445dc468d4f89803f2356963b406 + checksum: 10/ee24a17defec056eb9ef01824d7e4a1f65d531af6b4b79acfd0bcb95ce0b47926e80c61897f36f8c01ce733b069c9acdb1c9ce5ec07a729d0dbf9e8d859fe992 languageName: node linkType: hard -"@babel/plugin-transform-optional-chaining@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-optional-chaining@npm:7.25.9" +"@babel/plugin-transform-optional-chaining@npm:^7.27.1, @babel/plugin-transform-optional-chaining@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/bc838a499fd9892e163b8bc9bfbc4bf0b28cc3232ee0a6406ae078257c8096518f871d09b4a32c11f4a2d6953c3bc1984619ef748f7ad45aed0b0d9689a8eb36 + checksum: 10/c7cf29f99384a9a98748f04489a122c0106e0316aa64a2e61ef8af74c1057b587b96d9a08eb4e33d2ac17d1aaff1f0a86fae658d429fa7bcce4ef977e0ad684b languageName: node linkType: hard -"@babel/plugin-transform-parameters@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-parameters@npm:7.25.9" +"@babel/plugin-transform-parameters@npm:^7.27.7": + version: 7.27.7 + resolution: "@babel/plugin-transform-parameters@npm:7.27.7" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/014009a1763deb41fe9f0dbca2c4489ce0ac83dd87395f488492e8eb52399f6c883d5bd591bae3b8836f2460c3937fcebd07e57dce1e0bfe30cdbc63fdfc9d3a + checksum: 10/ba0aa8c977a03bf83030668f64c1d721e4e82d8cce89cdde75a2755862b79dbe9e7f58ca955e68c721fd494d6ee3826e46efad3fbf0855fcc92cb269477b4777 languageName: node linkType: hard -"@babel/plugin-transform-private-methods@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-private-methods@npm:7.25.9" +"@babel/plugin-transform-private-methods@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-private-methods@npm:7.28.6" dependencies: - "@babel/helper-create-class-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/6e3671b352c267847c53a170a1937210fa8151764d70d25005e711ef9b21969aaf422acc14f9f7fb86bc0e4ec43e7aefcc0ad9196ae02d262ec10f509f126a58 + checksum: 10/b80179b28f6a165674d0b0d6c6349b13a01dd282b18f56933423c0a33c23fc0626c8f011f859fc20737d021fe966eb8474a5233e4596401482e9ee7fb00e2aa2 languageName: node linkType: hard -"@babel/plugin-transform-private-property-in-object@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-private-property-in-object@npm:7.25.9" +"@babel/plugin-transform-private-property-in-object@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.28.6" dependencies: - "@babel/helper-annotate-as-pure": "npm:^7.25.9" - "@babel/helper-create-class-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-annotate-as-pure": "npm:^7.27.3" + "@babel/helper-create-class-features-plugin": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/aa45bb5669b610afa763d774a4b5583bb60ce7d38e4fd2dedfd0703e73e25aa560e6c6124e155aa90b101601743b127d9e5d3eb00989a7e4b4ab9c2eb88475ba + checksum: 10/d02008c62fd32ff747b850b8581ab5076b717320e1cb01c7fc66ebf5169095bd922e18cfb269992f85bc7fbd2cc61e5b5af25e2b54aad67411474b789ea94d5f languageName: node linkType: hard -"@babel/plugin-transform-property-literals@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-property-literals@npm:7.25.9" +"@babel/plugin-transform-property-literals@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-property-literals@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/436046ab07d54a9b44a384eeffec701d4e959a37a7547dda72e069e751ca7ff753d1782a8339e354b97c78a868b49ea97bf41bf5a44c6d7a3c0a05ad40eeb49c + checksum: 10/7caec27d5ed8870895c9faf4f71def72745d69da0d8e77903146a4e135fd7bed5778f5f9cebb36c5fba86338e6194dd67a08c033fc84b4299b7eceab6d9630cb languageName: node linkType: hard @@ -1682,25 +1790,25 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-self@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-react-jsx-self@npm:7.25.9" +"@babel/plugin-transform-react-jsx-self@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-react-jsx-self@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/41c833cd7f91b1432710f91b1325706e57979b2e8da44e83d86312c78bbe96cd9ef778b4e79e4e17ab25fa32c72b909f2be7f28e876779ede28e27506c41f4ae + checksum: 10/72cbae66a58c6c36f7e12e8ed79f292192d858dd4bb00e9e89d8b695e4c5cb6ef48eec84bffff421a5db93fd10412c581f1cccdb00264065df76f121995bdb68 languageName: node linkType: hard -"@babel/plugin-transform-react-jsx-source@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-react-jsx-source@npm:7.25.9" +"@babel/plugin-transform-react-jsx-source@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-react-jsx-source@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/a3e0e5672e344e9d01fb20b504fe29a84918eaa70cec512c4d4b1b035f72803261257343d8e93673365b72c371f35cf34bb0d129720bf178a4c87812c8b9c662 + checksum: 10/e2843362adb53692be5ee9fa07a386d2d8883daad2063a3575b3c373fc14cdf4ea7978c67a183cb631b4c9c8d77b2f48c24c088f8e65cc3600cb8e97d72a7161 languageName: node linkType: hard @@ -1731,38 +1839,37 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-regenerator@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-regenerator@npm:7.25.9" +"@babel/plugin-transform-regenerator@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/plugin-transform-regenerator@npm:7.29.0" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - regenerator-transform: "npm:^0.15.2" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/1c09e8087b476c5967282c9790fb8710e065eda77c60f6cb5da541edd59ded9d003d96f8ef640928faab4a0b35bf997673499a194973da4f0c97f0935807a482 + checksum: 10/c8fa9da74371568c5d34fd7d53de018752550cb10334040ca59e41f34b27f127974bdc5b4d1a1a8e8f3ebcf3cb7f650aa3f2df3b7bf1b7edf67c04493b9e3cb8 languageName: node linkType: hard -"@babel/plugin-transform-regexp-modifiers@npm:^7.26.0": - version: 7.26.0 - resolution: "@babel/plugin-transform-regexp-modifiers@npm:7.26.0" +"@babel/plugin-transform-regexp-modifiers@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-regexp-modifiers@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/726deca486bbd4b176f8a966eb0f4aabc19d9def3b8dabb8b3a656778eca0df1fda3f3c92b213aa5a184232fdafd5b7bd73b4e24ca4345c498ef6baff2bda4e1 + checksum: 10/5aacc570034c085afa0165137bb9a04cd4299b86eb9092933a96dcc1132c8f591d9d534419988f5f762b2f70d43a3c719a6b8fa05fdd3b2b1820d01cf85500da languageName: node linkType: hard -"@babel/plugin-transform-reserved-words@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-reserved-words@npm:7.25.9" +"@babel/plugin-transform-reserved-words@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-reserved-words@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/8beda04481b25767acbd1f6b9ef7b3a9c12fbd9dcb24df45a6ad120e1dc4b247c073db60ac742f9093657d6d8c050501fc0606af042f81a3bb6a3ff862cddc47 + checksum: 10/dea0b66742d2863b369c06c053e11e15ba785892ea19cccf7aef3c1bdaa38b6ab082e19984c5ea7810d275d9445c5400fcc385ad71ce707ed9256fadb102af3b languageName: node linkType: hard @@ -1782,59 +1889,59 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-shorthand-properties@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-shorthand-properties@npm:7.25.9" +"@babel/plugin-transform-shorthand-properties@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/f774995d58d4e3a992b732cf3a9b8823552d471040e280264dd15e0735433d51b468fef04d75853d061309389c66bda10ce1b298297ce83999220eb0ad62741d + checksum: 10/fbba6e2aef0b69681acb68202aa249c0598e470cc0853d7ff5bd0171fd6a7ec31d77cfabcce9df6360fc8349eded7e4a65218c32551bd3fc0caaa1ac899ac6d4 languageName: node linkType: hard -"@babel/plugin-transform-spread@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-spread@npm:7.25.9" +"@babel/plugin-transform-spread@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-spread@npm:7.28.6" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/fe72c6545267176cdc9b6f32f30f9ced37c1cafa1290e4436b83b8f377b4f1c175dad404228c96e3efdec75da692f15bfb9db2108fcd9ad260bc9968778ee41e + checksum: 10/1fa02ac60ae5e49d46fa2966aaf3f7578cf37255534c2ecf379d65855088a1623c3eea28b9ee6a0b1413b0199b51f9019d0da3fe9da89986bc47e07242415f60 languageName: node linkType: hard -"@babel/plugin-transform-sticky-regex@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-sticky-regex@npm:7.25.9" +"@babel/plugin-transform-sticky-regex@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-sticky-regex@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/7454b00844dbe924030dd15e2b3615b36e196500c4c47e98dabc6b37a054c5b1038ecd437e910aabf0e43bf56b973cb148d3437d50f6e2332d8309568e3e979b + checksum: 10/e1414a502efba92c7974681767e365a8cda6c5e9e5f33472a9eaa0ce2e75cea0a9bef881ff8dda37c7810ad902f98d3c00ead92a3ac3b73a79d011df85b5a189 languageName: node linkType: hard -"@babel/plugin-transform-template-literals@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-template-literals@npm:7.25.9" +"@babel/plugin-transform-template-literals@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-template-literals@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/92eb1d6e2d95bd24abbb74fa7640d02b66ff6214e0bb616d7fda298a7821ce15132a4265d576a3502a347a3c9e94b6c69ed265bb0784664592fa076785a3d16a + checksum: 10/93aad782503b691faef7c0893372d5243df3219b07f1f22cfc32c104af6a2e7acd6102c128439eab15336d048f1b214ca134b87b0630d8cd568bf447f78b25ce languageName: node linkType: hard -"@babel/plugin-transform-typeof-symbol@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/plugin-transform-typeof-symbol@npm:7.26.7" +"@babel/plugin-transform-typeof-symbol@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-typeof-symbol@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.26.5" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/c4ed244c9f252f941f4dff4b6ad06f6d6f5860e9aa5a6cccb5725ead670f2dab58bba4bad9c2b7bd25685e5205fde810857df964d417072c5c282bbfa4f6bf7a + checksum: 10/812d736402a6f9313b86b8adf36740394400be7a09c48e51ee45ab4a383a3f46fc618d656dd12e44934665e42ae71cf143e25b95491b699ef7c737950dbdb862 languageName: node linkType: hard @@ -1853,129 +1960,130 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-unicode-escapes@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-unicode-escapes@npm:7.25.9" +"@babel/plugin-transform-unicode-escapes@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-unicode-escapes@npm:7.27.1" dependencies: - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/f138cbee539963fb3da13f684e6f33c9f7495220369ae12a682b358f1e25ac68936825562c38eae87f01ac9992b2129208b35ec18533567fc805ce5ed0ffd775 + checksum: 10/87b9e49dee4ab6e78f4cdcdbdd837d7784f02868a96bfc206c8dbb17dd85db161b5a0ecbe95b19a42e8aea0ce57e80249e1facbf9221d7f4114d52c3b9136c9e languageName: node linkType: hard -"@babel/plugin-transform-unicode-property-regex@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.25.9" +"@babel/plugin-transform-unicode-property-regex@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/201f6f46c1beb399e79aa208b94c5d54412047511795ce1e790edcd189cef73752e6a099fdfc01b3ad12205f139ae344143b62f21f44bbe02338a95e8506a911 + checksum: 10/d14e8c51aa73f592575c1543400fd67d96df6410d75c9dc10dd640fd7eecb37366a2f2368bbdd7529842532eda4af181c921bda95146c6d373c64ea59c6e9991 languageName: node linkType: hard -"@babel/plugin-transform-unicode-regex@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-unicode-regex@npm:7.25.9" +"@babel/plugin-transform-unicode-regex@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-unicode-regex@npm:7.27.1" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-create-regexp-features-plugin": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.27.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/e8baae867526e179467c6ef5280d70390fa7388f8763a19a27c21302dd59b121032568be080749514b097097ceb9af716bf4b90638f1b3cf689aa837ba20150f + checksum: 10/a34d89a2b75fb78e66d97c3dc90d4877f7e31f43316b52176f95a5dee20e9bb56ecf158eafc42a001676ddf7b393d9e67650bad6b32f5405780f25fb83cd68e3 languageName: node linkType: hard -"@babel/plugin-transform-unicode-sets-regex@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.25.9" +"@babel/plugin-transform-unicode-sets-regex@npm:^7.28.6": + version: 7.28.6 + resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.28.6" dependencies: - "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" - "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-create-regexp-features-plugin": "npm:^7.28.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0 - checksum: 10/4445ef20de687cb4dcc95169742a8d9013d680aa5eee9186d8e25875bbfa7ee5e2de26a91177ccf70b1db518e36886abcd44750d28db5d7a9539f0efa6839f4b + checksum: 10/423971fe2eef9d18782b1c30f5f42613ee510e5b9c08760c5538a0997b36c34495acce261e0e37a27831f81330359230bd1f33c2e1822de70241002b45b7d68e languageName: node linkType: hard -"@babel/preset-env@npm:^7.12.7, @babel/preset-env@npm:^7.20.2, @babel/preset-env@npm:^7.25.9, @babel/preset-env@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/preset-env@npm:7.26.7" +"@babel/preset-env@npm:^7.12.7, @babel/preset-env@npm:^7.20.2, @babel/preset-env@npm:^7.25.9, @babel/preset-env@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/preset-env@npm:7.29.0" dependencies: - "@babel/compat-data": "npm:^7.26.5" - "@babel/helper-compilation-targets": "npm:^7.26.5" - "@babel/helper-plugin-utils": "npm:^7.26.5" - "@babel/helper-validator-option": "npm:^7.25.9" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "npm:^7.25.9" - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "npm:^7.25.9" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.25.9" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.25.9" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.25.9" + "@babel/compat-data": "npm:^7.29.0" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-plugin-utils": "npm:^7.28.6" + "@babel/helper-validator-option": "npm:^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "npm:^7.28.5" + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "npm:^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.28.6" "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions": "npm:^7.26.0" - "@babel/plugin-syntax-import-attributes": "npm:^7.26.0" + "@babel/plugin-syntax-import-assertions": "npm:^7.28.6" + "@babel/plugin-syntax-import-attributes": "npm:^7.28.6" "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" - "@babel/plugin-transform-arrow-functions": "npm:^7.25.9" - "@babel/plugin-transform-async-generator-functions": "npm:^7.25.9" - "@babel/plugin-transform-async-to-generator": "npm:^7.25.9" - "@babel/plugin-transform-block-scoped-functions": "npm:^7.26.5" - "@babel/plugin-transform-block-scoping": "npm:^7.25.9" - "@babel/plugin-transform-class-properties": "npm:^7.25.9" - "@babel/plugin-transform-class-static-block": "npm:^7.26.0" - "@babel/plugin-transform-classes": "npm:^7.25.9" - "@babel/plugin-transform-computed-properties": "npm:^7.25.9" - "@babel/plugin-transform-destructuring": "npm:^7.25.9" - "@babel/plugin-transform-dotall-regex": "npm:^7.25.9" - "@babel/plugin-transform-duplicate-keys": "npm:^7.25.9" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.25.9" - "@babel/plugin-transform-dynamic-import": "npm:^7.25.9" - "@babel/plugin-transform-exponentiation-operator": "npm:^7.26.3" - "@babel/plugin-transform-export-namespace-from": "npm:^7.25.9" - "@babel/plugin-transform-for-of": "npm:^7.25.9" - "@babel/plugin-transform-function-name": "npm:^7.25.9" - "@babel/plugin-transform-json-strings": "npm:^7.25.9" - "@babel/plugin-transform-literals": "npm:^7.25.9" - "@babel/plugin-transform-logical-assignment-operators": "npm:^7.25.9" - "@babel/plugin-transform-member-expression-literals": "npm:^7.25.9" - "@babel/plugin-transform-modules-amd": "npm:^7.25.9" - "@babel/plugin-transform-modules-commonjs": "npm:^7.26.3" - "@babel/plugin-transform-modules-systemjs": "npm:^7.25.9" - "@babel/plugin-transform-modules-umd": "npm:^7.25.9" - "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.25.9" - "@babel/plugin-transform-new-target": "npm:^7.25.9" - "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.26.6" - "@babel/plugin-transform-numeric-separator": "npm:^7.25.9" - "@babel/plugin-transform-object-rest-spread": "npm:^7.25.9" - "@babel/plugin-transform-object-super": "npm:^7.25.9" - "@babel/plugin-transform-optional-catch-binding": "npm:^7.25.9" - "@babel/plugin-transform-optional-chaining": "npm:^7.25.9" - "@babel/plugin-transform-parameters": "npm:^7.25.9" - "@babel/plugin-transform-private-methods": "npm:^7.25.9" - "@babel/plugin-transform-private-property-in-object": "npm:^7.25.9" - "@babel/plugin-transform-property-literals": "npm:^7.25.9" - "@babel/plugin-transform-regenerator": "npm:^7.25.9" - "@babel/plugin-transform-regexp-modifiers": "npm:^7.26.0" - "@babel/plugin-transform-reserved-words": "npm:^7.25.9" - "@babel/plugin-transform-shorthand-properties": "npm:^7.25.9" - "@babel/plugin-transform-spread": "npm:^7.25.9" - "@babel/plugin-transform-sticky-regex": "npm:^7.25.9" - "@babel/plugin-transform-template-literals": "npm:^7.25.9" - "@babel/plugin-transform-typeof-symbol": "npm:^7.26.7" - "@babel/plugin-transform-unicode-escapes": "npm:^7.25.9" - "@babel/plugin-transform-unicode-property-regex": "npm:^7.25.9" - "@babel/plugin-transform-unicode-regex": "npm:^7.25.9" - "@babel/plugin-transform-unicode-sets-regex": "npm:^7.25.9" + "@babel/plugin-transform-arrow-functions": "npm:^7.27.1" + "@babel/plugin-transform-async-generator-functions": "npm:^7.29.0" + "@babel/plugin-transform-async-to-generator": "npm:^7.28.6" + "@babel/plugin-transform-block-scoped-functions": "npm:^7.27.1" + "@babel/plugin-transform-block-scoping": "npm:^7.28.6" + "@babel/plugin-transform-class-properties": "npm:^7.28.6" + "@babel/plugin-transform-class-static-block": "npm:^7.28.6" + "@babel/plugin-transform-classes": "npm:^7.28.6" + "@babel/plugin-transform-computed-properties": "npm:^7.28.6" + "@babel/plugin-transform-destructuring": "npm:^7.28.5" + "@babel/plugin-transform-dotall-regex": "npm:^7.28.6" + "@babel/plugin-transform-duplicate-keys": "npm:^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.29.0" + "@babel/plugin-transform-dynamic-import": "npm:^7.27.1" + "@babel/plugin-transform-explicit-resource-management": "npm:^7.28.6" + "@babel/plugin-transform-exponentiation-operator": "npm:^7.28.6" + "@babel/plugin-transform-export-namespace-from": "npm:^7.27.1" + "@babel/plugin-transform-for-of": "npm:^7.27.1" + "@babel/plugin-transform-function-name": "npm:^7.27.1" + "@babel/plugin-transform-json-strings": "npm:^7.28.6" + "@babel/plugin-transform-literals": "npm:^7.27.1" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.28.6" + "@babel/plugin-transform-member-expression-literals": "npm:^7.27.1" + "@babel/plugin-transform-modules-amd": "npm:^7.27.1" + "@babel/plugin-transform-modules-commonjs": "npm:^7.28.6" + "@babel/plugin-transform-modules-systemjs": "npm:^7.29.0" + "@babel/plugin-transform-modules-umd": "npm:^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.29.0" + "@babel/plugin-transform-new-target": "npm:^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.28.6" + "@babel/plugin-transform-numeric-separator": "npm:^7.28.6" + "@babel/plugin-transform-object-rest-spread": "npm:^7.28.6" + "@babel/plugin-transform-object-super": "npm:^7.27.1" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.28.6" + "@babel/plugin-transform-optional-chaining": "npm:^7.28.6" + "@babel/plugin-transform-parameters": "npm:^7.27.7" + "@babel/plugin-transform-private-methods": "npm:^7.28.6" + "@babel/plugin-transform-private-property-in-object": "npm:^7.28.6" + "@babel/plugin-transform-property-literals": "npm:^7.27.1" + "@babel/plugin-transform-regenerator": "npm:^7.29.0" + "@babel/plugin-transform-regexp-modifiers": "npm:^7.28.6" + "@babel/plugin-transform-reserved-words": "npm:^7.27.1" + "@babel/plugin-transform-shorthand-properties": "npm:^7.27.1" + "@babel/plugin-transform-spread": "npm:^7.28.6" + "@babel/plugin-transform-sticky-regex": "npm:^7.27.1" + "@babel/plugin-transform-template-literals": "npm:^7.27.1" + "@babel/plugin-transform-typeof-symbol": "npm:^7.27.1" + "@babel/plugin-transform-unicode-escapes": "npm:^7.27.1" + "@babel/plugin-transform-unicode-property-regex": "npm:^7.28.6" + "@babel/plugin-transform-unicode-regex": "npm:^7.27.1" + "@babel/plugin-transform-unicode-sets-regex": "npm:^7.28.6" "@babel/preset-modules": "npm:0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2: "npm:^0.4.10" - babel-plugin-polyfill-corejs3: "npm:^0.10.6" - babel-plugin-polyfill-regenerator: "npm:^0.6.1" - core-js-compat: "npm:^3.38.1" + babel-plugin-polyfill-corejs2: "npm:^0.4.15" + babel-plugin-polyfill-corejs3: "npm:^0.14.0" + babel-plugin-polyfill-regenerator: "npm:^0.6.6" + core-js-compat: "npm:^3.48.0" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/d5833ac61580ca8ca672466d06edcf523b49f400caa8f4b8f21358a30875a8ca1628a250b89369e8a0be3439f6ae0002af6f64335794b06acaf603906055f43a + checksum: 10/211b33ec8644636275f61aa273071d8cbc2a6bb28d82ad246e3831a6aa7d96c610a55b5140bcd21be7f71fb04c3aa4a10eb08665fb5505e153cfdd8dbc8c1c1c languageName: node linkType: hard @@ -2024,25 +2132,22 @@ __metadata: linkType: hard "@babel/runtime-corejs3@npm:^7.25.9": - version: 7.26.7 - resolution: "@babel/runtime-corejs3@npm:7.26.7" + version: 7.29.0 + resolution: "@babel/runtime-corejs3@npm:7.29.0" dependencies: - core-js-pure: "npm:^3.30.2" - regenerator-runtime: "npm:^0.14.0" - checksum: 10/926147ffd75a22cb005a591a72a341084780adb14698fa5c062dbdf355d18ebaaa7ad45690eef99dcd0dea1ad9e617d8b54cfb7b933cec92275a918c73a42534 + core-js-pure: "npm:^3.48.0" + checksum: 10/59b3b614a8b4c5cf94d1f271f36eb96def0f0be0b6fa93da2ff7a87996bc3806f83690e7517f2e675078ff645e4e953f8bf3c6fc1a59cc5327e45f04bc0dd105 languageName: node linkType: hard -"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.3, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.19.0, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.9, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.26.9 - resolution: "@babel/runtime@npm:7.26.9" - dependencies: - regenerator-runtime: "npm:^0.14.0" - checksum: 10/08edd07d774eafbf157fdc8450ed6ddd22416fdd8e2a53e4a00349daba1b502c03ab7f7ad3ad3a7c46b9a24d99b5697591d0f852ee2f84642082ef7dda90b83d +"@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.3, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.23.2, @babel/runtime@npm:^7.24.7, @babel/runtime@npm:^7.25.9, @babel/runtime@npm:^7.26.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.28.6 + resolution: "@babel/runtime@npm:7.28.6" + checksum: 10/fbcd439cb74d4a681958eb064c509829e3f46d8a4bfaaf441baa81bb6733d1e680bccc676c813883d7741bcaada1d0d04b15aa320ef280b5734e2192b50decf9 languageName: node linkType: hard -"@babel/template@npm:^7.12.7, @babel/template@npm:^7.25.9, @babel/template@npm:^7.28.6, @babel/template@npm:^7.3.3": +"@babel/template@npm:^7.12.7, @babel/template@npm:^7.28.6": version: 7.28.6 resolution: "@babel/template@npm:7.28.6" dependencies: @@ -2053,28 +2158,28 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.5, @babel/traverse@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/traverse@npm:7.26.7" +"@babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.5, @babel/traverse@npm:^7.28.6, @babel/traverse@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/traverse@npm:7.29.0" dependencies: - "@babel/code-frame": "npm:^7.26.2" - "@babel/generator": "npm:^7.26.5" - "@babel/parser": "npm:^7.26.7" - "@babel/template": "npm:^7.25.9" - "@babel/types": "npm:^7.26.7" + "@babel/code-frame": "npm:^7.29.0" + "@babel/generator": "npm:^7.29.0" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/parser": "npm:^7.29.0" + "@babel/template": "npm:^7.28.6" + "@babel/types": "npm:^7.29.0" debug: "npm:^4.3.1" - globals: "npm:^11.1.0" - checksum: 10/c821c9682fe0b9edf7f7cbe9cc3e0787ffee3f73b52c13b21b463f8979950a6433f5e7e482a74348d22c0b7a05180e6f72b23eb6732328b49c59fc6388ebf6e5 + checksum: 10/3a0d0438f1ba9fed4fbe1706ea598a865f9af655a16ca9517ab57bda526e224569ca1b980b473fb68feea5e08deafbbf2cf9febb941f92f2d2533310c3fc4abc languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.7, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.5, @babel/types@npm:^7.26.7, @babel/types@npm:^7.28.6, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": - version: 7.28.6 - resolution: "@babel/types@npm:7.28.6" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.7, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.9, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.5, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.4.4": + version: 7.29.0 + resolution: "@babel/types@npm:7.29.0" dependencies: "@babel/helper-string-parser": "npm:^7.27.1" "@babel/helper-validator-identifier": "npm:^7.28.5" - checksum: 10/f9c6e52b451065aae5654686ecfc7de2d27dd0fbbc204ee2bd912a71daa359521a32f378981b1cf333ace6c8f86928814452cb9f388a7da59ad468038deb6b5f + checksum: 10/bfc2b211210f3894dcd7e6a33b2d1c32c93495dc1e36b547376aa33441abe551ab4bc1640d4154ee2acd8e46d3bbc925c7224caae02fcaf0e6a771e97fccc661 languageName: node linkType: hard @@ -2085,6 +2190,13 @@ __metadata: languageName: node linkType: hard +"@borewit/text-codec@npm:^0.2.1": + version: 0.2.1 + resolution: "@borewit/text-codec@npm:0.2.1" + checksum: 10/3d7e824ac4d3ea16e6e910a7f2bac79f262602c3dbc2f525fd9b86786269c5d7bbd673090a0277d7f92652e534f263e292d5ace080bc9bdf57dc6921c1973f70 + languageName: node + linkType: hard + "@braintree/sanitize-url@npm:^7.1.1": version: 7.1.1 resolution: "@braintree/sanitize-url@npm:7.1.1" @@ -2092,6 +2204,13 @@ __metadata: languageName: node linkType: hard +"@bufbuild/protobuf@npm:^2.5.0": + version: 2.11.0 + resolution: "@bufbuild/protobuf@npm:2.11.0" + checksum: 10/dddab84c2dc92f15b467449dc9d951b9aef6ea335dba448f8d4028f9b52fdb790d3b856a1dceb4dbcfe7f182072f0d1cd6ce05b2a95ff40132eea6a428e84883 + languageName: node + linkType: hard + "@chevrotain/cst-dts-gen@npm:11.0.3": version: 11.0.3 resolution: "@chevrotain/cst-dts-gen@npm:11.0.3" @@ -2148,18 +2267,6 @@ __metadata: languageName: node linkType: hard -"@crello/react-lottie@npm:0.0.9": - version: 0.0.9 - resolution: "@crello/react-lottie@npm:0.0.9" - dependencies: - lottie-web: "npm:5.5.9" - peerDependencies: - react: ~16.9.0 - react-dom: ~16.9.0 - checksum: 10/13e751a5995184f44bc01fee7ca70354164564e4c37e063d363bd262756cdd60f3032f2c4b6bbb91cd5f8ef5fb8e75cd60086a6d253ca0c2737651fc40a93cd6 - languageName: node - linkType: hard - "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -2719,14 +2826,14 @@ __metadata: languageName: node linkType: hard -"@dabh/diagnostics@npm:^2.0.2": - version: 2.0.3 - resolution: "@dabh/diagnostics@npm:2.0.3" +"@dabh/diagnostics@npm:^2.0.8": + version: 2.0.8 + resolution: "@dabh/diagnostics@npm:2.0.8" dependencies: - colorspace: "npm:1.1.x" + "@so-ric/colorspace": "npm:^1.1.6" enabled: "npm:2.0.x" kuler: "npm:^2.0.0" - checksum: 10/14e449a7f42f063f959b472f6ce02d16457a756e852a1910aaa831b63fc21d86f6c32b2a1aa98a4835b856548c926643b51062d241fb6e9b2b7117996053e6b9 + checksum: 10/ac2267a4ee1874f608493f21d386ea29f0acac6716124e26e3e48e01ce5706b095585a14adce1bee14b6567d3b8fdd0c5a0bbb7ab0e15c9a743d55eb02f093ce languageName: node linkType: hard @@ -3413,210 +3520,217 @@ __metadata: languageName: node linkType: hard -"@emnapi/core@npm:^1.1.0": - version: 1.3.1 - resolution: "@emnapi/core@npm:1.3.1" +"@emnapi/core@npm:^1.1.0, @emnapi/core@npm:^1.4.3": + version: 1.8.1 + resolution: "@emnapi/core@npm:1.8.1" dependencies: - "@emnapi/wasi-threads": "npm:1.0.1" + "@emnapi/wasi-threads": "npm:1.1.0" tslib: "npm:^2.4.0" - checksum: 10/00dbc2ae1b9682c3afadb39e0de4e69c7223b06df59b975c2a2ef58d6cbd91f5a7cfd666a97831c958737c5ec110735c6164bf0ac6f56b60477a933bd9ce793c + checksum: 10/904ea60c91fc7d8aeb4a8f2c433b8cfb47c50618f2b6f37429fc5093c857c6381c60628a5cfbc3a7b0d75b0a288f21d4ed2d4533e82f92c043801ef255fd6a5c languageName: node linkType: hard -"@emnapi/runtime@npm:^1.1.0": - version: 1.3.1 - resolution: "@emnapi/runtime@npm:1.3.1" +"@emnapi/runtime@npm:^1.1.0, @emnapi/runtime@npm:^1.4.3": + version: 1.8.1 + resolution: "@emnapi/runtime@npm:1.8.1" dependencies: tslib: "npm:^2.4.0" - checksum: 10/619915ee44682356f77f60455025e667b0b04ad3c95ced36c03782aea9ebc066fa73e86c4a59d221177eba5e5533d40b3a6dbff4e58ee5d81db4270185c21e22 + checksum: 10/26725e202d4baefdc4a6ba770f703dfc80825a27c27a08c22bac1e1ce6f8f75c47b4fe9424d9b63239463c33ef20b650f08d710da18dfa1164a95e5acb865dba languageName: node linkType: hard -"@emnapi/wasi-threads@npm:1.0.1": - version: 1.0.1 - resolution: "@emnapi/wasi-threads@npm:1.0.1" +"@emnapi/wasi-threads@npm:1.1.0": + version: 1.1.0 + resolution: "@emnapi/wasi-threads@npm:1.1.0" dependencies: tslib: "npm:^2.4.0" - checksum: 10/949f8bdcb11153d530652516b11d4b11d8c6ed48a692b4a59cbaa4305327aed59a61f0d87c366085c20ad0b0336c3b50eaddbddeeb3e8c55e7e82b583b9d98fb + checksum: 10/0d557e75262d2f4c95cb2a456ba0785ef61f919ce488c1d76e5e3acfd26e00c753ef928cd80068363e0c166ba8cc0141305daf0f81aad5afcd421f38f11e0f4e languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/aix-ppc64@npm:0.24.2" +"@esbuild/aix-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/aix-ppc64@npm:0.27.2" conditions: os=aix & cpu=ppc64 languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/android-arm64@npm:0.24.2" +"@esbuild/android-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm64@npm:0.27.2" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@esbuild/android-arm@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/android-arm@npm:0.24.2" +"@esbuild/android-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm@npm:0.27.2" conditions: os=android & cpu=arm languageName: node linkType: hard -"@esbuild/android-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/android-x64@npm:0.24.2" +"@esbuild/android-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-x64@npm:0.27.2" conditions: os=android & cpu=x64 languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/darwin-arm64@npm:0.24.2" +"@esbuild/darwin-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-arm64@npm:0.27.2" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/darwin-x64@npm:0.24.2" +"@esbuild/darwin-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-x64@npm:0.27.2" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/freebsd-arm64@npm:0.24.2" +"@esbuild/freebsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-arm64@npm:0.27.2" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/freebsd-x64@npm:0.24.2" +"@esbuild/freebsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-x64@npm:0.27.2" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-arm64@npm:0.24.2" +"@esbuild/linux-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm64@npm:0.27.2" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-arm@npm:0.24.2" +"@esbuild/linux-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm@npm:0.27.2" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-ia32@npm:0.24.2" +"@esbuild/linux-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ia32@npm:0.27.2" conditions: os=linux & cpu=ia32 languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-loong64@npm:0.24.2" +"@esbuild/linux-loong64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-loong64@npm:0.27.2" conditions: os=linux & cpu=loong64 languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-mips64el@npm:0.24.2" +"@esbuild/linux-mips64el@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-mips64el@npm:0.27.2" conditions: os=linux & cpu=mips64el languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-ppc64@npm:0.24.2" +"@esbuild/linux-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ppc64@npm:0.27.2" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-riscv64@npm:0.24.2" +"@esbuild/linux-riscv64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-riscv64@npm:0.27.2" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-s390x@npm:0.24.2" +"@esbuild/linux-s390x@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-s390x@npm:0.27.2" conditions: os=linux & cpu=s390x languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/linux-x64@npm:0.24.2" +"@esbuild/linux-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-x64@npm:0.27.2" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/netbsd-arm64@npm:0.24.2" +"@esbuild/netbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-arm64@npm:0.27.2" conditions: os=netbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/netbsd-x64@npm:0.24.2" +"@esbuild/netbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-x64@npm:0.27.2" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/openbsd-arm64@npm:0.24.2" +"@esbuild/openbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-arm64@npm:0.27.2" conditions: os=openbsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/openbsd-x64@npm:0.24.2" +"@esbuild/openbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-x64@npm:0.27.2" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/sunos-x64@npm:0.24.2" +"@esbuild/openharmony-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openharmony-arm64@npm:0.27.2" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/sunos-x64@npm:0.27.2" conditions: os=sunos & cpu=x64 languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/win32-arm64@npm:0.24.2" +"@esbuild/win32-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-arm64@npm:0.27.2" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/win32-ia32@npm:0.24.2" +"@esbuild/win32-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-ia32@npm:0.27.2" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.24.2": - version: 0.24.2 - resolution: "@esbuild/win32-x64@npm:0.24.2" +"@esbuild/win32-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-x64@npm:0.27.2" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.4.0, @eslint-community/eslint-utils@npm:^4.4.1, @eslint-community/eslint-utils@npm:^4.8.0": +"@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.5.0, @eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": version: 4.9.1 resolution: "@eslint-community/eslint-utils@npm:4.9.1" dependencies: @@ -3627,10 +3741,10 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.11.0, @eslint-community/regexpp@npm:^4.12.1": - version: 4.12.1 - resolution: "@eslint-community/regexpp@npm:4.12.1" - checksum: 10/c08f1dd7dd18fbb60bdd0d85820656d1374dd898af9be7f82cb00451313402a22d5e30569c150315b4385907cdbca78c22389b2a72ab78883b3173be317620cc +"@eslint-community/regexpp@npm:^4.11.0, @eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 10/049b280fddf71dd325514e0a520024969431dc3a8b02fa77476e6820e9122f28ab4c9168c11821f91a27982d2453bcd7a66193356ea84e84fb7c8d793be1ba0c languageName: node linkType: hard @@ -3663,6 +3777,15 @@ __metadata: languageName: node linkType: hard +"@eslint/core@npm:^1.0.1, @eslint/core@npm:^1.1.0": + version: 1.1.0 + resolution: "@eslint/core@npm:1.1.0" + dependencies: + "@types/json-schema": "npm:^7.0.15" + checksum: 10/f62724beacbb5fdd3560816a4edbbf832485cbec9516b76037fdf2cc2d75011e546e305a22feaa6bed4c1a26d069dc953979aa3c8c28eccf0a746a5ac53483b0 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^3.3.1": version: 3.3.3 resolution: "@eslint/eslintrc@npm:3.3.3" @@ -3704,47 +3827,107 @@ __metadata: languageName: node linkType: hard -"@fortawesome/fontawesome-common-types@npm:6.7.2": - version: 6.7.2 - resolution: "@fortawesome/fontawesome-common-types@npm:6.7.2" - checksum: 10/3c2e938afe6f5939bd63181faaec7b062902d9ed970c75d6becb1fd8e5ca0ed937e7d1513bd7ae545da407d0682039e50730cdb3136b58656128838ea2c58ac0 +"@eslint/plugin-kit@npm:^0.6.0": + version: 0.6.0 + resolution: "@eslint/plugin-kit@npm:0.6.0" + dependencies: + "@eslint/core": "npm:^1.1.0" + levn: "npm:^0.4.1" + checksum: 10/9c4e2901248ce092674b939fce9104a81d16222ed23c1b6057a5aaea8ec2fa802bcc9fbaf667e54a4206aca21f70c8b943ee14e6010530a46a4fcb64c41537b1 languageName: node linkType: hard -"@fortawesome/fontawesome-free@npm:^6.7.2": - version: 6.7.2 - resolution: "@fortawesome/fontawesome-free@npm:6.7.2" - checksum: 10/88101fee12470ede1e7f2588b86121924259d98889b950e2ccde71d934f4f344b592d1360700de4e92d81262014d3ae33fe995c7799f2be2abddcee3102413d6 +"@floating-ui/core@npm:^1.7.4": + version: 1.7.4 + resolution: "@floating-ui/core@npm:1.7.4" + dependencies: + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10/b750f306a99be879f0bce879108c440d5b0a67303d3d8318e153687f6ed1af27908428e27cc955475253bd902b95452a3434bd4f0cf96e66e5b5d0db1aa8ea3c languageName: node linkType: hard -"@fortawesome/fontawesome-svg-core@npm:^6.7.2": - version: 6.7.2 - resolution: "@fortawesome/fontawesome-svg-core@npm:6.7.2" +"@floating-ui/dom@npm:^1.7.5": + version: 1.7.5 + resolution: "@floating-ui/dom@npm:1.7.5" dependencies: - "@fortawesome/fontawesome-common-types": "npm:6.7.2" - checksum: 10/a3767631329aaa8c1bfafc9470718628533ceb42365774cd0c121477e0f3125f3cce4c2447058deee2874829ce11aa7a3fe183b0000bad81cf5ed0449c8470ef + "@floating-ui/core": "npm:^1.7.4" + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10/2764990da82bd5cfe942211480aa82352926326008de93f5f3f19749cc8b171fe05b77526a2652605eadcbeab902c6506f18d60a4c43281f2651802047de100b languageName: node linkType: hard -"@fortawesome/free-solid-svg-icons@npm:^6.7.2": - version: 6.7.2 - resolution: "@fortawesome/free-solid-svg-icons@npm:6.7.2" +"@floating-ui/react-dom@npm:^2.1.7": + version: 2.1.7 + resolution: "@floating-ui/react-dom@npm:2.1.7" dependencies: - "@fortawesome/fontawesome-common-types": "npm:6.7.2" - checksum: 10/efcd90cd5d333995ff4012a9d77a8b23523e246fa418524edf08bb6af8b14db2ee0b08ee5f7460a86474d352af06e1a2581cc827ee3706e9c0e92e178b50e27f + "@floating-ui/dom": "npm:^1.7.5" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10/870eb2109af3ab09ea0076eb8e0ad307da274978c3dfe28e83422136a7f85cac700f62c37663c75bc25b174d33457c0224d1944e80b9a7ca5ff7b28b8f77b7ab languageName: node linkType: hard -"@fortawesome/react-fontawesome@npm:^0.2.2": - version: 0.2.2 - resolution: "@fortawesome/react-fontawesome@npm:0.2.2" +"@floating-ui/react@npm:^0.27.15": + version: 0.27.17 + resolution: "@floating-ui/react@npm:0.27.17" dependencies: - prop-types: "npm:^15.8.1" + "@floating-ui/react-dom": "npm:^2.1.7" + "@floating-ui/utils": "npm:^0.2.10" + tabbable: "npm:^6.0.0" + peerDependencies: + react: ">=17.0.0" + react-dom: ">=17.0.0" + checksum: 10/467fac66e149fb8e779ad18124f1dd610ac61987ec70bb3a3eeb384045f583641f363e577e92ae93cfa804d5d47d53b3f8be28d8f758b0972a2aa153d660d4eb + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.2.10": + version: 0.2.10 + resolution: "@floating-ui/utils@npm:0.2.10" + checksum: 10/b635ea865a8be2484b608b7157f5abf9ed439f351011a74b7e988439e2898199a9a8b790f52291e05bdcf119088160dc782d98cff45cc98c5a271bc6f51327ae + languageName: node + linkType: hard + +"@fortawesome/fontawesome-common-types@npm:7.1.0": + version: 7.1.0 + resolution: "@fortawesome/fontawesome-common-types@npm:7.1.0" + checksum: 10/cf13595f54a7cb3e1e123032a2b34b939bacac300db25faa972da391b1bba7129beb1f929d6866a4b2aa28ac75b20a10a2b1a04e977c0948eceb5ff4c888663e + languageName: node + linkType: hard + +"@fortawesome/fontawesome-free@npm:^7.1.0": + version: 7.1.0 + resolution: "@fortawesome/fontawesome-free@npm:7.1.0" + checksum: 10/7a7bbb470b2de4e2764b4f9cc91703c5e1d5f430c834353035ef7aecba278cc9b5e2917ddaa7f1c28445530216543b3aa37598f56bb16576395fa060cbb33323 + languageName: node + linkType: hard + +"@fortawesome/fontawesome-svg-core@npm:^7.1.0": + version: 7.1.0 + resolution: "@fortawesome/fontawesome-svg-core@npm:7.1.0" + dependencies: + "@fortawesome/fontawesome-common-types": "npm:7.1.0" + checksum: 10/a6137c120d2009df445e72e07e1b9fba923847fc7a3d2429a216fb09ee444aaf7904d9e89c90a1aea47876381437041e42837bb2c4f5a1c758f1424218157bb8 + languageName: node + linkType: hard + +"@fortawesome/free-solid-svg-icons@npm:^7.1.0": + version: 7.1.0 + resolution: "@fortawesome/free-solid-svg-icons@npm:7.1.0" + dependencies: + "@fortawesome/fontawesome-common-types": "npm:7.1.0" + checksum: 10/302548ff3fd45272eb927c87c26d555cebfecdbd0744177f24a47b1743eb73f10c6e8017fc9e2eab560851e4a396a2a2be07b1e75e8c7fe7215c1d7629133f02 + languageName: node + linkType: hard + +"@fortawesome/react-fontawesome@npm:^3.1.1": + version: 3.1.1 + resolution: "@fortawesome/react-fontawesome@npm:3.1.1" peerDependencies: - "@fortawesome/fontawesome-svg-core": ~1 || ~6 - react: ">=16.3" - checksum: 10/05537fd7c34d43e0d8823df0195cb6fd935ff78e296e2d362e668bcf75f13d71c70c7fd6d596dff4e37b5f27e0ae43b98cb4732e0d91570f30b8a5581bbe2704 + "@fortawesome/fontawesome-svg-core": ~6 || ~7 + react: ^18.0.0 || ^19.0.0 + checksum: 10/23f39153719933b58010d52b1c3bc91bf1bb4b925a6c62f97c94fb08b33dba18f1203aabca287b14bf6f35b78bfb3264ff29ae9e79b35e4ae5390a15ea02d0a7 languageName: node linkType: hard @@ -3948,19 +4131,6 @@ __metadata: languageName: node linkType: hard -"@hypnosphi/create-react-context@npm:^0.3.1": - version: 0.3.1 - resolution: "@hypnosphi/create-react-context@npm:0.3.1" - dependencies: - gud: "npm:^1.0.0" - warning: "npm:^4.0.3" - peerDependencies: - prop-types: ^15.0.0 - react: ">=0.14.0" - checksum: 10/79b697d150f9b4aa6cadfb8026f20e023c05fefc4be841b1cdd5567c3fd970ccaae84a0ea6279f579fe2cc9844c201e80713a2691b24e59cc7d6925fa8130c34 - languageName: node - linkType: hard - "@iconify/types@npm:^2.0.0": version: 2.0.0 resolution: "@iconify/types@npm:2.0.0" @@ -4077,6 +4247,21 @@ __metadata: languageName: node linkType: hard +"@inquirer/external-editor@npm:^1.0.0": + version: 1.0.3 + resolution: "@inquirer/external-editor@npm:1.0.3" + dependencies: + chardet: "npm:^2.1.1" + iconv-lite: "npm:^0.7.0" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/c95d7237a885b32031715089f92820525731d4d3c2bd7afdb826307dc296cc2b39e7a644b0bb265441963348cca42e7785feb29c3aaf18fd2b63131769bf6587 + languageName: node + linkType: hard + "@inquirer/external-editor@npm:^1.0.2": version: 1.0.2 resolution: "@inquirer/external-editor@npm:1.0.2" @@ -4239,11 +4424,11 @@ __metadata: linkType: hard "@isaacs/brace-expansion@npm:^5.0.0": - version: 5.0.0 - resolution: "@isaacs/brace-expansion@npm:5.0.0" + version: 5.0.1 + resolution: "@isaacs/brace-expansion@npm:5.0.1" dependencies: "@isaacs/balanced-match": "npm:^4.0.1" - checksum: 10/cf3b7f206aff12128214a1df764ac8cdbc517c110db85249b945282407e3dfc5c6e66286383a7c9391a059fc8e6e6a8ca82262fc9d2590bd615376141fbebd2d + checksum: 10/aec226065bc4285436a27379e08cc35bf94ef59f5098ac1c026495c9ba4ab33d851964082d3648d56d63eb90f2642867bd15a3e1b810b98beb1a8c14efce6a94 languageName: node linkType: hard @@ -4290,65 +4475,65 @@ __metadata: languageName: node linkType: hard -"@istanbuljs/schema@npm:^0.1.2": +"@istanbuljs/schema@npm:^0.1.2, @istanbuljs/schema@npm:^0.1.3": version: 0.1.3 resolution: "@istanbuljs/schema@npm:0.1.3" checksum: 10/a9b1e49acdf5efc2f5b2359f2df7f90c5c725f2656f16099e8b2cd3a000619ecca9fc48cf693ba789cf0fd989f6e0df6a22bc05574be4223ecdbb7997d04384b languageName: node linkType: hard -"@jest/console@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/console@npm:29.7.0" +"@jest/console@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/console@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - chalk: "npm:^4.0.0" - jest-message-util: "npm:^29.7.0" - jest-util: "npm:^29.7.0" + chalk: "npm:^4.1.2" + jest-message-util: "npm:30.2.0" + jest-util: "npm:30.2.0" slash: "npm:^3.0.0" - checksum: 10/4a80c750e8a31f344233cb9951dee9b77bf6b89377cb131f8b3cde07ff218f504370133a5963f6a786af4d2ce7f85642db206ff7a15f99fe58df4c38ac04899e + checksum: 10/7cda9793962afa5c7fcfdde0ff5012694683b17941ee3c6a55ea9fd9a02f1c51ec4b4c767b867e1226f85a26af1d0f0d72c6a344e34c5bc4300312ebffd6e50b languageName: node linkType: hard -"@jest/core@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/core@npm:29.7.0" - dependencies: - "@jest/console": "npm:^29.7.0" - "@jest/reporters": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" +"@jest/core@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/core@npm:30.2.0" + dependencies: + "@jest/console": "npm:30.2.0" + "@jest/pattern": "npm:30.0.1" + "@jest/reporters": "npm:30.2.0" + "@jest/test-result": "npm:30.2.0" + "@jest/transform": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - ansi-escapes: "npm:^4.2.1" - chalk: "npm:^4.0.0" - ci-info: "npm:^3.2.0" - exit: "npm:^0.1.2" - graceful-fs: "npm:^4.2.9" - jest-changed-files: "npm:^29.7.0" - jest-config: "npm:^29.7.0" - jest-haste-map: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-regex-util: "npm:^29.6.3" - jest-resolve: "npm:^29.7.0" - jest-resolve-dependencies: "npm:^29.7.0" - jest-runner: "npm:^29.7.0" - jest-runtime: "npm:^29.7.0" - jest-snapshot: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-validate: "npm:^29.7.0" - jest-watcher: "npm:^29.7.0" - micromatch: "npm:^4.0.4" - pretty-format: "npm:^29.7.0" + ansi-escapes: "npm:^4.3.2" + chalk: "npm:^4.1.2" + ci-info: "npm:^4.2.0" + exit-x: "npm:^0.2.2" + graceful-fs: "npm:^4.2.11" + jest-changed-files: "npm:30.2.0" + jest-config: "npm:30.2.0" + jest-haste-map: "npm:30.2.0" + jest-message-util: "npm:30.2.0" + jest-regex-util: "npm:30.0.1" + jest-resolve: "npm:30.2.0" + jest-resolve-dependencies: "npm:30.2.0" + jest-runner: "npm:30.2.0" + jest-runtime: "npm:30.2.0" + jest-snapshot: "npm:30.2.0" + jest-util: "npm:30.2.0" + jest-validate: "npm:30.2.0" + jest-watcher: "npm:30.2.0" + micromatch: "npm:^4.0.8" + pretty-format: "npm:30.2.0" slash: "npm:^3.0.0" - strip-ansi: "npm:^6.0.0" peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true - checksum: 10/ab6ac2e562d083faac7d8152ec1cc4eccc80f62e9579b69ed40aedf7211a6b2d57024a6cd53c4e35fd051c39a236e86257d1d99ebdb122291969a0a04563b51e + checksum: 10/6763bb1efd937778f009821cd94c3705d3c31a156258a224b8745c1e0887976683f5413745ffb361b526f0fa2692e36aaa963aa197cc77ba932cff9d6d28af9d languageName: node linkType: hard @@ -4359,48 +4544,69 @@ __metadata: languageName: node linkType: hard -"@jest/environment@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/environment@npm:29.7.0" +"@jest/environment-jsdom-abstract@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/environment-jsdom-abstract@npm:30.2.0" dependencies: - "@jest/fake-timers": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" + "@jest/environment": "npm:30.2.0" + "@jest/fake-timers": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + "@types/jsdom": "npm:^21.1.7" "@types/node": "npm:*" - jest-mock: "npm:^29.7.0" - checksum: 10/90b5844a9a9d8097f2cf107b1b5e57007c552f64315da8c1f51217eeb0a9664889d3f145cdf8acf23a84f4d8309a6675e27d5b059659a004db0ea9546d1c81a8 + jest-mock: "npm:30.2.0" + jest-util: "npm:30.2.0" + peerDependencies: + canvas: ^3.0.0 + jsdom: "*" + peerDependenciesMeta: + canvas: + optional: true + checksum: 10/65a9c8504f213f4d125956383ffe6c4e566cfb0ff2fe67783adf9ebde33f772339e61fdd98ddc2bbae3029e3356d2386abedb9d101aa95d6fd51fabac38bebe0 languageName: node linkType: hard -"@jest/expect-utils@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/expect-utils@npm:29.7.0" +"@jest/environment@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/environment@npm:30.2.0" dependencies: - jest-get-type: "npm:^29.6.3" - checksum: 10/ef8d379778ef574a17bde2801a6f4469f8022a46a5f9e385191dc73bb1fc318996beaed4513fbd7055c2847227a1bed2469977821866534593a6e52a281499ee + "@jest/fake-timers": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + "@types/node": "npm:*" + jest-mock: "npm:30.2.0" + checksum: 10/e168a4ff328980eb9fde5e43aea80807fd0b2dbd4579ae8f68a03415a1e58adf5661db298054fa2351c7cb2b5a74bf67b8ab996656cf5927d0b0d0b6e2c2966b languageName: node linkType: hard -"@jest/expect@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/expect@npm:29.7.0" +"@jest/expect-utils@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/expect-utils@npm:30.2.0" dependencies: - expect: "npm:^29.7.0" - jest-snapshot: "npm:^29.7.0" - checksum: 10/fea6c3317a8da5c840429d90bfe49d928e89c9e89fceee2149b93a11b7e9c73d2f6e4d7cdf647163da938fc4e2169e4490be6bae64952902bc7a701033fd4880 + "@jest/get-type": "npm:30.1.0" + checksum: 10/f2442f1bceb3411240d0f16fd0074377211b4373d3b8b2dc28929e861b6527a6deb403a362c25afa511d933cda4dfbdc98d4a08eeb51ee4968f7cb0299562349 languageName: node linkType: hard -"@jest/fake-timers@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/fake-timers@npm:29.7.0" +"@jest/expect@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/expect@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" - "@sinonjs/fake-timers": "npm:^10.0.2" + expect: "npm:30.2.0" + jest-snapshot: "npm:30.2.0" + checksum: 10/d950d95a64d5c6a39d56171dabb8dbe59423096231bb4f21d8ee0019878e6626701ac9d782803dc2589e2799ed39704031f818533f8a3e571b57032eafa85d12 + languageName: node + linkType: hard + +"@jest/fake-timers@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/fake-timers@npm:30.2.0" + dependencies: + "@jest/types": "npm:30.2.0" + "@sinonjs/fake-timers": "npm:^13.0.0" "@types/node": "npm:*" - jest-message-util: "npm:^29.7.0" - jest-mock: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - checksum: 10/9b394e04ffc46f91725ecfdff34c4e043eb7a16e1d78964094c9db3fde0b1c8803e45943a980e8c740d0a3d45661906de1416ca5891a538b0660481a3a828c27 + jest-message-util: "npm:30.2.0" + jest-mock: "npm:30.2.0" + jest-util: "npm:30.2.0" + checksum: 10/c2df66576ba8049b07d5f239777243e21fcdaa09a446be1e55fac709d6273e2a926c1562e0372c3013142557ed9d386381624023549267a667b6e1b656e37fe6 languageName: node linkType: hard @@ -4411,52 +4617,61 @@ __metadata: languageName: node linkType: hard -"@jest/globals@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/globals@npm:29.7.0" +"@jest/globals@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/globals@npm:30.2.0" dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/expect": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - jest-mock: "npm:^29.7.0" - checksum: 10/97dbb9459135693ad3a422e65ca1c250f03d82b2a77f6207e7fa0edd2c9d2015fbe4346f3dc9ebff1678b9d8da74754d4d440b7837497f8927059c0642a22123 + "@jest/environment": "npm:30.2.0" + "@jest/expect": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + jest-mock: "npm:30.2.0" + checksum: 10/d4a331d3847cebb3acefe120350d8a6bb5517c1403de7cd2b4dc67be425f37ba0511beee77d6837b4da2d93a25a06d6f829ad7837da365fae45e1da57523525c languageName: node linkType: hard -"@jest/reporters@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/reporters@npm:29.7.0" +"@jest/pattern@npm:30.0.1": + version: 30.0.1 + resolution: "@jest/pattern@npm:30.0.1" + dependencies: + "@types/node": "npm:*" + jest-regex-util: "npm:30.0.1" + checksum: 10/afd03b4d3eadc9c9970cf924955dee47984a7e767901fe6fa463b17b246f0ddeec07b3e82c09715c54bde3c8abb92074160c0d79967bd23778724f184e7f5b7b + languageName: node + linkType: hard + +"@jest/reporters@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/reporters@npm:30.2.0" dependencies: "@bcoe/v8-coverage": "npm:^0.2.3" - "@jest/console": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@jridgewell/trace-mapping": "npm:^0.3.18" + "@jest/console": "npm:30.2.0" + "@jest/test-result": "npm:30.2.0" + "@jest/transform": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + "@jridgewell/trace-mapping": "npm:^0.3.25" "@types/node": "npm:*" - chalk: "npm:^4.0.0" - collect-v8-coverage: "npm:^1.0.0" - exit: "npm:^0.1.2" - glob: "npm:^7.1.3" - graceful-fs: "npm:^4.2.9" + chalk: "npm:^4.1.2" + collect-v8-coverage: "npm:^1.0.2" + exit-x: "npm:^0.2.2" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.11" istanbul-lib-coverage: "npm:^3.0.0" istanbul-lib-instrument: "npm:^6.0.0" istanbul-lib-report: "npm:^3.0.0" - istanbul-lib-source-maps: "npm:^4.0.0" + istanbul-lib-source-maps: "npm:^5.0.0" istanbul-reports: "npm:^3.1.3" - jest-message-util: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-worker: "npm:^29.7.0" + jest-message-util: "npm:30.2.0" + jest-util: "npm:30.2.0" + jest-worker: "npm:30.2.0" slash: "npm:^3.0.0" - string-length: "npm:^4.0.1" - strip-ansi: "npm:^6.0.0" + string-length: "npm:^4.0.2" v8-to-istanbul: "npm:^9.0.1" peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true - checksum: 10/a17d1644b26dea14445cedd45567f4ba7834f980be2ef74447204e14238f121b50d8b858fde648083d2cd8f305f81ba434ba49e37a5f4237a6f2a61180cc73dc + checksum: 10/3848b59bf740c10c4e5c234dcc41c54adbd74932bf05d1d1582d09d86e9baa86ddaf3c43903505fd042ba1203c2889a732137d08058ce9dc0069ba33b5d5373d languageName: node linkType: hard @@ -4478,61 +4693,88 @@ __metadata: languageName: node linkType: hard -"@jest/source-map@npm:^29.6.3": - version: 29.6.3 - resolution: "@jest/source-map@npm:29.6.3" +"@jest/snapshot-utils@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/snapshot-utils@npm:30.2.0" dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.18" - callsites: "npm:^3.0.0" - graceful-fs: "npm:^4.2.9" - checksum: 10/bcc5a8697d471396c0003b0bfa09722c3cd879ad697eb9c431e6164e2ea7008238a01a07193dfe3cbb48b1d258eb7251f6efcea36f64e1ebc464ea3c03ae2deb + "@jest/types": "npm:30.2.0" + chalk: "npm:^4.1.2" + graceful-fs: "npm:^4.2.11" + natural-compare: "npm:^1.4.0" + checksum: 10/6b30ab2b0682117e3ce775e70b5be1eb01e1ea53a74f12ac7090cd1a5f37e9b795cd8de83853afa7b4b799c96b1c482499aa993ca2034ea0679525d32b7f9625 languageName: node linkType: hard -"@jest/test-result@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/test-result@npm:29.7.0" +"@jest/source-map@npm:30.0.1": + version: 30.0.1 + resolution: "@jest/source-map@npm:30.0.1" dependencies: - "@jest/console": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@types/istanbul-lib-coverage": "npm:^2.0.0" - collect-v8-coverage: "npm:^1.0.0" - checksum: 10/c073ab7dfe3c562bff2b8fee6cc724ccc20aa96bcd8ab48ccb2aa309b4c0c1923a9e703cea386bd6ae9b71133e92810475bb9c7c22328fc63f797ad3324ed189 + "@jridgewell/trace-mapping": "npm:^0.3.25" + callsites: "npm:^3.1.0" + graceful-fs: "npm:^4.2.11" + checksum: 10/161b27cdf8d9d80fd99374d55222b90478864c6990514be6ebee72b7184a034224c9aceed12c476f3a48d48601bf8ed2e0c047a5a81bd907dc192ebe71365ed4 languageName: node linkType: hard -"@jest/test-sequencer@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/test-sequencer@npm:29.7.0" +"@jest/test-result@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/test-result@npm:30.2.0" dependencies: - "@jest/test-result": "npm:^29.7.0" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.7.0" + "@jest/console": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + "@types/istanbul-lib-coverage": "npm:^2.0.6" + collect-v8-coverage: "npm:^1.0.2" + checksum: 10/f58f79c3c3ba6dd15325e05b0b5a300777cd8cc38327f622608b6fe849b1073ee9633e33d1e5d7ef5b97a1ce71543d0ad92674b7a279f53033143e8dd7c22959 + languageName: node + linkType: hard + +"@jest/test-sequencer@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/test-sequencer@npm:30.2.0" + dependencies: + "@jest/test-result": "npm:30.2.0" + graceful-fs: "npm:^4.2.11" + jest-haste-map: "npm:30.2.0" slash: "npm:^3.0.0" - checksum: 10/4420c26a0baa7035c5419b0892ff8ffe9a41b1583ec54a10db3037cd46a7e29dd3d7202f8aa9d376e9e53be5f8b1bc0d16e1de6880a6d319b033b01dc4c8f639 + checksum: 10/7923964b27048b2233858b32aa1b34d4dd9e404311626d944a706bcdcaa0b1585f43f2ffa3fa893ecbf133566f31ba2b79ab5eaaaf674b8558c6c7029ecbea5e languageName: node linkType: hard -"@jest/transform@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/transform@npm:29.7.0" +"@jest/transform@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/transform@npm:30.2.0" dependencies: - "@babel/core": "npm:^7.11.6" - "@jest/types": "npm:^29.6.3" - "@jridgewell/trace-mapping": "npm:^0.3.18" - babel-plugin-istanbul: "npm:^6.1.1" - chalk: "npm:^4.0.0" + "@babel/core": "npm:^7.27.4" + "@jest/types": "npm:30.2.0" + "@jridgewell/trace-mapping": "npm:^0.3.25" + babel-plugin-istanbul: "npm:^7.0.1" + chalk: "npm:^4.1.2" convert-source-map: "npm:^2.0.0" fast-json-stable-stringify: "npm:^2.1.0" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.7.0" - jest-regex-util: "npm:^29.6.3" - jest-util: "npm:^29.7.0" - micromatch: "npm:^4.0.4" - pirates: "npm:^4.0.4" + graceful-fs: "npm:^4.2.11" + jest-haste-map: "npm:30.2.0" + jest-regex-util: "npm:30.0.1" + jest-util: "npm:30.2.0" + micromatch: "npm:^4.0.8" + pirates: "npm:^4.0.7" slash: "npm:^3.0.0" - write-file-atomic: "npm:^4.0.2" - checksum: 10/30f42293545ab037d5799c81d3e12515790bb58513d37f788ce32d53326d0d72ebf5b40f989e6896739aa50a5f77be44686e510966370d58511d5ad2637c68c1 + write-file-atomic: "npm:^5.0.1" + checksum: 10/c75d72d524c2a50ea6c05778a9b76a6e48bc228a3390896a6edd4416f7b4954ee0a07e229ed7b4949ce8889324b70034c784751e3fc455a25648bd8dcad17d0d + languageName: node + linkType: hard + +"@jest/types@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/types@npm:30.2.0" + dependencies: + "@jest/pattern": "npm:30.0.1" + "@jest/schemas": "npm:30.0.5" + "@types/istanbul-lib-coverage": "npm:^2.0.6" + "@types/istanbul-reports": "npm:^3.0.4" + "@types/node": "npm:*" + "@types/yargs": "npm:^17.0.33" + chalk: "npm:^4.1.2" + checksum: 10/f50fcaea56f873a51d19254ab16762f2ea8ca88e3e08da2e496af5da2b67c322915a4fcd0153803cc05063ffe87ebef2ab4330e0a1b06ab984a26c916cbfc26b languageName: node linkType: hard @@ -4561,6 +4803,26 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.3.12": + version: 0.3.13 + resolution: "@jridgewell/gen-mapping@npm:0.3.13" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.0" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10/902f8261dcf450b4af7b93f9656918e02eec80a2169e155000cb2059f90113dd98f3ccf6efc6072cee1dd84cac48cade51da236972d942babc40e4c23da4d62a + languageName: node + linkType: hard + +"@jridgewell/remapping@npm:^2.3.5": + version: 2.3.5 + resolution: "@jridgewell/remapping@npm:2.3.5" + dependencies: + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.24" + checksum: 10/c2bb01856e65b506d439455f28aceacf130d6c023d1d4e3b48705e88def3571753e1a887daa04b078b562316c92d26ce36408a60534bceca3f830aec88a339ad + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.1 resolution: "@jridgewell/resolve-uri@npm:3.1.1" @@ -4585,10 +4847,10 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15": - version: 1.5.0 - resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" - checksum: 10/4ed6123217569a1484419ac53f6ea0d9f3b57e5b57ab30d7c267bdb27792a27eb0e4b08e84a2680aa55cc2f2b411ffd6ec3db01c44fdc6dc43aca4b55f8374fd +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.4.15, @jridgewell/sourcemap-codec@npm:^1.5.0": + version: 1.5.5 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" + checksum: 10/5d9d207b462c11e322d71911e55e21a4e2772f71ffe8d6f1221b8eb5ae6774458c1d242f897fb0814e8714ca9a6b498abfa74dfe4f434493342902b1a48b33a5 languageName: node linkType: hard @@ -4602,13 +4864,13 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.20, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.25 - resolution: "@jridgewell/trace-mapping@npm:0.3.25" +"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.23, @jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25, @jridgewell/trace-mapping@npm:^0.3.28, @jridgewell/trace-mapping@npm:^0.3.9": + version: 0.3.31 + resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: "@jridgewell/resolve-uri": "npm:^3.1.0" "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10/dced32160a44b49d531b80a4a2159dceab6b3ddf0c8e95a0deae4b0e894b172defa63d5ac52a19c2068e1fe7d31ea4ba931fbeec103233ecb4208953967120fc + checksum: 10/da0283270e691bdb5543806077548532791608e52386cfbbf3b9e8fb00457859d1bd01d512851161c886eb3a2f3ce6fd9bcf25db8edf3bddedd275bd4a88d606 languageName: node linkType: hard @@ -4715,9 +4977,9 @@ __metadata: languageName: node linkType: hard -"@jstarpl/react-contextmenu@npm:^2.15.1": - version: 2.15.1 - resolution: "@jstarpl/react-contextmenu@npm:2.15.1" +"@jstarpl/react-contextmenu@npm:^2.15.3": + version: 2.15.3 + resolution: "@jstarpl/react-contextmenu@npm:2.15.3" dependencies: classnames: "npm:^2.2.5" object-assign: "npm:^4.1.0" @@ -4725,7 +4987,7 @@ __metadata: prop-types: ^15.0.0 react: ^0.14.0 || ^15.0.0 || ^16.0.1 || ^17 || ^18 react-dom: ^0.14.0 || ^15.0.0 || ^16.0.1 || ^17 || ^18 - checksum: 10/b0195bf013fdc325f2cca4eb263d4da8f805ae52b827f52af39e8d6b913d7f6d4eaba4461efd0dd4395f78ad342ed366a50b85f5ca341917db32045bbb028804 + checksum: 10/112adfddf06b38679a4b16a4523e18066cbe29ee23cd343287e002976a125a972127b8fd26c44acb226333a5e43a86ee72cfa754384e0327f3d1358dd5d802a7 languageName: node linkType: hard @@ -4747,15 +5009,20 @@ __metadata: languageName: node linkType: hard -"@koa/router@npm:^14.0.0": - version: 14.0.0 - resolution: "@koa/router@npm:14.0.0" +"@koa/router@npm:^15.3.0": + version: 15.3.0 + resolution: "@koa/router@npm:15.3.0" dependencies: - debug: "npm:^4.4.1" - http-errors: "npm:^2.0.0" + debug: "npm:^4.4.3" + http-errors: "npm:^2.0.1" koa-compose: "npm:^4.1.0" - path-to-regexp: "npm:^8.2.0" - checksum: 10/f5f9bedd4c163ad376bcf9626ebb13f35febc44c1f81545ee5efaceb67324e3caf476f9d2a966b4590cac41ab9994b1bcb11f050afbdccd6343f27f31758ff68 + path-to-regexp: "npm:^8.3.0" + peerDependencies: + koa: ^2.0.0 || ^3.0.0 + peerDependenciesMeta: + koa: + optional: false + checksum: 10/5f2679916514c28a1694ec3c0eac1b869c21d5527a4fdc4b248b3f4c595010389973401eadf6ff3f5c26d24ee84a391325880de51320f1965f917a21120d2899 languageName: node linkType: hard @@ -4782,13 +5049,13 @@ __metadata: languageName: node linkType: hard -"@lerna/create@npm:9.0.3": - version: 9.0.3 - resolution: "@lerna/create@npm:9.0.3" +"@lerna/create@npm:9.0.5": + version: 9.0.5 + resolution: "@lerna/create@npm:9.0.5" dependencies: "@npmcli/arborist": "npm:9.1.6" "@npmcli/package-json": "npm:7.0.2" - "@npmcli/run-script": "npm:10.0.2" + "@npmcli/run-script": "npm:10.0.3" "@nx/devkit": "npm:>=21.5.2 < 23.0.0" "@octokit/plugin-enterprise-rest": "npm:6.0.1" "@octokit/rest": "npm:20.1.2" @@ -4819,7 +5086,7 @@ __metadata: load-json-file: "npm:6.2.0" make-dir: "npm:4.0.0" make-fetch-happen: "npm:15.0.2" - minimatch: "npm:3.0.5" + minimatch: "npm:3.1.4" multimatch: "npm:5.0.0" npm-package-arg: "npm:13.0.1" npm-packlist: "npm:10.0.3" @@ -4833,14 +5100,14 @@ __metadata: pify: "npm:5.0.0" read-cmd-shim: "npm:4.0.0" resolve-from: "npm:5.0.0" - rimraf: "npm:^4.4.1" + rimraf: "npm:^6.1.2" semver: "npm:7.7.2" set-blocking: "npm:^2.0.0" signal-exit: "npm:3.0.7" slash: "npm:^3.0.0" ssri: "npm:12.0.0" string-width: "npm:^4.2.3" - tar: "npm:6.2.1" + tar: "npm:7.5.8" temp-dir: "npm:1.0.0" through: "npm:2.3.8" tinyglobby: "npm:0.2.12" @@ -4853,7 +5120,7 @@ __metadata: write-pkg: "npm:4.0.0" yargs: "npm:17.7.2" yargs-parser: "npm:21.1.1" - checksum: 10/830388fca001128b2715d87c8744f4579123438f3de2708cc804bce84256687906e1cb5a9ddac0b15fb058931b0998cd3fae60fc29502875fd6c700d94e4b21a + checksum: 10/199ad62a77387385db3d0b15314b00e741779be08a487847743d32de45113e04d8ba0788aa4f5cc97f5ae3864a9d319cbf134d40d0835649093903450f3f43de languageName: node linkType: hard @@ -4916,12 +5183,12 @@ __metadata: languageName: node linkType: hard -"@mongodb-js/saslprep@npm:^1.1.9": - version: 1.1.9 - resolution: "@mongodb-js/saslprep@npm:1.1.9" +"@mongodb-js/saslprep@npm:^1.3.0": + version: 1.4.5 + resolution: "@mongodb-js/saslprep@npm:1.4.5" dependencies: sparse-bitfield: "npm:^3.0.3" - checksum: 10/6a0d5e9068635fff59815de387d71be0e3b9d683f1d299876b2760ac18bbf0a1d4b26eff6b1ab89ff8802c20ffb15c047ba675b2cc306a51077a013286c2694a + checksum: 10/40cde05e68d5ab243b1db7196b86b91c1de099a451c73fe2faa4ba3f220009f0e829a150a716de991a764068fd12f5d9303ae7d05ab3c9973d39c5588a67ebf7 languageName: node linkType: hard @@ -4978,24 +5245,35 @@ __metadata: languageName: node linkType: hard -"@nestjs/axios@npm:4.0.0": - version: 4.0.0 - resolution: "@nestjs/axios@npm:4.0.0" +"@napi-rs/wasm-runtime@npm:^0.2.11": + version: 0.2.12 + resolution: "@napi-rs/wasm-runtime@npm:0.2.12" + dependencies: + "@emnapi/core": "npm:^1.4.3" + "@emnapi/runtime": "npm:^1.4.3" + "@tybys/wasm-util": "npm:^0.10.0" + checksum: 10/5fd518182427980c28bc724adf06c5f32f9a8915763ef560b5f7d73607d30cd15ac86d0cbd2eb80d4cfab23fc80d0876d89ca36a9daadcb864bc00917c94187c + languageName: node + linkType: hard + +"@nestjs/axios@npm:4.0.1": + version: 4.0.1 + resolution: "@nestjs/axios@npm:4.0.1" peerDependencies: "@nestjs/common": ^10.0.0 || ^11.0.0 axios: ^1.3.1 rxjs: ^7.0.0 - checksum: 10/9a61ac8a2fdbf304961696148945ba9e19c0ed73256767b0a643bbafe77106b2f738cb2f35f2fc63bff09a8abcd18365d2f8c772484e2fdd70b455bceb7b5822 + checksum: 10/0cc741e4fbfc39920afbb6c58050e0dbfecc8e2f7b249d879802a5b03b65df3714e828970e2bf12283d3d27e1f1ab7ca5ec62b5698dc50f105680e100a0a33f6 languageName: node linkType: hard -"@nestjs/common@npm:11.1.1": - version: 11.1.1 - resolution: "@nestjs/common@npm:11.1.1" +"@nestjs/common@npm:11.1.12": + version: 11.1.12 + resolution: "@nestjs/common@npm:11.1.12" dependencies: - file-type: "npm:20.5.0" + file-type: "npm:21.3.0" iterare: "npm:1.2.1" - load-esm: "npm:1.0.2" + load-esm: "npm:1.0.3" tslib: "npm:2.8.1" uid: "npm:2.0.2" peerDependencies: @@ -5008,18 +5286,18 @@ __metadata: optional: true class-validator: optional: true - checksum: 10/b58b951984df667b794a6c9cf65dda8ee43fe0c4e552183e045b126c82d99b50cddb1e84ea03b8dd7b04fca512acb107222ffad6b19dba359323f9b813032161 + checksum: 10/0e8a0b73453f1e77ae1be6eabc68b8374e6a8a39cf46d5beafaa398fa72aed6b653df91b72e5b2aa2225251b1463ca79d8424d207a981f586ac216441f83ac59 languageName: node linkType: hard -"@nestjs/core@npm:11.1.1": - version: 11.1.1 - resolution: "@nestjs/core@npm:11.1.1" +"@nestjs/core@npm:11.1.12": + version: 11.1.12 + resolution: "@nestjs/core@npm:11.1.12" dependencies: "@nuxt/opencollective": "npm:0.4.1" fast-safe-stringify: "npm:2.1.1" iterare: "npm:1.2.1" - path-to-regexp: "npm:8.2.0" + path-to-regexp: "npm:8.3.0" tslib: "npm:2.8.1" uid: "npm:2.0.2" peerDependencies: @@ -5036,7 +5314,7 @@ __metadata: optional: true "@nestjs/websockets": optional: true - checksum: 10/88f8a3c52a98c059e6d93c5faf9194daee4667b1a855f88233582acf51e3be9aedb3e3f7f992da46e0e45df31ad4e045e0296e6e35ea14e2245d4069176711c0 + checksum: 10/44f122101f411abb2b83cd6bab865006c4a74552d3903ca47a58b396608453b36b59cd6461f8689cd31c3d91b81960cea190d654448921ae0efedbd20ff52284 languageName: node linkType: hard @@ -5067,19 +5345,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/agent@npm:^3.0.0": - version: 3.0.0 - resolution: "@npmcli/agent@npm:3.0.0" - dependencies: - agent-base: "npm:^7.1.0" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.1" - lru-cache: "npm:^10.0.1" - socks-proxy-agent: "npm:^8.0.3" - checksum: 10/775c9a7eb1f88c195dfb3bce70c31d0fe2a12b28b754e25c08a3edb4bc4816bfedb7ac64ef1e730579d078ca19dacf11630e99f8f3c3e0fd7b23caa5fd6d30a6 - languageName: node - linkType: hard - "@npmcli/agent@npm:^4.0.0": version: 4.0.0 resolution: "@npmcli/agent@npm:4.0.0" @@ -5449,7 +5714,7 @@ __metadata: languageName: node linkType: hard -"@npmcli/package-json@npm:7.0.2, @npmcli/package-json@npm:^7.0.0": +"@npmcli/package-json@npm:7.0.2": version: 7.0.2 resolution: "@npmcli/package-json@npm:7.0.2" dependencies: @@ -5488,6 +5753,21 @@ __metadata: languageName: node linkType: hard +"@npmcli/package-json@npm:^7.0.0": + version: 7.0.1 + resolution: "@npmcli/package-json@npm:7.0.1" + dependencies: + "@npmcli/git": "npm:^7.0.0" + glob: "npm:^11.0.3" + hosted-git-info: "npm:^9.0.0" + json-parse-even-better-errors: "npm:^4.0.0" + proc-log: "npm:^5.0.0" + semver: "npm:^7.5.3" + validate-npm-package-license: "npm:^3.0.4" + checksum: 10/be69096e889ebd3b832de24c56be17784ba00529af5f16d8092c0e911ac29acaf18ba86792e791a15f0681366ffd923a696b0b0f3840b1e68407909273c23e3e + languageName: node + linkType: hard + "@npmcli/promise-spawn@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/promise-spawn@npm:3.0.0" @@ -5551,17 +5831,17 @@ __metadata: languageName: node linkType: hard -"@npmcli/run-script@npm:10.0.2, @npmcli/run-script@npm:^10.0.0": - version: 10.0.2 - resolution: "@npmcli/run-script@npm:10.0.2" +"@npmcli/run-script@npm:10.0.3, @npmcli/run-script@npm:^10.0.0": + version: 10.0.3 + resolution: "@npmcli/run-script@npm:10.0.3" dependencies: "@npmcli/node-gyp": "npm:^5.0.0" "@npmcli/package-json": "npm:^7.0.0" "@npmcli/promise-spawn": "npm:^9.0.0" - node-gyp: "npm:^11.0.0" + node-gyp: "npm:^12.1.0" proc-log: "npm:^6.0.0" - which: "npm:^5.0.0" - checksum: 10/546e68c8f828e721b54e6386d5874e3b834ae2bf7ff5f41aa40b34b90241b71eb229985bf36042e97a660669c1cf7073d32a8b4cde0b333eec765df1a3cd765f + which: "npm:^6.0.0" + checksum: 10/3b2b6b02a40c7470a900e8d77d23e2239608c08e919d6ddee7849fc7093be0999d9eb2c9dec871988e80165a64f9d8c55430f0a699690e555ebd3e81bf1dbd35 languageName: node linkType: hard @@ -5578,13 +5858,6 @@ __metadata: languageName: node linkType: hard -"@nrk/core-icons@npm:^9.6.0": - version: 9.6.0 - resolution: "@nrk/core-icons@npm:9.6.0" - checksum: 10/0384037b0b7ec21ea6bc516685a510e7f0c0acbcc86549365fb8fd90aceb15129327e59a3a36944bba93c5453f1d1482f31002311e85d1f78bd5f7ca2100894b - languageName: node - linkType: hard - "@nuxt/opencollective@npm:0.4.1": version: 0.4.1 resolution: "@nuxt/opencollective@npm:0.4.1" @@ -5837,31 +6110,30 @@ __metadata: languageName: node linkType: hard -"@openapitools/openapi-generator-cli@npm:^2.20.2": - version: 2.20.2 - resolution: "@openapitools/openapi-generator-cli@npm:2.20.2" +"@openapitools/openapi-generator-cli@npm:^2.28.0": + version: 2.28.0 + resolution: "@openapitools/openapi-generator-cli@npm:2.28.0" dependencies: - "@nestjs/axios": "npm:4.0.0" - "@nestjs/common": "npm:11.1.1" - "@nestjs/core": "npm:11.1.1" + "@nestjs/axios": "npm:4.0.1" + "@nestjs/common": "npm:11.1.12" + "@nestjs/core": "npm:11.1.12" "@nuxtjs/opencollective": "npm:0.3.2" - axios: "npm:1.9.0" + axios: "npm:1.13.2" chalk: "npm:4.1.2" commander: "npm:8.3.0" - compare-versions: "npm:4.1.4" - concurrently: "npm:6.5.1" + compare-versions: "npm:6.1.1" + concurrently: "npm:9.2.1" console.table: "npm:0.10.0" - fs-extra: "npm:11.3.0" - glob: "npm:9.3.5" - inquirer: "npm:8.2.6" - lodash: "npm:4.17.21" + fs-extra: "npm:11.3.3" + glob: "npm:13.0.0" + inquirer: "npm:8.2.7" proxy-agent: "npm:6.5.0" reflect-metadata: "npm:0.2.2" rxjs: "npm:7.8.2" tslib: "npm:2.8.1" bin: openapi-generator-cli: main.js - checksum: 10/29807b52555e7307207eff6e0c4c1a25b970252915e13d847a6c275d1d638af0732761e51816f5bb5a91830ebabc044b3924a106db8cffa19dda01517a77be17 + checksum: 10/42f6c887d8a16eca90420139e1d0d464bd78aad1d609a8a9589c3fbcda4ab3052f6b9310b7b54521583df50f2dfb4cb0c9de073e717e91eadbf4e230e70f3007 languageName: node linkType: hard @@ -6066,10 +6338,10 @@ __metadata: languageName: node linkType: hard -"@pkgr/core@npm:^0.1.0": - version: 0.1.1 - resolution: "@pkgr/core@npm:0.1.1" - checksum: 10/6f25fd2e3008f259c77207ac9915b02f1628420403b2630c92a07ff963129238c9262afc9e84344c7a23b5cc1f3965e2cd17e3798219f5fd78a63d144d3cceba +"@pkgr/core@npm:^0.2.9": + version: 0.2.9 + resolution: "@pkgr/core@npm:0.2.9" + checksum: 10/bb2fb86977d63f836f8f5b09015d74e6af6488f7a411dcd2bfdca79d76b5a681a9112f41c45bdf88a9069f049718efc6f3900d7f1de66a2ec966068308ae517f languageName: node linkType: hard @@ -6187,6 +6459,23 @@ __metadata: languageName: node linkType: hard +"@puppeteer/browsers@npm:2.11.2": + version: 2.11.2 + resolution: "@puppeteer/browsers@npm:2.11.2" + dependencies: + debug: "npm:^4.4.3" + extract-zip: "npm:^2.0.1" + progress: "npm:^2.0.3" + proxy-agent: "npm:^6.5.0" + semver: "npm:^7.7.3" + tar-fs: "npm:^3.1.1" + yargs: "npm:^17.7.2" + bin: + browsers: lib/cjs/main-cli.js + checksum: 10/7de1cbf31fe75a455ea2ad9bd1acb62111e16510096d4e0bdb7930f57d72fd764d43a6106f43429605e8490702850d9b2c0da04860e7fd8371b52f4a7271bd0a + languageName: node + linkType: hard + "@rc-component/portal@npm:^1.1.0": version: 1.1.2 resolution: "@rc-component/portal@npm:1.1.2" @@ -6292,6 +6581,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:1.0.0-rc.2": + version: 1.0.0-rc.2 + resolution: "@rolldown/pluginutils@npm:1.0.0-rc.2" + checksum: 10/8dba3626ca26f49ed83d4db4a9eaacfcc6715cc8544f2969419489c90a2bb000025976049e0f6c5c2880817bff753fb04bec8fb57df9423f07958ce8da97035e + languageName: node + linkType: hard + "@rollup/plugin-babel@npm:^5.2.1": version: 5.3.1 resolution: "@rollup/plugin-babel@npm:5.3.1" @@ -6354,135 +6650,177 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.34.2" +"@rollup/rollup-android-arm-eabi@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.59.0" conditions: os=android & cpu=arm languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-android-arm64@npm:4.34.2" +"@rollup/rollup-android-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-android-arm64@npm:4.59.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-darwin-arm64@npm:4.34.2" +"@rollup/rollup-darwin-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.59.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-darwin-x64@npm:4.34.2" +"@rollup/rollup-darwin-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.59.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.34.2" +"@rollup/rollup-freebsd-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.59.0" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-freebsd-x64@npm:4.34.2" +"@rollup/rollup-freebsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-freebsd-x64@npm:4.59.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.34.2" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.59.0" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.34.2" +"@rollup/rollup-linux-arm-musleabihf@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.59.0" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.34.2" +"@rollup/rollup-linux-arm64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.59.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.34.2" +"@rollup/rollup-linux-arm64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.59.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.34.2" +"@rollup/rollup-linux-loong64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.59.0" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.34.2" +"@rollup/rollup-linux-loong64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.59.0" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.59.0" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.34.2" +"@rollup/rollup-linux-ppc64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.59.0" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.59.0" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.34.2" +"@rollup/rollup-linux-riscv64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.59.0" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.59.0" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.34.2" +"@rollup/rollup-linux-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.59.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.34.2" +"@rollup/rollup-linux-x64-musl@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.59.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.34.2" +"@rollup/rollup-openbsd-x64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openbsd-x64@npm:4.59.0" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.59.0" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.59.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.34.2" +"@rollup/rollup-win32-ia32-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.59.0" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.34.2": - version: 4.34.2 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.34.2" +"@rollup/rollup-win32-x64-gnu@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.59.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.59.0": + version: 4.59.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.59.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -6643,12 +6981,13 @@ __metadata: languageName: node linkType: hard -"@shuttle-lib/core@npm:0.0.2": - version: 0.0.2 - resolution: "@shuttle-lib/core@npm:0.0.2" +"@shuttle-lib/core@npm:0.1.3": + version: 0.1.3 + resolution: "@shuttle-lib/core@npm:0.1.3" dependencies: - tslib: "npm:^2.4.0" - checksum: 10/edbb825940fee2d5fc22fb4c8c44607f6f75197ade72204876356153a6274045efcea8f9cc6ab6a6bec08da1a65e87ea4b9e15678bbfb9721b72aa4104d29598 + eventemitter3: "npm:^5.0.1" + tslib: "npm:^2.8.1" + checksum: 10/1b3e426df3cf9ca6f2a07d2aad9ef8e14a51feb4a76d9d26272dee309cbe92f180a5de6c047ea7c82d373eb932cbb15d5110626afa8038299863be4ce7b4ceee languageName: node linkType: hard @@ -6761,75 +7100,21 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^1.7.0": - version: 1.8.6 - resolution: "@sinonjs/commons@npm:1.8.6" - dependencies: - type-detect: "npm:4.0.8" - checksum: 10/51987338fd8b4d1e135822ad593dd23a3288764aa41d83c695124d512bc38b87eece859078008651ecc7f1df89a7e558a515dc6f02d21a93be4ba50b39a28914 - languageName: node - linkType: hard - -"@sinonjs/commons@npm:^2.0.0": - version: 2.0.0 - resolution: "@sinonjs/commons@npm:2.0.0" - dependencies: - type-detect: "npm:4.0.8" - checksum: 10/bd6b44957077cd99067dcf401e80ed5ea03ba930cba2066edbbfe302d5fc973a108db25c0ae4930ee53852716929e4c94fa3b8a1510a51ac6869443a139d1e3d - languageName: node - linkType: hard - -"@sinonjs/commons@npm:^3.0.0": - version: 3.0.0 - resolution: "@sinonjs/commons@npm:3.0.0" +"@sinonjs/commons@npm:^3.0.1": + version: 3.0.1 + resolution: "@sinonjs/commons@npm:3.0.1" dependencies: type-detect: "npm:4.0.8" - checksum: 10/086720ae0bc370829322df32612205141cdd44e592a8a9ca97197571f8f970352ea39d3bda75b347c43789013ddab36b34b59e40380a49bdae1c2df3aa85fe4f - languageName: node - linkType: hard - -"@sinonjs/fake-timers@npm:^10.0.2": - version: 10.3.0 - resolution: "@sinonjs/fake-timers@npm:10.3.0" - dependencies: - "@sinonjs/commons": "npm:^3.0.0" - checksum: 10/78155c7bd866a85df85e22028e046b8d46cf3e840f72260954f5e3ed5bd97d66c595524305a6841ffb3f681a08f6e5cef572a2cce5442a8a232dc29fb409b83e - languageName: node - linkType: hard - -"@sinonjs/fake-timers@npm:^11.2.2": - version: 11.2.2 - resolution: "@sinonjs/fake-timers@npm:11.2.2" - dependencies: - "@sinonjs/commons": "npm:^3.0.0" - checksum: 10/da7dfa677b2362bc5a321fc1563184755b5c62fbb1a72457fb9e901cd187ba9dc834f9e8a0fb5a4e1d1e6e6ad4c5b54e90900faa44dd6c82d3c49c92ec23ecd4 - languageName: node - linkType: hard - -"@sinonjs/fake-timers@npm:^9.1.2": - version: 9.1.2 - resolution: "@sinonjs/fake-timers@npm:9.1.2" - dependencies: - "@sinonjs/commons": "npm:^1.7.0" - checksum: 10/033c74ad389b0655b6af2fa1af31dddf45878e65879f06c5d1940e0ceb053a234f2f46c728dcd97df8ee9312431e45dd7aedaee3a69d47f73a2001a7547fc3d6 + checksum: 10/a0af217ba7044426c78df52c23cedede6daf377586f3ac58857c565769358ab1f44ebf95ba04bbe38814fba6e316ca6f02870a009328294fc2c555d0f85a7117 languageName: node linkType: hard -"@sinonjs/samsam@npm:^7.0.1": - version: 7.0.1 - resolution: "@sinonjs/samsam@npm:7.0.1" +"@sinonjs/fake-timers@npm:^13.0.0": + version: 13.0.5 + resolution: "@sinonjs/fake-timers@npm:13.0.5" dependencies: - "@sinonjs/commons": "npm:^2.0.0" - lodash.get: "npm:^4.4.2" - type-detect: "npm:^4.0.8" - checksum: 10/1ebb5c4e589f4e2684fbe846f12552b27d90139d118da1c940e3a05ab6322ac6b2d7033975c535357020db36a748cb6579cc4576b36917aba89f7f79519e584f - languageName: node - linkType: hard - -"@sinonjs/text-encoding@npm:^0.7.2": - version: 0.7.2 - resolution: "@sinonjs/text-encoding@npm:0.7.2" - checksum: 10/ec713fb44888c852d84ca54f6abf9c14d036c11a5d5bfab7825b8b9d2b22127dbe53412c68f4dbb0c05ea5ed61c64679bd2845c177d81462db41e0d3d7eca499 + "@sinonjs/commons": "npm:^3.0.1" + checksum: 10/11ee417968fc4dce1896ab332ac13f353866075a9d2a88ed1f6258f17cc4f7d93e66031b51fcddb8c203aa4d53fd980b0ae18aba06269f4682164878a992ec3f languageName: node linkType: hard @@ -6840,14 +7125,14 @@ __metadata: languageName: node linkType: hard -"@slack/webhook@npm:^7.0.4": - version: 7.0.4 - resolution: "@slack/webhook@npm:7.0.4" +"@slack/webhook@npm:^7.0.6": + version: 7.0.6 + resolution: "@slack/webhook@npm:7.0.6" dependencies: "@slack/types": "npm:^2.9.0" "@types/node": "npm:>=18.0.0" - axios: "npm:^1.7.8" - checksum: 10/f4a3c7400b2281622eb2a3ed992425e4f777e80876cd69b0d8897fe3d5f5dfac4008131fd9afdd1d7bcb6ba00e5e562c7e6df7236e16bd6447d0c85b25930d23 + axios: "npm:^1.11.0" + checksum: 10/8f8083f9654e590f04731985b337f576842b2034a9261010f85d813c4e262f69d856c142b0dcf2022bfe69c22c2e97cc7d877a79989cd0f7a0cf2554ae0754ed languageName: node linkType: hard @@ -6862,31 +7147,42 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@npm:1.53.0-in-development, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@so-ric/colorspace@npm:^1.1.6": + version: 1.1.6 + resolution: "@so-ric/colorspace@npm:1.1.6" + dependencies: + color: "npm:^5.0.2" + text-hex: "npm:1.0.x" + checksum: 10/fc3285e5cb9a458d255aa678d9453174ca40689a4c692f1617907996ab8eb78839542439604ced484c4f674a5297f7ba8b0e63fcfe901174f43c3d9c3c881b52 + languageName: node + linkType: hard + +"@sofie-automation/blueprints-integration@npm:26.3.0-2, @sofie-automation/blueprints-integration@workspace:blueprints-integration": version: 0.0.0-use.local resolution: "@sofie-automation/blueprints-integration@workspace:blueprints-integration" dependencies: - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" + "@sofie-automation/shared-lib": "npm:26.3.0-2" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" languageName: unknown linkType: soft -"@sofie-automation/code-standard-preset@npm:^3.0.0": - version: 3.0.0 - resolution: "@sofie-automation/code-standard-preset@npm:3.0.0" +"@sofie-automation/code-standard-preset@npm:^3.2.1": + version: 3.2.1 + resolution: "@sofie-automation/code-standard-preset@npm:3.2.1" dependencies: - "@sofie-automation/eslint-plugin": "npm:0.2.0" + "@sofie-automation/eslint-plugin": "npm:0.2.1" + "@vitest/eslint-plugin": "npm:^1.6.6" date-fns: "npm:^4.1.0" - eslint-config-prettier: "npm:^10.0.1" - eslint-plugin-jest: "npm:^28.11.0" - eslint-plugin-n: "npm:^17.15.1" - eslint-plugin-prettier: "npm:^5.2.3" + eslint-config-prettier: "npm:^10.1.8" + eslint-plugin-jest: "npm:^28.14.0" + eslint-plugin-n: "npm:^17.23.2" + eslint-plugin-prettier: "npm:^5.5.5" license-checker: "npm:^25.0.1" meow: "npm:^13.2.0" read-package-up: "npm:^11.0.0" - semver: "npm:^7.6.3" - typescript-eslint: "npm:^8.21.0" + semver: "npm:^7.7.3" + typescript-eslint: "npm:^8.54.0" peerDependencies: eslint: ^9 prettier: ^3 @@ -6894,39 +7190,40 @@ __metadata: bin: sofie-licensecheck: ./bin/checkLicenses.mjs sofie-version: ./bin/updateVersion.mjs - checksum: 10/fa61dc1f90377ad2196f2e6c33dea9988bbe9cfd6eb8b277a083ae1147c00e83e526b7520bb5548d4935fb91b7f9f1d8f9b701db419da760488c318ea42a243f + checksum: 10/db31f2aa51b504c86b8a4e4cca9cd6a8a80769d4ed020f84ec2780abb0735a14e465693f8de907fbc9e668e07e04141a95c14caeea0e61916546a576671f34aa languageName: node linkType: hard -"@sofie-automation/corelib@npm:1.53.0-in-development, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@npm:26.3.0-2, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": "npm:1.53.0-in-development" - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" + "@sofie-automation/blueprints-integration": "npm:26.3.0-2" + "@sofie-automation/shared-lib": "npm:26.3.0-2" fast-clone: "npm:^1.5.13" i18next: "npm:^21.10.0" - influx: "npm:^5.9.7" - nanoid: "npm:^3.3.8" + influx: "npm:^5.12.0" + nanoid: "npm:^3.3.11" object-path: "npm:^0.11.8" prom-client: "npm:^15.1.3" timecode: "npm:0.0.4" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" underscore: "npm:^1.13.7" peerDependencies: mongodb: ^6.12.0 languageName: unknown linkType: soft -"@sofie-automation/eslint-plugin@npm:0.2.0": - version: 0.2.0 - resolution: "@sofie-automation/eslint-plugin@npm:0.2.0" +"@sofie-automation/eslint-plugin@npm:0.2.1": + version: 0.2.1 + resolution: "@sofie-automation/eslint-plugin@npm:0.2.1" dependencies: "@typescript-eslint/utils": "npm:^8.21.0" + tslib: "npm:^2.8.1" peerDependencies: eslint: ^9 - checksum: 10/7d2898cab2d89fcab727597a7a8ff49dacb030166f390d4b20ec27fbb53f8e330a2a034090484611f1cb0fe98bd4a1bc961e0cc6e77236d5c84065ae830fa1ad + checksum: 10/650cd6f075d531a9f88012aff314d3a9d5a0e9414f2062a13ba49ba955ad6140255ec863ee69ea2b112c31228341de7ad4f42ecf3ebd39cfdea1fba5be62da0b languageName: node linkType: hard @@ -6934,56 +7231,57 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: - "@slack/webhook": "npm:^7.0.4" - "@sofie-automation/blueprints-integration": "npm:1.53.0-in-development" - "@sofie-automation/corelib": "npm:1.53.0-in-development" - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" - amqplib: "npm:^0.10.5" + "@slack/webhook": "npm:^7.0.6" + "@sofie-automation/blueprints-integration": "npm:26.3.0-2" + "@sofie-automation/corelib": "npm:26.3.0-2" + "@sofie-automation/shared-lib": "npm:26.3.0-2" + amqplib: "npm:0.10.5" + chrono-node: "npm:^2.9.0" deepmerge: "npm:^4.3.1" - elastic-apm-node: "npm:^4.11.0" - jest: "npm:^29.7.0" - jest-mock-extended: "npm:^3.0.7" - mongodb: "npm:^6.12.0" + elastic-apm-node: "npm:^4.15.0" + jest: "npm:^30.2.0" + jest-mock-extended: "npm:^4.0.0" + mongodb: "npm:^6.21.0" p-lazy: "npm:^3.1.0" p-timeout: "npm:^4.1.0" superfly-timeline: "npm:9.2.0" - threadedclass: "npm:^1.2.2" + threadedclass: "npm:^1.3.0" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" typescript: "npm:~5.7.3" underscore: "npm:^1.13.7" languageName: unknown linkType: soft -"@sofie-automation/live-status-gateway-api@npm:1.53.0-in-development, @sofie-automation/live-status-gateway-api@workspace:live-status-gateway-api": +"@sofie-automation/live-status-gateway-api@npm:26.3.0-2, @sofie-automation/live-status-gateway-api@workspace:live-status-gateway-api": version: 0.0.0-use.local resolution: "@sofie-automation/live-status-gateway-api@workspace:live-status-gateway-api" dependencies: - "@apidevtools/json-schema-ref-parser": "npm:^14.2.1" - "@asyncapi/generator": "npm:^2.6.0" - "@asyncapi/html-template": "npm:^3.2.0" - "@asyncapi/modelina": "npm:^4.0.4" + "@apidevtools/json-schema-ref-parser": "npm:^15.2.2" + "@asyncapi/generator": "npm:^2.11.0" + "@asyncapi/html-template": "npm:^3.5.4" + "@asyncapi/modelina": "npm:^5.10.1" "@asyncapi/nodejs-ws-template": "npm:^0.10.0" - "@asyncapi/parser": "npm:^3.4.0" - tslib: "npm:^2.6.2" - yaml: "npm:^2.8.1" + "@asyncapi/parser": "npm:^3.6.0" + tslib: "npm:^2.8.1" + yaml: "npm:^2.8.2" languageName: unknown linkType: soft -"@sofie-automation/meteor-lib@npm:1.53.0-in-development, @sofie-automation/meteor-lib@workspace:meteor-lib": +"@sofie-automation/meteor-lib@npm:26.3.0-2, @sofie-automation/meteor-lib@workspace:meteor-lib": version: 0.0.0-use.local resolution: "@sofie-automation/meteor-lib@workspace:meteor-lib" dependencies: "@mos-connection/helper": "npm:^5.0.0-alpha.0" - "@sofie-automation/blueprints-integration": "npm:1.53.0-in-development" - "@sofie-automation/corelib": "npm:1.53.0-in-development" - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" + "@sofie-automation/blueprints-integration": "npm:26.3.0-2" + "@sofie-automation/corelib": "npm:26.3.0-2" + "@sofie-automation/shared-lib": "npm:26.3.0-2" "@types/deep-extend": "npm:^0.6.2" - "@types/semver": "npm:^7.5.8" + "@types/semver": "npm:^7.7.1" "@types/underscore": "npm:^1.13.0" deep-extend: "npm:0.6.0" - semver: "npm:^7.6.3" - type-fest: "npm:^4.33.0" + semver: "npm:^7.7.3" + type-fest: "npm:^4.41.0" underscore: "npm:^1.13.7" peerDependencies: i18next: ^21.10.0 @@ -6995,41 +7293,40 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/openapi@workspace:openapi" dependencies: - "@openapitools/openapi-generator-cli": "npm:^2.20.2" - eslint: "npm:^9.18.0" - eslint-plugin-yml: "npm:^1.16.0" - js-yaml: "npm:^4.1.0" + "@openapitools/openapi-generator-cli": "npm:^2.28.0" + eslint: "npm:^9.39.2" + js-yaml: "npm:^4.1.1" tslib: "npm:^2.8.1" wget-improved: "npm:^3.4.0" languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@npm:1.53.0-in-development, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@npm:26.3.0-2, @sofie-automation/server-core-integration@workspace:server-core-integration": version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@koa/router": "npm:^14.0.0" - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" - "@types/koa": "npm:^3.0.0" - "@types/koa__router": "npm:^12.0.4" + "@koa/router": "npm:^15.3.0" + "@sofie-automation/shared-lib": "npm:26.3.0-2" + "@types/koa": "npm:^3.0.1" + "@types/koa__router": "npm:^12.0.5" ejson: "npm:^2.2.3" faye-websocket: "npm:^0.11.4" got: "npm:^11.8.6" - koa: "npm:^3.0.1" + koa: "npm:^3.1.1" tslib: "npm:^2.8.1" underscore: "npm:^1.13.7" languageName: unknown linkType: soft -"@sofie-automation/shared-lib@npm:1.53.0-in-development, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@npm:26.3.0-2, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: "@mos-connection/model": "npm:^5.0.0-alpha.0" kairos-lib: "npm:^0.2.3" - timeline-state-resolver-types: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" + timeline-state-resolver-types: "npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" languageName: unknown linkType: soft @@ -7044,82 +7341,75 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/webui@workspace:webui" dependencies: - "@babel/preset-env": "npm:^7.26.7" - "@crello/react-lottie": "npm:0.0.9" - "@fortawesome/fontawesome-free": "npm:^6.7.2" - "@fortawesome/fontawesome-svg-core": "npm:^6.7.2" - "@fortawesome/free-solid-svg-icons": "npm:^6.7.2" - "@fortawesome/react-fontawesome": "npm:^0.2.2" - "@jstarpl/react-contextmenu": "npm:^2.15.1" - "@nrk/core-icons": "npm:^9.6.0" + "@babel/preset-env": "npm:^7.29.0" + "@fortawesome/fontawesome-free": "npm:^7.1.0" + "@fortawesome/fontawesome-svg-core": "npm:^7.1.0" + "@fortawesome/free-solid-svg-icons": "npm:^7.1.0" + "@fortawesome/react-fontawesome": "npm:^3.1.1" + "@jstarpl/react-contextmenu": "npm:^2.15.3" "@popperjs/core": "npm:^2.11.8" - "@sofie-automation/blueprints-integration": "npm:1.53.0-in-development" - "@sofie-automation/corelib": "npm:1.53.0-in-development" - "@sofie-automation/meteor-lib": "npm:1.53.0-in-development" - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" + "@sofie-automation/blueprints-integration": "npm:26.3.0-2" + "@sofie-automation/corelib": "npm:26.3.0-2" + "@sofie-automation/meteor-lib": "npm:26.3.0-2" + "@sofie-automation/shared-lib": "npm:26.3.0-2" "@sofie-automation/sorensen": "npm:^1.5.11" - "@testing-library/dom": "npm:^10.4.0" - "@testing-library/jest-dom": "npm:^6.6.3" - "@testing-library/react": "npm:^16.2.0" + "@testing-library/dom": "npm:^10.4.1" + "@testing-library/jest-dom": "npm:^6.9.1" + "@testing-library/react": "npm:^16.3.2" "@testing-library/user-event": "npm:^14.6.1" - "@types/bootstrap": "npm:^5" + "@types/bootstrap": "npm:^5.2.10" "@types/classnames": "npm:^2.3.4" "@types/deep-extend": "npm:^0.6.2" - "@types/react": "npm:^18.3.18" - "@types/react-circular-progressbar": "npm:^1.1.0" - "@types/react-datepicker": "npm:^3.1.8" - "@types/react-dom": "npm:^18.3.5" + "@types/react": "npm:^18.3.27" + "@types/react-dom": "npm:^18.3.7" "@types/react-router": "npm:^5.1.20" - "@types/react-router-bootstrap": "npm:^0" + "@types/react-router-bootstrap": "npm:^0.26.8" "@types/react-router-dom": "npm:^5.3.3" "@types/sha.js": "npm:^2.4.4" - "@types/sinon": "npm:^10.0.20" "@types/xml2js": "npm:^0.4.14" - "@vitejs/plugin-react": "npm:^4.3.4" + "@vitejs/plugin-react": "npm:^5.1.3" "@welldone-software/why-did-you-render": "npm:^4.3.2" - "@xmldom/xmldom": "npm:^0.8.10" - babel-jest: "npm:^29.7.0" - bootstrap: "npm:^5.3.3" + "@xmldom/xmldom": "npm:^0.8.11" + babel-jest: "npm:^30.2.0" + bootstrap: "npm:^5.3.8" classnames: "npm:^2.5.1" cubic-spline: "npm:^3.0.3" deep-extend: "npm:0.6.0" ejson: "npm:^2.2.3" - globals: "npm:^15.14.0" + globals: "npm:^17.3.0" i18next: "npm:^21.10.0" - i18next-browser-languagedetector: "npm:^6.1.8" - i18next-http-backend: "npm:^1.4.5" + i18next-browser-languagedetector: "npm:^8.2.0" + i18next-http-backend: "npm:^3.0.2" immutability-helper: "npm:^3.1.1" - lottie-web: "npm:^5.12.2" + lottie-react: "npm:^2.4.1" moment: "npm:^2.30.1" - motion: "npm:^12.4.7" + motion: "npm:^12.31.0" promise.allsettled: "npm:^1.0.7" query-string: "npm:^6.14.1" rc-tooltip: "npm:^6.4.0" react: "npm:^18.3.1" - react-bootstrap: "npm:^2.10.9" - react-circular-progressbar: "npm:^2.1.0" - react-datepicker: "npm:^3.8.0" + react-bootstrap: "npm:^2.10.10" + react-datepicker: "npm:^9.1.0" react-dnd: "npm:^14.0.5" react-dnd-html5-backend: "npm:^14.1.0" react-dom: "npm:^18.3.1" react-focus-bounder: "npm:^1.1.6" react-hotkeys: "npm:^2.0.0" react-i18next: "npm:^11.18.6" - react-intersection-observer: "npm:^9.15.1" - react-moment: "npm:^0.9.7" + react-intersection-observer: "npm:^9.16.0" + react-moment: "npm:^1.2.1" react-popper: "npm:^2.3.0" react-router-bootstrap: "npm:^0.25.0" react-router-dom: "npm:^5.3.4" - sass: "npm:^1.83.4" - semver: "npm:^7.6.3" - sha.js: "npm:^2.4.11" - shuttle-webhid: "npm:^0.0.2" - sinon: "npm:^14.0.2" - type-fest: "npm:^4.33.0" + sass-embedded: "npm:^1.97.3" + semver: "npm:^7.7.3" + sha.js: "npm:^2.4.12" + shuttle-webhid: "npm:^0.1.3" + type-fest: "npm:^4.41.0" typescript: "npm:~5.7.3" underscore: "npm:^1.13.7" - vite: "npm:^6.0.11" - vite-plugin-node-polyfills: "npm:^0.23.0" + vite: "npm:^7.3.1" + vite-plugin-node-polyfills: "npm:^0.25.0" vite-tsconfig-paths: "npm:^5.1.4" webmidi: "npm:^2.5.3" xml2js: "npm:^0.6.2" @@ -7534,40 +7824,39 @@ __metadata: languageName: node linkType: hard -"@testing-library/dom@npm:^10.4.0": - version: 10.4.0 - resolution: "@testing-library/dom@npm:10.4.0" +"@testing-library/dom@npm:^10.4.1": + version: 10.4.1 + resolution: "@testing-library/dom@npm:10.4.1" dependencies: "@babel/code-frame": "npm:^7.10.4" "@babel/runtime": "npm:^7.12.5" "@types/aria-query": "npm:^5.0.1" aria-query: "npm:5.3.0" - chalk: "npm:^4.1.0" dom-accessibility-api: "npm:^0.5.9" lz-string: "npm:^1.5.0" + picocolors: "npm:1.1.1" pretty-format: "npm:^27.0.2" - checksum: 10/05825ee9a15b88cbdae12c137db7111c34069ed3c7a1bd03b6696cb1b37b29f6f2d2de581ebf03033e7df1ab7ebf08399310293f440a4845d95c02c0a9ecc899 + checksum: 10/7f93e09ea015f151f8b8f42cbab0b2b858999b5445f15239a72a612ef7716e672b14c40c421218194cf191cbecbde0afa6f3dc2cc83dda93ff6a4fb0237df6e6 languageName: node linkType: hard -"@testing-library/jest-dom@npm:^6.6.3": - version: 6.6.3 - resolution: "@testing-library/jest-dom@npm:6.6.3" +"@testing-library/jest-dom@npm:^6.9.1": + version: 6.9.1 + resolution: "@testing-library/jest-dom@npm:6.9.1" dependencies: "@adobe/css-tools": "npm:^4.4.0" aria-query: "npm:^5.0.0" - chalk: "npm:^3.0.0" css.escape: "npm:^1.5.1" dom-accessibility-api: "npm:^0.6.3" - lodash: "npm:^4.17.21" + picocolors: "npm:^1.1.1" redent: "npm:^3.0.0" - checksum: 10/1f3427e45870eab9dcc59d6504b780d4a595062fe1687762ae6e67d06a70bf439b40ab64cf58cbace6293a99e3764d4647fdc8300a633b721764f5ce39dade18 + checksum: 10/409b4f519e4c68f4d31e3b0317338cc19098b9029513fca61aa2af8270086ae3956a1eaedd19bbce2d2c9e2cf9ff27a616c06556be7a26e101c0d529a0062233 languageName: node linkType: hard -"@testing-library/react@npm:^16.2.0": - version: 16.2.0 - resolution: "@testing-library/react@npm:16.2.0" +"@testing-library/react@npm:^16.3.2": + version: 16.3.2 + resolution: "@testing-library/react@npm:16.3.2" dependencies: "@babel/runtime": "npm:^7.12.5" peerDependencies: @@ -7581,7 +7870,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10/cf10bfa9a363384e6861417696fff4a464a64f98ec6f0bb7f1fa7cbb550d075d23a2f6a943b7df85dded7bde3234f6ea6b6e36f95211f4544b846ea72c288289 + checksum: 10/0ca88c6f672d00c2afd1bdedeff9b5382dd8157038efeb9762dc016731030075624be7106b92d2b5e5c52812faea85263e69272c14b6f8700eb48a4a8af6feef languageName: node linkType: hard @@ -7594,14 +7883,13 @@ __metadata: languageName: node linkType: hard -"@tokenizer/inflate@npm:^0.2.6": - version: 0.2.7 - resolution: "@tokenizer/inflate@npm:0.2.7" +"@tokenizer/inflate@npm:^0.4.1": + version: 0.4.1 + resolution: "@tokenizer/inflate@npm:0.4.1" dependencies: - debug: "npm:^4.4.0" - fflate: "npm:^0.8.2" - token-types: "npm:^6.0.0" - checksum: 10/6cee1857e47ca0fc053d6cd87773b7c21857ab84cb847c7d9437a76d923e265c88f8e99a4ac9643c2f989f4b9791259ca17128f0480191449e2b412821a1b9a7 + debug: "npm:^4.4.3" + token-types: "npm:^6.1.1" + checksum: 10/27d58757e1a6c004e86f8a5f1a40fe47cb48aa6891864d03de6eab27d42fafc1456f396bc8bc300e16913b0a85f42034d011db0213d17e544ed201a7fc24244e languageName: node linkType: hard @@ -7626,13 +7914,6 @@ __metadata: languageName: node linkType: hard -"@trysound/sax@npm:0.2.0": - version: 0.2.0 - resolution: "@trysound/sax@npm:0.2.0" - checksum: 10/7379713eca480ac0d9b6c7b063e06b00a7eac57092354556c81027066eb65b61ea141a69d0cc2e15d32e05b2834d4c9c2184793a5e36bbf5daf05ee5676af18c - languageName: node - linkType: hard - "@tsconfig/node10@npm:^1.0.7": version: 1.0.9 resolution: "@tsconfig/node10@npm:1.0.9" @@ -7690,6 +7971,15 @@ __metadata: languageName: node linkType: hard +"@tybys/wasm-util@npm:^0.10.0": + version: 0.10.1 + resolution: "@tybys/wasm-util@npm:0.10.1" + dependencies: + tslib: "npm:^2.4.0" + checksum: 10/7fe0d239397aebb002ac4855d30c197c06a05ea8df8511350a3a5b1abeefe26167c60eda8a5508337571161e4c4b53d7c1342296123f9607af8705369de9fa7f + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.9.0": version: 0.9.0 resolution: "@tybys/wasm-util@npm:0.9.0" @@ -7717,7 +8007,7 @@ __metadata: languageName: node linkType: hard -"@types/amqplib@npm:^0.10.6": +"@types/amqplib@npm:0.10.6": version: 0.10.6 resolution: "@types/amqplib@npm:0.10.6" dependencies: @@ -7733,7 +8023,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.20.5": +"@types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" dependencies: @@ -7765,7 +8055,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.0.6": +"@types/babel__traverse@npm:*": version: 7.20.2 resolution: "@types/babel__traverse@npm:7.20.2" dependencies: @@ -7793,7 +8083,7 @@ __metadata: languageName: node linkType: hard -"@types/bootstrap@npm:^5": +"@types/bootstrap@npm:^5.2.10": version: 5.2.10 resolution: "@types/bootstrap@npm:5.2.10" dependencies: @@ -8194,10 +8484,10 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:1.0.6, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6": - version: 1.0.6 - resolution: "@types/estree@npm:1.0.6" - checksum: 10/9d35d475095199c23e05b431bcdd1f6fec7380612aed068b14b2a08aa70494de8a9026765a5a91b1073f636fb0368f6d8973f518a31391d519e20c59388ed88d +"@types/estree@npm:*, @types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.6, @types/estree@npm:^1.0.8": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: 10/25a4c16a6752538ffde2826c2cc0c6491d90e69cd6187bef4a006dd2c3c45469f049e643d7e516c515f21484dc3d48fd5c870be158a5beb72f5baf3dc43e4099 languageName: node linkType: hard @@ -8239,36 +8529,6 @@ __metadata: languageName: node linkType: hard -"@types/glob@npm:*": - version: 8.1.0 - resolution: "@types/glob@npm:8.1.0" - dependencies: - "@types/minimatch": "npm:^5.1.2" - "@types/node": "npm:*" - checksum: 10/9101f3a9061e40137190f70626aa0e202369b5ec4012c3fabe6f5d229cce04772db9a94fa5a0eb39655e2e4ad105c38afbb4af56a56c0996a8c7d4fc72350e3d - languageName: node - linkType: hard - -"@types/got@npm:^9.6.12": - version: 9.6.12 - resolution: "@types/got@npm:9.6.12" - dependencies: - "@types/node": "npm:*" - "@types/tough-cookie": "npm:*" - form-data: "npm:^2.5.0" - checksum: 10/21d300355d0ce460490659763fa761b79d8ca381c0fce3fcc98002ace7e43abe7806aed5905cbf3b1e754aec079afc08417ae489d0a8dc63a430d978b16e04b3 - languageName: node - linkType: hard - -"@types/graceful-fs@npm:^4.1.3": - version: 4.1.6 - resolution: "@types/graceful-fs@npm:4.1.6" - dependencies: - "@types/node": "npm:*" - checksum: 10/c3070ccdc9ca0f40df747bced1c96c71a61992d6f7c767e8fd24bb6a3c2de26e8b84135ede000b7e79db530a23e7e88dcd9db60eee6395d0f4ce1dae91369dd4 - languageName: node - linkType: hard - "@types/gtag.js@npm:^0.0.12": version: 0.0.12 resolution: "@types/gtag.js@npm:0.0.12" @@ -8329,10 +8589,10 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": - version: 2.0.4 - resolution: "@types/istanbul-lib-coverage@npm:2.0.4" - checksum: 10/a25d7589ee65c94d31464c16b72a9dc81dfa0bea9d3e105ae03882d616e2a0712a9c101a599ec482d297c3591e16336962878cb3eb1a0a62d5b76d277a890ce7 +"@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.6": + version: 2.0.6 + resolution: "@types/istanbul-lib-coverage@npm:2.0.6" + checksum: 10/3feac423fd3e5449485afac999dcfcb3d44a37c830af898b689fadc65d26526460bedb889db278e0d4d815a670331796494d073a10ee6e3a6526301fe7415778 languageName: node linkType: hard @@ -8345,37 +8605,37 @@ __metadata: languageName: node linkType: hard -"@types/istanbul-reports@npm:^3.0.0": - version: 3.0.1 - resolution: "@types/istanbul-reports@npm:3.0.1" +"@types/istanbul-reports@npm:^3.0.0, @types/istanbul-reports@npm:^3.0.4": + version: 3.0.4 + resolution: "@types/istanbul-reports@npm:3.0.4" dependencies: "@types/istanbul-lib-report": "npm:*" - checksum: 10/f1ad54bc68f37f60b30c7915886b92f86b847033e597f9b34f2415acdbe5ed742fa559a0a40050d74cdba3b6a63c342cac1f3a64dba5b68b66a6941f4abd7903 + checksum: 10/93eb18835770b3431f68ae9ac1ca91741ab85f7606f310a34b3586b5a34450ec038c3eed7ab19266635499594de52ff73723a54a72a75b9f7d6a956f01edee95 languageName: node linkType: hard -"@types/jest@npm:^29.5.14": - version: 29.5.14 - resolution: "@types/jest@npm:29.5.14" +"@types/jest@npm:^30.0.0": + version: 30.0.0 + resolution: "@types/jest@npm:30.0.0" dependencies: - expect: "npm:^29.0.0" - pretty-format: "npm:^29.0.0" - checksum: 10/59ec7a9c4688aae8ee529316c43853468b6034f453d08a2e1064b281af9c81234cec986be796288f1bbb29efe943bc950e70c8fa8faae1e460d50e3cf9760f9b + expect: "npm:^30.0.0" + pretty-format: "npm:^30.0.0" + checksum: 10/cdeaa924c68b5233d9ff92861a89e7042df2b0f197633729bcf3a31e65bd4e9426e751c5665b5ac2de0b222b33f100a5502da22aefce3d2c62931c715e88f209 languageName: node linkType: hard -"@types/jsdom@npm:^20.0.0": - version: 20.0.1 - resolution: "@types/jsdom@npm:20.0.1" +"@types/jsdom@npm:^21.1.7": + version: 21.1.7 + resolution: "@types/jsdom@npm:21.1.7" dependencies: "@types/node": "npm:*" "@types/tough-cookie": "npm:*" parse5: "npm:^7.0.0" - checksum: 10/15fbb9a0bfb4a5845cf6e795f2fd12400aacfca53b8c7e5bca4a3e5e8fa8629f676327964d64258aefb127d2d8a2be86dad46359efbfca0e8c9c2b790e7f8a88 + checksum: 10/a5ee54aec813ac928ef783f69828213af4d81325f584e1fe7573a9ae139924c40768d1d5249237e62d51b9a34ed06bde059c86c6b0248d627457ec5e5d532dfa languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 @@ -8407,9 +8667,9 @@ __metadata: languageName: node linkType: hard -"@types/koa@npm:*, @types/koa@npm:^3.0.0": - version: 3.0.0 - resolution: "@types/koa@npm:3.0.0" +"@types/koa@npm:*, @types/koa@npm:^3.0.1": + version: 3.0.1 + resolution: "@types/koa@npm:3.0.1" dependencies: "@types/accepts": "npm:*" "@types/content-disposition": "npm:*" @@ -8419,23 +8679,23 @@ __metadata: "@types/keygrip": "npm:*" "@types/koa-compose": "npm:*" "@types/node": "npm:*" - checksum: 10/db461810b71afe7b73dc849c0832669d1d838edd15b81b0c07d37d32545620dfb0efc19cc120485f5e06a805a151cd89696eb53a5bfad8e223d7e593080069cb + checksum: 10/b1581d31d562bb5d9f61bc0148652abffc701c39930eb77a57b7d1f43aaad56ffa1970f6f3d4d6a0a56395a6832e2711a3278850dcc5a6c986ba6ed2cd0f4f1f languageName: node linkType: hard -"@types/koa__router@npm:^12.0.4": - version: 12.0.4 - resolution: "@types/koa__router@npm:12.0.4" +"@types/koa__router@npm:^12.0.5": + version: 12.0.5 + resolution: "@types/koa__router@npm:12.0.5" dependencies: "@types/koa": "npm:*" - checksum: 10/c01311980bf9a921b77cca5a93cc85522a6d13fe49575e6190fa80407a60237e7351d99a399316dda3119641d498f5d8236b905cd3b4f54fad2c0839ab655dd4 + checksum: 10/c619137a2871835b5918ea67b15f2e01052ae94c8de4d27f8b26b366cddd543fa1c623c6588a839dfcbd45ca961c78bfb46c4f824de2c7c3c2cdcd491d3c7170 languageName: node linkType: hard -"@types/lodash@npm:^4.14.168": - version: 4.14.198 - resolution: "@types/lodash@npm:4.14.198" - checksum: 10/2bd7e82245cf0c66169ed074a2e625da644335a29f65c0c37d501cf66d09d8a0e92408e9e0ce4ee5133343e5b27267e6a132ca38a9ded837d4341be8a3cf8008 +"@types/lodash@npm:^4.17.7": + version: 4.17.23 + resolution: "@types/lodash@npm:4.17.23" + checksum: 10/05935534a44aadef67c2158b2fb4a042a226970088106a40ddc67e4f063783149fe5cf02279d7dd4a1e72c98d9189b9430face659645dbf77270f8c4c3e387f5 languageName: node linkType: hard @@ -8469,13 +8729,6 @@ __metadata: languageName: node linkType: hard -"@types/minimatch@npm:^5.1.2": - version: 5.1.2 - resolution: "@types/minimatch@npm:5.1.2" - checksum: 10/94db5060d20df2b80d77b74dd384df3115f01889b5b6c40fa2dfa27cfc03a68fb0ff7c1f2a0366070263eb2e9d6bfd8c87111d4bc3ae93c3f291297c1bf56c85 - languageName: node - linkType: hard - "@types/minimist@npm:^1.2.0": version: 1.2.2 resolution: "@types/minimist@npm:1.2.2" @@ -8499,12 +8752,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:>=18.0.0, @types/node@npm:^22.10.10": - version: 22.13.1 - resolution: "@types/node@npm:22.13.1" +"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:>=18.0.0, @types/node@npm:^22.19.8": + version: 22.19.8 + resolution: "@types/node@npm:22.19.8" dependencies: - undici-types: "npm:~6.20.0" - checksum: 10/d8ba7068b0445643c0fa6e4917cdb7a90e8756a9daff8c8a332689cd5b2eaa01e4cd07de42e3cd7e6a6f465eeda803d5a1363d00b5ab3f6cea7950350a159497 + undici-types: "npm:~6.21.0" + checksum: 10/a61c68d434871d4a13496e3607502b2ff8e2ff69dca7e09228de5bea3bc95eb627d09243a8cff8e0bf9ff1fa13baaf0178531748f59ae81f0569c7a2f053bfa5 languageName: node linkType: hard @@ -8543,13 +8796,6 @@ __metadata: languageName: node linkType: hard -"@types/prettier@npm:^2.1.5": - version: 2.7.3 - resolution: "@types/prettier@npm:2.7.3" - checksum: 10/cda84c19acc3bf327545b1ce71114a7d08efbd67b5030b9e8277b347fa57b05178045f70debe1d363ff7efdae62f237260713aafc2d7217e06fc99b048a88497 - languageName: node - linkType: hard - "@types/prismjs@npm:^1.26.0": version: 1.26.3 resolution: "@types/prismjs@npm:1.26.3" @@ -8587,41 +8833,21 @@ __metadata: languageName: node linkType: hard -"@types/react-circular-progressbar@npm:^1.1.0": - version: 1.1.0 - resolution: "@types/react-circular-progressbar@npm:1.1.0" - dependencies: - react-circular-progressbar: "npm:*" - checksum: 10/8cfdad2feb1a5e8315474ec3ae4096e803431e394eddc7be4e91dbd4632e3840171efbe2ea55aeba2fbcd3302a488e4514732465f9702211ac81174a5e8b2d58 - languageName: node - linkType: hard - -"@types/react-datepicker@npm:^3.1.8": - version: 3.1.8 - resolution: "@types/react-datepicker@npm:3.1.8" - dependencies: - "@types/react": "npm:*" - date-fns: "npm:^2.0.1" - popper.js: "npm:^1.14.1" - checksum: 10/aedd4ed2f5ce1a6a53ab565fac7e50b392c0ae53bb9a666e7adcd55ddd07ae7d848fcc8dd8dcb4e591c222e2096007ff9f4225213ad48178bdb275d81cc70810 - languageName: node - linkType: hard - -"@types/react-dom@npm:^18.3.5": - version: 18.3.5 - resolution: "@types/react-dom@npm:18.3.5" +"@types/react-dom@npm:^18.3.7": + version: 18.3.7 + resolution: "@types/react-dom@npm:18.3.7" peerDependencies: "@types/react": ^18.0.0 - checksum: 10/02095b326f373867498e0eb2b5ebb60f9bd9535db0d757ea13504c4b7d75e16605cf1d43ce7a2e67893d177b51db4357cabb2842fb4257c49427d02da1a14e09 + checksum: 10/317569219366d487a3103ba1e5e47154e95a002915fdcf73a44162c48fe49c3a57fcf7f57fc6979e70d447112681e6b13c6c3c1df289db8b544df4aab2d318f3 languageName: node linkType: hard -"@types/react-router-bootstrap@npm:^0": - version: 0.26.6 - resolution: "@types/react-router-bootstrap@npm:0.26.6" +"@types/react-router-bootstrap@npm:^0.26.8": + version: 0.26.8 + resolution: "@types/react-router-bootstrap@npm:0.26.8" dependencies: "@types/react": "npm:*" - checksum: 10/9a1d419c0b74186d1fa1795da77cb675725356d51fec03a40a436db8fddc0030eba6a18470cde038c8cacf758d7bad98e44f2dc132b22801c2ed34621022a82d + checksum: 10/a67c804ab0abb4972785be59f13293ecc6dd25661cd624298ccb989a17559e0af656bbf98859c5483cc69e912c00bac5ec79651e5892d488acd06443f1b249c9 languageName: node linkType: hard @@ -8666,13 +8892,13 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:>=16.9.11, @types/react@npm:^18.3.18": - version: 18.3.18 - resolution: "@types/react@npm:18.3.18" +"@types/react@npm:*, @types/react@npm:>=16.9.11, @types/react@npm:^18.3.27": + version: 18.3.27 + resolution: "@types/react@npm:18.3.27" dependencies: "@types/prop-types": "npm:*" - csstype: "npm:^3.0.2" - checksum: 10/7fdd8b853e0d291d4138133f93f8d5c333da918e5804afcea61a923aab4bdfc9bb15eb21a5640959b452972b8715ddf10ffb12b3bd071898b9e37738636463f2 + csstype: "npm:^3.2.2" + checksum: 10/90155820a2af315cad1ff47df695f3f2f568c12ad641a7805746a6a9a9aa6c40b1374e819e50d39afe0e375a6b9160a73176cbdb4e09807262bc6fcdc06e67db languageName: node linkType: hard @@ -8710,10 +8936,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.5.8": - version: 7.5.8 - resolution: "@types/semver@npm:7.5.8" - checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 +"@types/semver@npm:^7.7.1": + version: 7.7.1 + resolution: "@types/semver@npm:7.7.1" + checksum: 10/8f09e7e6ca3ded67d78ba7a8f7535c8d9cf8ced83c52e7f3ac3c281fe8c689c3fe475d199d94390dc04fc681d51f2358b430bb7b2e21c62de24f2bee2c719068 languageName: node linkType: hard @@ -8756,22 +8982,6 @@ __metadata: languageName: node linkType: hard -"@types/sinon@npm:^10.0.20": - version: 10.0.20 - resolution: "@types/sinon@npm:10.0.20" - dependencies: - "@types/sinonjs__fake-timers": "npm:*" - checksum: 10/4c62cb8e45298ac8311e312f54e8afe9b170e79a6c1b10459e1216cc58ab66c90c9654d984e96de114003cfc62ddedb94f7e25b571e7da9b08800c9e8d864b0d - languageName: node - linkType: hard - -"@types/sinonjs__fake-timers@npm:*": - version: 8.1.5 - resolution: "@types/sinonjs__fake-timers@npm:8.1.5" - checksum: 10/3a0b285fcb8e1eca435266faa27ffff206608b69041022a42857274e44d9305822e85af5e7a43a9fae78d2ab7dc0fcb49f3ae3bda1fa81f0203064dbf5afd4f6 - languageName: node - linkType: hard - "@types/sockjs@npm:^0.3.36": version: 0.3.36 resolution: "@types/sockjs@npm:0.3.36" @@ -8781,10 +8991,10 @@ __metadata: languageName: node linkType: hard -"@types/stack-utils@npm:^2.0.0": - version: 2.0.1 - resolution: "@types/stack-utils@npm:2.0.1" - checksum: 10/205fdbe3326b7046d7eaf5e494d8084f2659086a266f3f9cf00bccc549c8e36e407f88168ad4383c8b07099957ad669f75f2532ed4bc70be2b037330f7bae019 +"@types/stack-utils@npm:^2.0.3": + version: 2.0.3 + resolution: "@types/stack-utils@npm:2.0.3" + checksum: 10/72576cc1522090fe497337c2b99d9838e320659ac57fa5560fcbdcbafcf5d0216c6b3a0a8a4ee4fdb3b1f5e3420aa4f6223ab57b82fef3578bec3206425c6cf5 languageName: node linkType: hard @@ -8837,7 +9047,7 @@ __metadata: languageName: node linkType: hard -"@types/w3c-web-hid@npm:^1.0.3": +"@types/w3c-web-hid@npm:^1.0.6": version: 1.0.6 resolution: "@types/w3c-web-hid@npm:1.0.6" checksum: 10/14773befa9c458b3459cdb530a8269937e623e6b72c6bd2d7f88b42f8d47c02d8a64ddc98f79c81c930b6eadf1dc1c94917b553ead72acc13c8406f65310c85d @@ -8892,6 +9102,15 @@ __metadata: languageName: node linkType: hard +"@types/yargs@npm:^17.0.33": + version: 17.0.35 + resolution: "@types/yargs@npm:17.0.35" + dependencies: + "@types/yargs-parser": "npm:*" + checksum: 10/47bcd4476a4194ea11617ea71cba8a1eddf5505fc39c44336c1a08d452a0de4486aedbc13f47a017c8efbcb5a8aa358d976880663732ebcbc6dbcbbecadb0581 + languageName: node + linkType: hard + "@types/yargs@npm:^17.0.8": version: 17.0.24 resolution: "@types/yargs@npm:17.0.24" @@ -8910,122 +9129,280 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:8.30.1": - version: 8.30.1 - resolution: "@typescript-eslint/eslint-plugin@npm:8.30.1" +"@typescript-eslint/eslint-plugin@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/eslint-plugin@npm:8.54.0" dependencies: - "@eslint-community/regexpp": "npm:^4.10.0" - "@typescript-eslint/scope-manager": "npm:8.30.1" - "@typescript-eslint/type-utils": "npm:8.30.1" - "@typescript-eslint/utils": "npm:8.30.1" - "@typescript-eslint/visitor-keys": "npm:8.30.1" - graphemer: "npm:^1.4.0" - ignore: "npm:^5.3.1" + "@eslint-community/regexpp": "npm:^4.12.2" + "@typescript-eslint/scope-manager": "npm:8.54.0" + "@typescript-eslint/type-utils": "npm:8.54.0" + "@typescript-eslint/utils": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" + ignore: "npm:^7.0.5" natural-compare: "npm:^1.4.0" - ts-api-utils: "npm:^2.0.1" + ts-api-utils: "npm:^2.4.0" peerDependencies: - "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 + "@typescript-eslint/parser": ^8.54.0 eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.9.0" - checksum: 10/769b0365c1eda5d15ecb24cd297ca60d264001d46e14f42fae30f6f519610414726885a8d5cf57ef5a01484f92166104a74fb2ca2fd2af28f11cab149b6de591 + typescript: ">=4.8.4 <6.0.0" + checksum: 10/8f1c74ac77d7a84ae3f201bb09cb67271662befed036266af1eaa0653d09b545353441640516c1c86e0a94939887d32f0473c61a642488b14d46533742bfbd1b languageName: node linkType: hard -"@typescript-eslint/parser@npm:8.30.1": - version: 8.30.1 - resolution: "@typescript-eslint/parser@npm:8.30.1" +"@typescript-eslint/parser@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/parser@npm:8.54.0" dependencies: - "@typescript-eslint/scope-manager": "npm:8.30.1" - "@typescript-eslint/types": "npm:8.30.1" - "@typescript-eslint/typescript-estree": "npm:8.30.1" - "@typescript-eslint/visitor-keys": "npm:8.30.1" - debug: "npm:^4.3.4" + "@typescript-eslint/scope-manager": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" + debug: "npm:^4.4.3" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.9.0" - checksum: 10/ffff7bfa7e6b0233feb2d2c9bc27e0fd16faa50a00e9853efcc59de312420ef5a54b94833e80727bc5c966c1b211d70601c2337e33cc5610fa2f28d858642f5b + typescript: ">=4.8.4 <6.0.0" + checksum: 10/d2e09462c9966ef3deeba71d9e41d1d4876c61eea65888c93a3db6fba48b89a2165459c6519741d40e969da05ed98d3f4c87a7f56c5521ab5699743cc315f6cb languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.30.1": - version: 8.30.1 - resolution: "@typescript-eslint/scope-manager@npm:8.30.1" +"@typescript-eslint/project-service@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/project-service@npm:8.54.0" dependencies: - "@typescript-eslint/types": "npm:8.30.1" - "@typescript-eslint/visitor-keys": "npm:8.30.1" - checksum: 10/ecae69888a06126d57f3ac2db9935199b708406e8cd84e0918dd8302f31771145d62b52bf3c454be43c5aa4f93685d3f8c15b118d0de1c0323e02113c127aa66 + "@typescript-eslint/tsconfig-utils": "npm:^8.54.0" + "@typescript-eslint/types": "npm:^8.54.0" + debug: "npm:^4.4.3" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10/93f0483f6bbcf7cf776a53a130f7606f597fba67cf111e1897873bf1531efaa96e4851cfd461da0f0cc93afbdb51e47bcce11cf7dd4fb68b7030c7f9f240b92f languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:8.30.1": - version: 8.30.1 - resolution: "@typescript-eslint/type-utils@npm:8.30.1" +"@typescript-eslint/scope-manager@npm:8.54.0, @typescript-eslint/scope-manager@npm:^8.51.0": + version: 8.54.0 + resolution: "@typescript-eslint/scope-manager@npm:8.54.0" dependencies: - "@typescript-eslint/typescript-estree": "npm:8.30.1" - "@typescript-eslint/utils": "npm:8.30.1" - debug: "npm:^4.3.4" - ts-api-utils: "npm:^2.0.1" + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" + checksum: 10/3474f3197e8647754393dee62b3145c9de71eaa66c8a68f61c8283aa332141803885db9c96caa6a51f78128ad9ef92f774a90361655e57bd951d5b57eb76f914 + languageName: node + linkType: hard + +"@typescript-eslint/tsconfig-utils@npm:8.54.0, @typescript-eslint/tsconfig-utils@npm:^8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.54.0" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10/e9d6b29538716f007919bfcee94f09b7f8e7d2b684ad43d1a3c8d43afb9f0539c7707f84a34f42054e31c8c056b0ccf06575d89e860b4d34632ffefaefafe1fc + languageName: node + linkType: hard + +"@typescript-eslint/type-utils@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/type-utils@npm:8.54.0" + dependencies: + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" + "@typescript-eslint/utils": "npm:8.54.0" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.4.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.9.0" - checksum: 10/c7a285bae7806a1e4aa9840feb727fe47f5de4ef3d68ecd1bbebc593a72ec08df17953098d71dc83a6936a42d5a44bcd4a49e6f067ec0947293795b0a389498f + typescript: ">=4.8.4 <6.0.0" + checksum: 10/60e92fb32274abd70165ce6f4187e4cffa55416374c63731d7de8fdcfb7a558b4dd48909ff1ad38ac39d2ea1248ec54d6ce38dbc065fd34529a217fc2450d5b1 languageName: node linkType: hard -"@typescript-eslint/types@npm:8.30.1": - version: 8.30.1 - resolution: "@typescript-eslint/types@npm:8.30.1" - checksum: 10/342ec75ba2c596ffaa93612c6c6afd2b0a05c346bdfa73ac208b49f1969b48a3f739f306431f9a10cf34e99e8585ca924fdde7f9508dd7869142b25f399d6bd6 +"@typescript-eslint/types@npm:8.54.0, @typescript-eslint/types@npm:^8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/types@npm:8.54.0" + checksum: 10/c25cc0bdf90fb150cf6ce498897f43fe3adf9e872562159118f34bd91a9bfab5f720cb1a41f3cdf253b2e840145d7d372089b7cef5156624ef31e98d34f91b31 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:8.30.1": - version: 8.30.1 - resolution: "@typescript-eslint/typescript-estree@npm:8.30.1" +"@typescript-eslint/typescript-estree@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.54.0" dependencies: - "@typescript-eslint/types": "npm:8.30.1" - "@typescript-eslint/visitor-keys": "npm:8.30.1" - debug: "npm:^4.3.4" - fast-glob: "npm:^3.3.2" - is-glob: "npm:^4.0.3" - minimatch: "npm:^9.0.4" - semver: "npm:^7.6.0" - ts-api-utils: "npm:^2.0.1" + "@typescript-eslint/project-service": "npm:8.54.0" + "@typescript-eslint/tsconfig-utils": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/visitor-keys": "npm:8.54.0" + debug: "npm:^4.4.3" + minimatch: "npm:^9.0.5" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.4.0" peerDependencies: - typescript: ">=4.8.4 <5.9.0" - checksum: 10/60c307fbb8ec86d28e4b2237b624427b7aee737bced82e5f94acc84229eae907e7742ccf0c9c0825326b3ccb9f72b14075893d90e06c28f8ce2fd04502c0b410 + typescript: ">=4.8.4 <6.0.0" + checksum: 10/3a545037c6f9319251d3ba44cf7a3216b1372422469e27f7ed3415244ebf42553da1ab4644da42d3f0ae2706a8cad12529ffebcb2e75406f74e3b30b812d010d languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.30.1, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.21.0": - version: 8.30.1 - resolution: "@typescript-eslint/utils@npm:8.30.1" +"@typescript-eslint/utils@npm:8.54.0, @typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.21.0, @typescript-eslint/utils@npm:^8.51.0": + version: 8.54.0 + resolution: "@typescript-eslint/utils@npm:8.54.0" dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.30.1" - "@typescript-eslint/types": "npm:8.30.1" - "@typescript-eslint/typescript-estree": "npm:8.30.1" + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.54.0" + "@typescript-eslint/types": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.9.0" - checksum: 10/97d27d2f0bce6f60a1857d511dba401f076766477a2896405aca52e860f9c5460111299f6e17642e18e578be1dbf850a0b1202ba61aa65d6a52646429ff9c99c + typescript: ">=4.8.4 <6.0.0" + checksum: 10/9f88a2a7ab3e11aa0ff7f99c0e66a0cf2cba10b640def4c64a4f4ef427fecfb22f28dbe5697535915eb01f6507515ac43e45e0ff384bf82856e3420194d9ffdd languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.30.1": - version: 8.30.1 - resolution: "@typescript-eslint/visitor-keys@npm:8.30.1" +"@typescript-eslint/visitor-keys@npm:8.54.0": + version: 8.54.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.54.0" dependencies: - "@typescript-eslint/types": "npm:8.30.1" - eslint-visitor-keys: "npm:^4.2.0" - checksum: 10/0c08169123ebca4ab04464486a7f41093ba77e75fb088e2c8af9f36bb4c0f785d4e82940f6b62e47457d4758fa57a53423db4226250d6eb284e75a3f96f03f2b + "@typescript-eslint/types": "npm:8.54.0" + eslint-visitor-keys: "npm:^4.2.1" + checksum: 10/cca5380ee30250302ee1459e5a0a38de8c16213026dbbff3d167fa7d71d012f31d60ac4483ad45ebd13f2ac963d1ca52dd5f22759a68d4ee57626e421769187a languageName: node linkType: hard -"@ungap/structured-clone@npm:^1.0.0": - version: 1.2.0 - resolution: "@ungap/structured-clone@npm:1.2.0" - checksum: 10/c6fe89a505e513a7592e1438280db1c075764793a2397877ff1351721fe8792a966a5359769e30242b3cd023f2efb9e63ca2ca88019d73b564488cc20e3eab12 +"@ungap/structured-clone@npm:^1.0.0, @ungap/structured-clone@npm:^1.3.0": + version: 1.3.0 + resolution: "@ungap/structured-clone@npm:1.3.0" + checksum: 10/80d6910946f2b1552a2406650051c91bbd1f24a6bf854354203d84fe2714b3e8ce4618f49cc3410494173a1c1e8e9777372fe68dce74bd45faf0a7a1a6ccf448 + languageName: node + linkType: hard + +"@unrs/resolver-binding-android-arm-eabi@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-android-arm-eabi@npm:1.11.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@unrs/resolver-binding-android-arm64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-android-arm64@npm:1.11.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-darwin-arm64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-darwin-arm64@npm:1.11.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-darwin-x64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-darwin-x64@npm:1.11.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-freebsd-x64@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-freebsd-x64@npm:1.11.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm-gnueabihf@npm:1.11.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-arm-musleabihf@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm-musleabihf@npm:1.11.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-arm64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm64-gnu@npm:1.11.1" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-arm64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-arm64-musl@npm:1.11.1" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-ppc64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-ppc64-gnu@npm:1.11.1" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-riscv64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-riscv64-gnu@npm:1.11.1" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-riscv64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-riscv64-musl@npm:1.11.1" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-s390x-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-s390x-gnu@npm:1.11.1" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-x64-gnu@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-x64-gnu@npm:1.11.1" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@unrs/resolver-binding-linux-x64-musl@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-linux-x64-musl@npm:1.11.1" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@unrs/resolver-binding-wasm32-wasi@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-wasm32-wasi@npm:1.11.1" + dependencies: + "@napi-rs/wasm-runtime": "npm:^0.2.11" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@unrs/resolver-binding-win32-arm64-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-arm64-msvc@npm:1.11.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@unrs/resolver-binding-win32-ia32-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-ia32-msvc@npm:1.11.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@unrs/resolver-binding-win32-x64-msvc@npm:1.11.1": + version: 1.11.1 + resolution: "@unrs/resolver-binding-win32-x64-msvc@npm:1.11.1" + conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -9036,18 +9413,38 @@ __metadata: languageName: node linkType: hard -"@vitejs/plugin-react@npm:^4.3.4": - version: 4.3.4 - resolution: "@vitejs/plugin-react@npm:4.3.4" +"@vitejs/plugin-react@npm:^5.1.3": + version: 5.1.3 + resolution: "@vitejs/plugin-react@npm:5.1.3" dependencies: - "@babel/core": "npm:^7.26.0" - "@babel/plugin-transform-react-jsx-self": "npm:^7.25.9" - "@babel/plugin-transform-react-jsx-source": "npm:^7.25.9" + "@babel/core": "npm:^7.29.0" + "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" + "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" + "@rolldown/pluginutils": "npm:1.0.0-rc.2" "@types/babel__core": "npm:^7.20.5" - react-refresh: "npm:^0.14.2" + react-refresh: "npm:^0.18.0" + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10/e431b2ea5b33f96e670ccf1c7e597bda46581f9eef5033249cf9fd74f4c3d9927a402d143568befaa22c6f98af571478c6cae84c5212e3f2a124d922d5c04f6d + languageName: node + linkType: hard + +"@vitest/eslint-plugin@npm:^1.6.6": + version: 1.6.6 + resolution: "@vitest/eslint-plugin@npm:1.6.6" + dependencies: + "@typescript-eslint/scope-manager": "npm:^8.51.0" + "@typescript-eslint/utils": "npm:^8.51.0" peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - checksum: 10/3b220908ed9b7b96a380a9c53e82fb428ca1f76b798ab59d1c63765bdff24de61b4778dd3655952b7d3d922645aea2d97644503b879aba6e3fcf467605b9913d + eslint: ">=8.57.0" + typescript: ">=5.0.0" + vitest: "*" + peerDependenciesMeta: + typescript: + optional: true + vitest: + optional: true + checksum: 10/d08d90480547435a3a2324f3138e37f65a78e4fa3b1e9a5030d5b0b2f1ee09547e7631084445586c67e1d465191116e0ef5623f9b1d8d571a3c74099fba21c55 languageName: node linkType: hard @@ -9213,10 +9610,10 @@ __metadata: languageName: node linkType: hard -"@xmldom/xmldom@npm:^0.8.10": - version: 0.8.10 - resolution: "@xmldom/xmldom@npm:0.8.10" - checksum: 10/62400bc5e0e75b90650e33a5ceeb8d94829dd11f9b260962b71a784cd014ddccec3e603fe788af9c1e839fa4648d8c521ebd80d8b752878d3a40edabc9ce7ccf +"@xmldom/xmldom@npm:^0.8.11": + version: 0.8.11 + resolution: "@xmldom/xmldom@npm:0.8.11" + checksum: 10/f6d6ffdf71cf19d9b3c10e978fad40d2f85453bf5b2aa05be8aa0c5ad13f84690c3153316729213cc652d06ec12c605ddb0aa03886f1d73d51b974b4105d31e3 languageName: node linkType: hard @@ -9281,13 +9678,6 @@ __metadata: languageName: node linkType: hard -"abab@npm:^2.0.6": - version: 2.0.6 - resolution: "abab@npm:2.0.6" - checksum: 10/ebe95d7278999e605823fc515a3b05d689bc72e7f825536e73c95ebf621636874c6de1b749b3c4bf866b96ccd4b3a2802efa313d0e45ad51a413c8c73247db20 - languageName: node - linkType: hard - "abbrev@npm:1, abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" @@ -9309,6 +9699,13 @@ __metadata: languageName: node linkType: hard +"abbrev@npm:^4.0.0": + version: 4.0.0 + resolution: "abbrev@npm:4.0.0" + checksum: 10/e2f0c6a6708ad738b3e8f50233f4800de31ad41a6cdc50e0cbe51b76fed69fd0213516d92c15ce1a9985fca71a14606a9be22bf00f8475a58987b9bfb671c582 + languageName: node + linkType: hard + "abort-controller@npm:^3.0.0": version: 3.0.0 resolution: "abort-controller@npm:3.0.0" @@ -9318,7 +9715,7 @@ __metadata: languageName: node linkType: hard -"accepts@npm:^1.3.8, accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": +"accepts@npm:^1.3.8, accepts@npm:~1.3.4, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" dependencies: @@ -9328,16 +9725,6 @@ __metadata: languageName: node linkType: hard -"acorn-globals@npm:^7.0.0": - version: 7.0.1 - resolution: "acorn-globals@npm:7.0.1" - dependencies: - acorn: "npm:^8.1.0" - acorn-walk: "npm:^8.0.2" - checksum: 10/2a2998a547af6d0db5f0cdb90acaa7c3cbca6709010e02121fb8b8617c0fbd8bab0b869579903fde358ac78454356a14fadcc1a672ecb97b04b1c2ccba955ce8 - languageName: node - linkType: hard - "acorn-import-attributes@npm:^1.9.5": version: 1.9.5 resolution: "acorn-import-attributes@npm:1.9.5" @@ -9347,6 +9734,15 @@ __metadata: languageName: node linkType: hard +"acorn-import-phases@npm:^1.0.3": + version: 1.0.4 + resolution: "acorn-import-phases@npm:1.0.4" + peerDependencies: + acorn: ^8.14.0 + checksum: 10/471050ac7d9b61909c837b426de9eeef2958997f6277ad7dea88d5894fd9b3245d8ed4a225c2ca44f814dbb20688009db7a80e525e8196fc9e98c5285b66161d + languageName: node + linkType: hard + "acorn-jsx@npm:^5.0.0, acorn-jsx@npm:^5.3.2": version: 5.3.2 resolution: "acorn-jsx@npm:5.3.2" @@ -9356,7 +9752,7 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1": +"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.1.1": version: 8.3.3 resolution: "acorn-walk@npm:8.3.3" dependencies: @@ -9365,12 +9761,12 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.0, acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.15.0, acorn@npm:^8.4.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2": - version: 8.15.0 - resolution: "acorn@npm:8.15.0" +"acorn@npm:^8.0.0, acorn@npm:^8.0.4, acorn@npm:^8.11.0, acorn@npm:^8.14.0, acorn@npm:^8.15.0, acorn@npm:^8.16.0, acorn@npm:^8.4.1": + version: 8.16.0 + resolution: "acorn@npm:8.16.0" bin: acorn: bin/acorn - checksum: 10/77f2de5051a631cf1729c090e5759148459cdb76b5f5c70f890503d629cf5052357b0ce783c0f976dd8a93c5150f59f6d18df1def3f502396a20f81282482fa4 + checksum: 10/690c673bb4d61b38ef82795fab58526471ad7f7e67c0e40c4ff1e10ecd80ce5312554ef633c9995bfc4e6d170cef165711f9ca9e49040b62c0c66fbf2dd3df2b languageName: node linkType: hard @@ -9524,26 +9920,26 @@ __metadata: linkType: hard "ajv@npm:^6.12.4, ajv@npm:^6.12.5": - version: 6.12.6 - resolution: "ajv@npm:6.12.6" + version: 6.14.0 + resolution: "ajv@npm:6.14.0" dependencies: fast-deep-equal: "npm:^3.1.1" fast-json-stable-stringify: "npm:^2.0.0" json-schema-traverse: "npm:^0.4.1" uri-js: "npm:^4.2.2" - checksum: 10/48d6ad21138d12eb4d16d878d630079a2bda25a04e745c07846a4ad768319533031e28872a9b3c5790fa1ec41aabdf2abed30a56e5a03ebc2cf92184b8ee306c + checksum: 10/c71f14dd2b6f2535d043f74019c8169f7aeb1106bafbb741af96f34fdbf932255c919ddd46344043d03b62ea0ccb319f83667ec5eedf612393f29054fe5ce4a5 languageName: node linkType: hard "ajv@npm:^8.0.0, ajv@npm:^8.11.0, ajv@npm:^8.12.0, ajv@npm:^8.17.1, ajv@npm:^8.6.3, ajv@npm:^8.9.0": - version: 8.17.1 - resolution: "ajv@npm:8.17.1" + version: 8.18.0 + resolution: "ajv@npm:8.18.0" dependencies: fast-deep-equal: "npm:^3.1.3" fast-uri: "npm:^3.0.1" json-schema-traverse: "npm:^1.0.0" require-from-string: "npm:^2.0.2" - checksum: 10/ee3c62162c953e91986c838f004132b6a253d700f1e51253b99791e2dbfdb39161bc950ebdc2f156f8568035bb5ed8be7bd78289cd9ecbf3381fe8f5b82e3f33 + checksum: 10/bfed9de827a2b27c6d4084324eda76a4e32bdde27410b3e9b81d06e6f8f5c78370fc6b93fe1d869f1939ff1d7c4ae8896960995acb8425e3e9288c8884247c48 languageName: node linkType: hard @@ -9601,7 +9997,7 @@ __metadata: languageName: node linkType: hard -"amqplib@npm:^0.10.5": +"amqplib@npm:0.10.5": version: 0.10.5 resolution: "amqplib@npm:0.10.5" dependencies: @@ -9692,14 +10088,7 @@ __metadata: languageName: node linkType: hard -"any-promise@npm:^1.0.0": - version: 1.3.0 - resolution: "any-promise@npm:1.3.0" - checksum: 10/6737469ba353b5becf29e4dc3680736b9caa06d300bda6548812a8fee63ae7d336d756f88572fa6b5219aed36698d808fa55f62af3e7e6845c7a1dc77d240edb - languageName: node - linkType: hard - -"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": +"anymatch@npm:^3.1.3, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -9984,6 +10373,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"async-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-function@npm:1.0.0" + checksum: 10/1a09379937d846f0ce7614e75071c12826945d4e417db634156bf0e4673c495989302f52186dfa9767a1d9181794554717badd193ca2bbab046ef1da741d8efd + languageName: node + linkType: hard + +"async-generator-function@npm:^1.0.0": + version: 1.0.0 + resolution: "async-generator-function@npm:1.0.0" + checksum: 10/3d49e7acbeee9e84537f4cb0e0f91893df8eba976759875ae8ee9e3d3c82f6ecdebdb347c2fad9926b92596d93cdfc78ecc988bcdf407e40433e8e8e6fe5d78e + languageName: node + linkType: hard + "async-value-promise@npm:^1.1.1": version: 1.1.1 resolution: "async-value-promise@npm:1.1.1" @@ -10032,16 +10435,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"atem-state@npm:1.2.0": - version: 1.2.0 - resolution: "atem-state@npm:1.2.0" +"atem-state@npm:1.3.0": + version: 1.3.0 + resolution: "atem-state@npm:1.3.0" dependencies: deepmerge: "npm:^4.3.1" tslib: "npm:^2.6.2" type-fest: "npm:^3.13.1" peerDependencies: - atem-connection: 3.4 - checksum: 10/9eecbc871e7e1311d05ef2a40ac620480bfef9deb93ef81ca277bd6e34700c17a6ca0a4f27d1369669ef96990745fb58baa538de388149180dbfd2394e197e02 + atem-connection: 3.7 + checksum: 10/06d1e82eb9c603c83b30007a8b972f90ba15975a069fd1696fff85e6ad70f7cb02687c8482701852864c434781177b9e40b543b015d9abb7a6dc0d6d104b11e1 languageName: node linkType: hard @@ -10086,18 +10489,29 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"axios@npm:1.9.0": - version: 1.9.0 - resolution: "axios@npm:1.9.0" +"axios@npm:1.13.2": + version: 1.13.2 + resolution: "axios@npm:1.13.2" dependencies: follow-redirects: "npm:^1.15.6" - form-data: "npm:^4.0.0" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10/ae4e06dcd18289f2fd18179256d550d27f9a53ecb2f9c59f2ccc4efd1d7151839ba8c3e0fb533dac793e4a59a576ca8689a19244dce5c396680837674a47a867 + languageName: node + linkType: hard + +"axios@npm:^1.11.0": + version: 1.13.6 + resolution: "axios@npm:1.13.6" + dependencies: + follow-redirects: "npm:^1.15.11" + form-data: "npm:^4.0.5" proxy-from-env: "npm:^1.1.0" - checksum: 10/a2f90bba56820883879f32a237e2b9ff25c250365dcafd41cec41b3406a3df334a148f90010182dfdadb4b41dc59f6f0b3e8898ff41b666d1157b5f3f4523497 + checksum: 10/a7ed83c2af3ef21d64609df0f85e76893a915a864c5934df69241001d0578082d6521a0c730bf37518ee458821b5695957cb10db9fc705f2a8996c8686ea7a89 languageName: node linkType: hard -"axios@npm:^1.12.0, axios@npm:^1.7.8": +"axios@npm:^1.12.0": version: 1.13.3 resolution: "axios@npm:1.13.3" dependencies: @@ -10108,20 +10522,32 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"babel-jest@npm:^29.7.0": - version: 29.7.0 - resolution: "babel-jest@npm:29.7.0" +"b4a@npm:^1.6.4": + version: 1.7.3 + resolution: "b4a@npm:1.7.3" + peerDependencies: + react-native-b4a: "*" + peerDependenciesMeta: + react-native-b4a: + optional: true + checksum: 10/048ddd0eeec6a75e6f8dee07d52354e759032f0ef678b556e05bf5a137d7a4102002cadb953b3fb37a635995a1013875d715d115dbafaf12bcad6528d2166054 + languageName: node + linkType: hard + +"babel-jest@npm:30.2.0, babel-jest@npm:^30.2.0": + version: 30.2.0 + resolution: "babel-jest@npm:30.2.0" dependencies: - "@jest/transform": "npm:^29.7.0" - "@types/babel__core": "npm:^7.1.14" - babel-plugin-istanbul: "npm:^6.1.1" - babel-preset-jest: "npm:^29.6.3" - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" + "@jest/transform": "npm:30.2.0" + "@types/babel__core": "npm:^7.20.5" + babel-plugin-istanbul: "npm:^7.0.1" + babel-preset-jest: "npm:30.2.0" + chalk: "npm:^4.1.2" + graceful-fs: "npm:^4.2.11" slash: "npm:^3.0.0" peerDependencies: - "@babel/core": ^7.8.0 - checksum: 10/8a0953bd813b3a8926008f7351611055548869e9a53dd36d6e7e96679001f71e65fd7dbfe253265c3ba6a4e630dc7c845cf3e78b17d758ef1880313ce8fba258 + "@babel/core": ^7.11.0 || ^8.0.0-0 + checksum: 10/4c7351a366cf8ac2b8a2e4e438867693eb9d83ed24c29c648da4576f700767aaf72a5d14337fc3f92c50b069f5025b26c7b89e3b7b867914b7cf8997fc15f095 languageName: node linkType: hard @@ -10147,41 +10573,38 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"babel-plugin-istanbul@npm:^6.1.1": - version: 6.1.1 - resolution: "babel-plugin-istanbul@npm:6.1.1" +"babel-plugin-istanbul@npm:^7.0.1": + version: 7.0.1 + resolution: "babel-plugin-istanbul@npm:7.0.1" dependencies: "@babel/helper-plugin-utils": "npm:^7.0.0" "@istanbuljs/load-nyc-config": "npm:^1.0.0" - "@istanbuljs/schema": "npm:^0.1.2" - istanbul-lib-instrument: "npm:^5.0.4" + "@istanbuljs/schema": "npm:^0.1.3" + istanbul-lib-instrument: "npm:^6.0.2" test-exclude: "npm:^6.0.0" - checksum: 10/ffd436bb2a77bbe1942a33245d770506ab2262d9c1b3c1f1da7f0592f78ee7445a95bc2efafe619dd9c1b6ee52c10033d6c7d29ddefe6f5383568e60f31dfe8d + checksum: 10/fe9f865f975aaa7a033de9ccb2b63fdcca7817266c5e98d3e02ac7ffd774c695093d215302796cb3770a71ef4574e7a9b298504c3c0c104cf4b48c8eda67b2a6 languageName: node linkType: hard -"babel-plugin-jest-hoist@npm:^29.6.3": - version: 29.6.3 - resolution: "babel-plugin-jest-hoist@npm:29.6.3" +"babel-plugin-jest-hoist@npm:30.2.0": + version: 30.2.0 + resolution: "babel-plugin-jest-hoist@npm:30.2.0" dependencies: - "@babel/template": "npm:^7.3.3" - "@babel/types": "npm:^7.3.3" - "@types/babel__core": "npm:^7.1.14" - "@types/babel__traverse": "npm:^7.0.6" - checksum: 10/9bfa86ec4170bd805ab8ca5001ae50d8afcb30554d236ba4a7ffc156c1a92452e220e4acbd98daefc12bf0216fccd092d0a2efed49e7e384ec59e0597a926d65 + "@types/babel__core": "npm:^7.20.5" + checksum: 10/360e87a9aa35f4cf208a10ba79e1821ea906f9e3399db2a9762cbc5076fd59f808e571d88b5b1106738d22e23f9ddefbb8137b2780b2abd401c8573b85c8a2f5 languageName: node linkType: hard -"babel-plugin-polyfill-corejs2@npm:^0.4.10": - version: 0.4.10 - resolution: "babel-plugin-polyfill-corejs2@npm:0.4.10" +"babel-plugin-polyfill-corejs2@npm:^0.4.10, babel-plugin-polyfill-corejs2@npm:^0.4.15": + version: 0.4.15 + resolution: "babel-plugin-polyfill-corejs2@npm:0.4.15" dependencies: - "@babel/compat-data": "npm:^7.22.6" - "@babel/helper-define-polyfill-provider": "npm:^0.6.1" + "@babel/compat-data": "npm:^7.28.6" + "@babel/helper-define-polyfill-provider": "npm:^0.6.6" semver: "npm:^6.3.1" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/9fb5e59a3235eba66fb05060b2a3ecd6923084f100df7526ab74b6272347d7adcf99e17366b82df36e592cde4e82fdb7ae24346a990eced76c7d504cac243400 + checksum: 10/e5f8a4e716400b2b5c51f7b3c0eec58da92f1d8cc1c6fe2e32555c98bc594be1de7fa1da373f8e42ab098c33867c4cc2931ce648c92aab7a4f4685417707c438 languageName: node linkType: hard @@ -10197,14 +10620,26 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"babel-plugin-polyfill-regenerator@npm:^0.6.1": - version: 0.6.1 - resolution: "babel-plugin-polyfill-regenerator@npm:0.6.1" +"babel-plugin-polyfill-corejs3@npm:^0.14.0": + version: 0.14.0 + resolution: "babel-plugin-polyfill-corejs3@npm:0.14.0" dependencies: - "@babel/helper-define-polyfill-provider": "npm:^0.6.1" + "@babel/helper-define-polyfill-provider": "npm:^0.6.6" + core-js-compat: "npm:^3.48.0" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/9df4a8e9939dd419fed3d9ea26594b4479f2968f37c225e1b2aa463001d7721f5537740e6622909d2a570b61cec23256924a1701404fc9d6fd4474d3e845cedb + checksum: 10/09c854a3bda9a930fbce4b80d52a24e5b0744fccb0c81bf8f470d62296f197a2afe111b2b9ecb0d8a47068de2f938d14b748295953377e47594b0673d53c9396 + languageName: node + linkType: hard + +"babel-plugin-polyfill-regenerator@npm:^0.6.1, babel-plugin-polyfill-regenerator@npm:^0.6.6": + version: 0.6.6 + resolution: "babel-plugin-polyfill-regenerator@npm:0.6.6" + dependencies: + "@babel/helper-define-polyfill-provider": "npm:^0.6.6" + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + checksum: 10/8de7ea32856e75784601cacf8f4e3cbf04ce1fd05d56614b08b7bbe0674d1e59e37ccaa1c7ed16e3b181a63abe5bd43a1ab0e28b8c95618a9ebf0be5e24d6b25 languageName: node linkType: hard @@ -10217,51 +10652,133 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"babel-preset-current-node-syntax@npm:^1.0.0": - version: 1.0.1 - resolution: "babel-preset-current-node-syntax@npm:1.0.1" +"babel-preset-current-node-syntax@npm:^1.2.0": + version: 1.2.0 + resolution: "babel-preset-current-node-syntax@npm:1.2.0" dependencies: "@babel/plugin-syntax-async-generators": "npm:^7.8.4" "@babel/plugin-syntax-bigint": "npm:^7.8.3" - "@babel/plugin-syntax-class-properties": "npm:^7.8.3" - "@babel/plugin-syntax-import-meta": "npm:^7.8.3" + "@babel/plugin-syntax-class-properties": "npm:^7.12.13" + "@babel/plugin-syntax-class-static-block": "npm:^7.14.5" + "@babel/plugin-syntax-import-attributes": "npm:^7.24.7" + "@babel/plugin-syntax-import-meta": "npm:^7.10.4" "@babel/plugin-syntax-json-strings": "npm:^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators": "npm:^7.10.4" "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" - "@babel/plugin-syntax-numeric-separator": "npm:^7.8.3" + "@babel/plugin-syntax-numeric-separator": "npm:^7.10.4" "@babel/plugin-syntax-object-rest-spread": "npm:^7.8.3" "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" - "@babel/plugin-syntax-top-level-await": "npm:^7.8.3" + "@babel/plugin-syntax-private-property-in-object": "npm:^7.14.5" + "@babel/plugin-syntax-top-level-await": "npm:^7.14.5" peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10/94561959cb12bfa80867c9eeeace7c3d48d61707d33e55b4c3fdbe82fc745913eb2dbfafca62aef297421b38aadcb58550e5943f50fbcebbeefd70ce2bed4b74 + "@babel/core": ^7.0.0 || ^8.0.0-0 + checksum: 10/3608fa671cfa46364ea6ec704b8fcdd7514b7b70e6ec09b1199e13ae73ed346c51d5ce2cb6d4d5b295f6a3f2cad1fdeec2308aa9e037002dd7c929194cc838ea + languageName: node + linkType: hard + +"babel-preset-jest@npm:30.2.0": + version: 30.2.0 + resolution: "babel-preset-jest@npm:30.2.0" + dependencies: + babel-plugin-jest-hoist: "npm:30.2.0" + babel-preset-current-node-syntax: "npm:^1.2.0" + peerDependencies: + "@babel/core": ^7.11.0 || ^8.0.0-beta.1 + checksum: 10/f75e155a8cf63ea1c5ca942bf757b934427630a1eeafdf861e9117879b3367931fc521da3c41fd52f8d59d705d1093ffb46c9474b3fd4d765d194bea5659d7d9 + languageName: node + linkType: hard + +"bail@npm:^2.0.0": + version: 2.0.2 + resolution: "bail@npm:2.0.2" + checksum: 10/aab4e8ccdc8d762bf3fdfce8e706601695620c0c2eda256dd85088dc0be3cfd7ff126f6e99c2bee1f24f5d418414aacf09d7f9702f16d6963df2fa488cda8824 + languageName: node + linkType: hard + +"balanced-match@npm:^1.0.0": + version: 1.0.2 + resolution: "balanced-match@npm:1.0.2" + checksum: 10/9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 + languageName: node + linkType: hard + +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 + languageName: node + linkType: hard + +"bare-events@npm:^2.5.4, bare-events@npm:^2.7.0": + version: 2.8.2 + resolution: "bare-events@npm:2.8.2" + peerDependencies: + bare-abort-controller: "*" + peerDependenciesMeta: + bare-abort-controller: + optional: true + checksum: 10/f31848ea2f5627c3a50aadfc17e518a602629f7a6671da1352975cc6c8a520441fcc9d93c0a21f8f95de65b1a5133fcd5f766d312f3d5a326dde4fe7d2fc575f + languageName: node + linkType: hard + +"bare-fs@npm:^4.0.1": + version: 4.5.3 + resolution: "bare-fs@npm:4.5.3" + dependencies: + bare-events: "npm:^2.5.4" + bare-path: "npm:^3.0.0" + bare-stream: "npm:^2.6.4" + bare-url: "npm:^2.2.2" + fast-fifo: "npm:^1.3.2" + peerDependencies: + bare-buffer: "*" + peerDependenciesMeta: + bare-buffer: + optional: true + checksum: 10/7f0d40af9182a345f3ac901ae71e08bf1db9ad27ee9799d0bd88a512b3595fdd59f712f38cfa30d85db3f8f1e491350e5277f8ac6ed3c597418e4116445701cb + languageName: node + linkType: hard + +"bare-os@npm:^3.0.1": + version: 3.6.2 + resolution: "bare-os@npm:3.6.2" + checksum: 10/11e127cdce86444be2039a28f1e25a5635f3e4ada09aeb35b33d524766b51c5f71db3dc1e8d8d88018ea5255e9f6663a55174960ca45f002132d7808b9b34e29 languageName: node linkType: hard -"babel-preset-jest@npm:^29.6.3": - version: 29.6.3 - resolution: "babel-preset-jest@npm:29.6.3" +"bare-path@npm:^3.0.0": + version: 3.0.0 + resolution: "bare-path@npm:3.0.0" dependencies: - babel-plugin-jest-hoist: "npm:^29.6.3" - babel-preset-current-node-syntax: "npm:^1.0.0" - peerDependencies: - "@babel/core": ^7.0.0 - checksum: 10/aa4ff2a8a728d9d698ed521e3461a109a1e66202b13d3494e41eea30729a5e7cc03b3a2d56c594423a135429c37bf63a9fa8b0b9ce275298be3095a88c69f6fb + bare-os: "npm:^3.0.1" + checksum: 10/712d90e9cd8c3263cc11b0e0d386d1531a452706d7840c081ee586b34b00d72544e65df7a40013d47c1b177277495225deeede65cb2984db88a979cb65aaa2ff languageName: node linkType: hard -"bail@npm:^2.0.0": - version: 2.0.2 - resolution: "bail@npm:2.0.2" - checksum: 10/aab4e8ccdc8d762bf3fdfce8e706601695620c0c2eda256dd85088dc0be3cfd7ff126f6e99c2bee1f24f5d418414aacf09d7f9702f16d6963df2fa488cda8824 +"bare-stream@npm:^2.6.4": + version: 2.7.0 + resolution: "bare-stream@npm:2.7.0" + dependencies: + streamx: "npm:^2.21.0" + peerDependencies: + bare-buffer: "*" + bare-events: "*" + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + checksum: 10/fe8f6e5a8e6d66e9210b4810060e8a25c6e78f9a8ee230c7dd2083b3ad48a79b1993e98eecc8ebd7890b336c66796da457aa8a2253bbb7a31e0e3a0f06bb1f5e languageName: node linkType: hard -"balanced-match@npm:^1.0.0": - version: 1.0.2 - resolution: "balanced-match@npm:1.0.2" - checksum: 10/9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 +"bare-url@npm:^2.2.2": + version: 2.3.2 + resolution: "bare-url@npm:2.3.2" + dependencies: + bare-path: "npm:^3.0.0" + checksum: 10/aa203d79e2dafdb47a4e3bee398cb7db5c7eabcf0b3adf1e1530a21ac69806d1ca05b3343666e3aeda9fc3568c995272deea8ae3cead77ad00f66a7e415de0ef languageName: node linkType: hard @@ -10279,12 +10796,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"baseline-browser-mapping@npm:^2.8.25": - version: 2.8.29 - resolution: "baseline-browser-mapping@npm:2.8.29" +"baseline-browser-mapping@npm:^2.9.0": + version: 2.9.19 + resolution: "baseline-browser-mapping@npm:2.9.19" bin: baseline-browser-mapping: dist/cli.js - checksum: 10/122c5841268dee007afe191cab1038118d3f513e784e36e8f69535a7924f650eb085f90ed5be97e1096619f903cc7c419c689594c767f3b2d8c4462c4e3a899d + checksum: 10/8d7bbb7fe3d1ad50e04b127c819ba6d059c01ed0d2a7a5fc3327e23a8c42855fa3a8b510550c1fe1e37916147e6a390243566d3ef85bf6130c8ddfe5cc3db530 languageName: node linkType: hard @@ -10298,9 +10815,9 @@ asn1@evs-broadcast/node-asn1: linkType: hard "basic-ftp@npm:^5.0.2": - version: 5.0.5 - resolution: "basic-ftp@npm:5.0.5" - checksum: 10/3dc56b2092b10d67e84621f5b9bbb0430469499178e857869194184d46fbdd367a9aa9fad660084388744b074b5f540e6ac8c22c0826ebba4fcc86a9d1c324e2 + version: 5.2.0 + resolution: "basic-ftp@npm:5.2.0" + checksum: 10/f5a15d789aa98859af4da9e976154b2aeae19052e1762dc68d259d2bce631dafa40c667aa06d7346cd630aa6f9cc9a26f515b468e0bd24243fbae2149c7d01ad languageName: node linkType: hard @@ -10399,16 +10916,16 @@ asn1@evs-broadcast/node-asn1: linkType: hard "bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9": - version: 4.12.0 - resolution: "bn.js@npm:4.12.0" - checksum: 10/10f8db196d3da5adfc3207d35d0a42aa29033eb33685f20ba2c36cadfe2de63dad05df0a20ab5aae01b418d1c4b3d4d205273085262fa020d17e93ff32b67527 + version: 4.12.3 + resolution: "bn.js@npm:4.12.3" + checksum: 10/57ed5a055f946f3e009f1589c45a5242db07f3dddfc72e4506f0dd9d8b145f0dbee4edabc2499288f3fc338eb712fb96a1c623a2ed2bcd49781df1a64db64dd1 languageName: node linkType: hard -"bn.js@npm:^5.0.0, bn.js@npm:^5.2.1": - version: 5.2.1 - resolution: "bn.js@npm:5.2.1" - checksum: 10/7a7e8764d7a6e9708b8b9841b2b3d6019cc154d2fc23716d0efecfe1e16921b7533c6f7361fb05471eab47986c4aa310c270f88e3507172104632ac8df2cfd84 +"bn.js@npm:^5.2.1, bn.js@npm:^5.2.2": + version: 5.2.3 + resolution: "bn.js@npm:5.2.3" + checksum: 10/dfb3927e0d531e6ec4f191597ce6f7f7665310c356fef5f968ada676b8058027f959af42eaa37b5f5c63617e819d3741813025ab15dd71a90f2e74698df0b58e languageName: node linkType: hard @@ -10449,12 +10966,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"bootstrap@npm:^5.3.3": - version: 5.3.3 - resolution: "bootstrap@npm:5.3.3" +"bootstrap@npm:^5.3.8": + version: 5.3.8 + resolution: "bootstrap@npm:5.3.8" peerDependencies: "@popperjs/core": ^2.11.8 - checksum: 10/f05183948b00b496400cc13df5798ecab7a85975e7d9a77b314a763b574a990aec0f1bbf1913c648a93b5d8cc82e73bc05f5ec1161d2932aad7ef7f316d9c82d + checksum: 10/ca36e1816940ee424b91f3a534e7a359c1f180da00e92c650b92a9c2621a1ca24ee71a0886666675718b58527f9193cd0ead934da7a92108224013fa07bec2d2 languageName: node linkType: hard @@ -10500,7 +11017,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"brace-expansion@npm:^2.0.1": +"brace-expansion@npm:^2.0.1, brace-expansion@npm:^2.0.2": version: 2.0.2 resolution: "brace-expansion@npm:2.0.2" dependencies: @@ -10509,6 +11026,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"brace-expansion@npm:^5.0.2": + version: 5.0.4 + resolution: "brace-expansion@npm:5.0.4" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10/cfd57e20d8ded9578149e47ae4d3fff2b2f78d06b54a32a73057bddff65c8e9b930613f0cbcfefedf12dd117151e19d4da16367d5127c54f3bff02d8a4479bb2 + languageName: node + linkType: hard + "braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -10557,7 +11083,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"browserify-cipher@npm:^1.0.0": +"browserify-cipher@npm:^1.0.1": version: 1.0.1 resolution: "browserify-cipher@npm:1.0.1" dependencies: @@ -10580,31 +11106,31 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.1.0": - version: 4.1.0 - resolution: "browserify-rsa@npm:4.1.0" +"browserify-rsa@npm:^4.0.0, browserify-rsa@npm:^4.1.1": + version: 4.1.1 + resolution: "browserify-rsa@npm:4.1.1" dependencies: - bn.js: "npm:^5.0.0" - randombytes: "npm:^2.0.1" - checksum: 10/155f0c135873efc85620571a33d884aa8810e40176125ad424ec9d85016ff105a07f6231650914a760cca66f29af0494087947b7be34880dd4599a0cd3c38e54 + bn.js: "npm:^5.2.1" + randombytes: "npm:^2.1.0" + safe-buffer: "npm:^5.2.1" + checksum: 10/62ae0da60e49e8d5dd3b0922119b6edee94ebfa3a184211c804024b3a75f9dab31a1d124cc0545ed050e273f0325c2fd7aba6a51e44ba6f726fceae3210ddade languageName: node linkType: hard -"browserify-sign@npm:^4.0.0": - version: 4.2.3 - resolution: "browserify-sign@npm:4.2.3" +"browserify-sign@npm:^4.2.3": + version: 4.2.5 + resolution: "browserify-sign@npm:4.2.5" dependencies: - bn.js: "npm:^5.2.1" - browserify-rsa: "npm:^4.1.0" + bn.js: "npm:^5.2.2" + browserify-rsa: "npm:^4.1.1" create-hash: "npm:^1.2.0" create-hmac: "npm:^1.1.7" - elliptic: "npm:^6.5.5" - hash-base: "npm:~3.0" + elliptic: "npm:^6.6.1" inherits: "npm:^2.0.4" - parse-asn1: "npm:^5.1.7" + parse-asn1: "npm:^5.1.9" readable-stream: "npm:^2.3.8" safe-buffer: "npm:^5.2.1" - checksum: 10/403a8061d229ae31266670345b4a7c00051266761d2c9bbeb68b1a9bcb05f68143b16110cf23a171a5d6716396a1f41296282b3e73eeec0a1871c77f0ff4ee6b + checksum: 10/ccfe54ab61b8e01e84c507b60912f9ae8701f4e53accc3d85c3773db13f14c51f17b684167735d28c59aaf5523ee59c66cc831ddc178bc7f598257e590ca1a35 languageName: node linkType: hard @@ -10617,18 +11143,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.23.0, browserslist@npm:^4.24.0, browserslist@npm:^4.24.3, browserslist@npm:^4.26.0, browserslist@npm:^4.27.0": - version: 4.28.0 - resolution: "browserslist@npm:4.28.0" +"browserslist@npm:^4.0.0, browserslist@npm:^4.23.0, browserslist@npm:^4.24.0, browserslist@npm:^4.26.0, browserslist@npm:^4.27.0, browserslist@npm:^4.28.1": + version: 4.28.1 + resolution: "browserslist@npm:4.28.1" dependencies: - baseline-browser-mapping: "npm:^2.8.25" - caniuse-lite: "npm:^1.0.30001754" - electron-to-chromium: "npm:^1.5.249" + baseline-browser-mapping: "npm:^2.9.0" + caniuse-lite: "npm:^1.0.30001759" + electron-to-chromium: "npm:^1.5.263" node-releases: "npm:^2.0.27" - update-browserslist-db: "npm:^1.1.4" + update-browserslist-db: "npm:^1.2.0" bin: browserslist: cli.js - checksum: 10/59dc88f8d950e44a064361cb874f486e532a8ba932e0cf549aee8b36dd2b791da2bc11f36c1cf820ebb9c1f3250b100f8c56364dd6e86dbc90495af424100e19 + checksum: 10/64f2a97de4bce8473c0e5ae0af8d76d1ead07a5b05fc6bc87b848678bb9c3a91ae787b27aa98cdd33fc00779607e6c156000bed58fefb9cf8e4c5a183b994cdb languageName: node linkType: hard @@ -10650,10 +11176,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"bson@npm:^6.10.1": - version: 6.10.2 - resolution: "bson@npm:6.10.2" - checksum: 10/c729cf609bf96ee3ab8edbd1c5117bfc2f7ea33eb45a49aeeda8144a9d5616bfee6ad78d4b591757151acddaedcf11dc82c0ad6c0712270221cf340da4006962 +"bson@npm:^6.10.4": + version: 6.10.4 + resolution: "bson@npm:6.10.4" + checksum: 10/8a79a452219a13898358a5abc93e32bc3805236334f962661da121ce15bd5cade27718ba3310ee2a143ff508489b08467eed172ecb2a658cb8d2e94fdb76b215 languageName: node linkType: hard @@ -10685,7 +11211,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"buffer@npm:^5.2.1, buffer@npm:^5.5.0, buffer@npm:^5.7.1": +"buffer@npm:^5.5.0, buffer@npm:^5.7.1": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -10810,26 +11336,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cacache@npm:^19.0.1": - version: 19.0.1 - resolution: "cacache@npm:19.0.1" - dependencies: - "@npmcli/fs": "npm:^4.0.0" - fs-minipass: "npm:^3.0.0" - glob: "npm:^10.2.2" - lru-cache: "npm:^10.0.1" - minipass: "npm:^7.0.3" - minipass-collect: "npm:^2.0.1" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - p-map: "npm:^7.0.2" - ssri: "npm:^12.0.0" - tar: "npm:^7.4.3" - unique-filename: "npm:^4.0.0" - checksum: 10/ea026b27b13656330c2bbaa462a88181dcaa0435c1c2e705db89b31d9bdf7126049d6d0445ba746dca21454a0cfdf1d6f47fd39d34c8c8435296b30bc5738a13 - languageName: node - linkType: hard - "cacache@npm:^20.0.0, cacache@npm:^20.0.1": version: 20.0.1 resolution: "cacache@npm:20.0.1" @@ -10893,13 +11399,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1": - version: 1.0.1 - resolution: "call-bind-apply-helpers@npm:1.0.1" +"call-bind-apply-helpers@npm:^1.0.0, call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" dependencies: es-errors: "npm:^1.3.0" function-bind: "npm:^1.1.2" - checksum: 10/6e30c621170e45f1fd6735e84d02ee8e02a3ab95cb109499d5308cbe5d1e84d0cd0e10b48cc43c76aa61450ae1b03a7f89c37c10fc0de8d4998b42aab0f268cc + checksum: 10/00482c1f6aa7cfb30fb1dbeb13873edf81cfac7c29ed67a5957d60635a56b2a4a480f1016ddbdb3395cc37900d46037fb965043a51c5c789ffeab4fc535d18b5 languageName: node linkType: hard @@ -10915,13 +11421,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3": - version: 1.0.3 - resolution: "call-bound@npm:1.0.3" +"call-bound@npm:^1.0.2, call-bound@npm:^1.0.3, call-bound@npm:^1.0.4": + version: 1.0.4 + resolution: "call-bound@npm:1.0.4" dependencies: - call-bind-apply-helpers: "npm:^1.0.1" - get-intrinsic: "npm:^1.2.6" - checksum: 10/c39a8245f68cdb7c1f5eea7b3b1e3a7a90084ea6efebb78ebc454d698ade2c2bb42ec033abc35f1e596d62496b6100e9f4cdfad1956476c510130e2cda03266d + call-bind-apply-helpers: "npm:^1.0.2" + get-intrinsic: "npm:^1.3.0" + checksum: 10/ef2b96e126ec0e58a7ff694db43f4d0d44f80e641370c21549ed911fecbdbc2df3ebc9bddad918d6bbdefeafb60bb3337902006d5176d72bcd2da74820991af7 languageName: node linkType: hard @@ -10967,7 +11473,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"camelcase@npm:^6.2.0": +"camelcase@npm:^6.2.0, camelcase@npm:^6.3.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 10/8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d @@ -10993,10 +11499,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001754": - version: 1.0.30001755 - resolution: "caniuse-lite@npm:1.0.30001755" - checksum: 10/67f3b87bfd8f4da6fd69df185f54ab5409171f62185a52c916e1eb2f70f853aa374b0ce75d1742cc0215ca61e4bd1da8aa5557081bb2b6bb7220bf03a19b3b6e +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001754, caniuse-lite@npm:^1.0.30001759": + version: 1.0.30001767 + resolution: "caniuse-lite@npm:1.0.30001767" + checksum: 10/786028f1b4036b0fddef29eaa7cee40549136662ed803fda3ca77b05889159ab4469a6e4469f7bb01923336db415b73a845a5522be2d7383af6e9a4784a491fc languageName: node linkType: hard @@ -11082,16 +11588,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"chalk@npm:^3.0.0": - version: 3.0.0 - resolution: "chalk@npm:3.0.0" - dependencies: - ansi-styles: "npm:^4.1.0" - supports-color: "npm:^7.1.0" - checksum: 10/37f90b31fd655fb49c2bd8e2a68aebefddd64522655d001ef417e6f955def0ed9110a867ffc878a533f2dafea5f2032433a37c8a7614969baa7f8a1cd424ddfc - languageName: node - linkType: hard - "chalk@npm:^5.0.1, chalk@npm:^5.2.0": version: 5.4.1 resolution: "chalk@npm:5.4.1" @@ -11154,13 +11650,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"chardet@npm:^0.7.0": - version: 0.7.0 - resolution: "chardet@npm:0.7.0" - checksum: 10/b0ec668fba5eeec575ed2559a0917ba41a6481f49063c8445400e476754e0957ee09e44dc032310f526182b8f1bf25e9d4ed371f74050af7be1383e06bc44952 - languageName: node - linkType: hard - "chardet@npm:^2.1.0": version: 2.1.0 resolution: "chardet@npm:2.1.0" @@ -11168,6 +11657,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"chardet@npm:^2.1.1": + version: 2.1.1 + resolution: "chardet@npm:2.1.1" + checksum: 10/d56913b65e45c5c86f331988e2ef6264c131bfeadaae098ee719bf6610546c77740e37221ffec802dde56b5e4466613a4c754786f4da6b5f6c5477243454d324 + languageName: node + linkType: hard + "cheerio-select@npm:^2.1.0": version: 2.1.0 resolution: "cheerio-select@npm:2.1.0" @@ -11250,13 +11746,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"chownr@npm:^1.1.1": - version: 1.1.4 - resolution: "chownr@npm:1.1.4" - checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d - languageName: node - linkType: hard - "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" @@ -11278,6 +11767,25 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"chromium-bidi@npm:13.0.1": + version: 13.0.1 + resolution: "chromium-bidi@npm:13.0.1" + dependencies: + mitt: "npm:^3.0.1" + zod: "npm:^3.24.1" + peerDependencies: + devtools-protocol: "*" + checksum: 10/0672b4b27cde3bec582967feb03d5472e769c1b995dd8060156839c630eeb2c9285c7d442de6503bdc93764ee0e88bf0c7e87cddec125e038ba95269d8c47c60 + languageName: node + linkType: hard + +"chrono-node@npm:^2.9.0": + version: 2.9.0 + resolution: "chrono-node@npm:2.9.0" + checksum: 10/a30bbaa67f9a127e711db6e694ee4c89292d8f533dbfdc3d7cb34f479728e02e377f682e75ad84dd4b6a16016c248a5e85fb453943b96f93f5993f5ccddc6d08 + languageName: node + linkType: hard + "ci-info@npm:^3.2.0": version: 3.8.0 resolution: "ci-info@npm:3.8.0" @@ -11285,31 +11793,39 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ci-info@npm:^4.0.0": - version: 4.1.0 - resolution: "ci-info@npm:4.1.0" - checksum: 10/546628efd04e37da3182a58b6995a3313deb86ec7c8112e22ffb644317a61296b89bbfa128219e5bfcce43d9613a434ed89907ed8e752db947f7291e0405125f +"ci-info@npm:^4.0.0, ci-info@npm:^4.2.0": + version: 4.4.0 + resolution: "ci-info@npm:4.4.0" + checksum: 10/dfded0c630267d89660c8abb988ac8395a382bdfefedcc03e3e2858523312c5207db777c239c34774e3fcff11f015477c19d2ac8a58ea58aa476614a2e64f434 languageName: node linkType: hard "cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3": - version: 1.0.4 - resolution: "cipher-base@npm:1.0.4" + version: 1.0.7 + resolution: "cipher-base@npm:1.0.7" dependencies: - inherits: "npm:^2.0.1" - safe-buffer: "npm:^5.0.1" - checksum: 10/3d5d6652ca499c3f7c5d7fdc2932a357ec1e5aa84f2ad766d850efd42e89753c97b795c3a104a8e7ae35b4e293f5363926913de3bf8181af37067d9d541ca0db + inherits: "npm:^2.0.4" + safe-buffer: "npm:^5.2.1" + to-buffer: "npm:^1.2.2" + checksum: 10/9501d2241b7968aaae74fc3db1d6a69a804e0b14117a8fd5d811edf351fcd39a1807bfd98e090a799cfe98b183fbf2e01ebb57f1239080850db07b68dcd9ba02 languageName: node linkType: hard -"cjs-module-lexer@npm:^1.0.0, cjs-module-lexer@npm:^1.2.2": +"cjs-module-lexer@npm:^1.2.2": version: 1.2.3 resolution: "cjs-module-lexer@npm:1.2.3" checksum: 10/f96a5118b0a012627a2b1c13bd2fcb92509778422aaa825c5da72300d6dcadfb47134dd2e9d97dfa31acd674891dd91642742772d19a09a8adc3e56bd2f5928c languageName: node linkType: hard -"classnames@npm:*, classnames@npm:^2.2.1, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.1, classnames@npm:^2.3.2, classnames@npm:^2.5.1": +"cjs-module-lexer@npm:^2.1.0": + version: 2.2.0 + resolution: "cjs-module-lexer@npm:2.2.0" + checksum: 10/fc8eb5c1919504366d8260a150d93c4e857740e770467dc59ca0cc34de4b66c93075559a5af65618f359187866b1be40e036f4e1a1bab2f1e06001c216415f74 + languageName: node + linkType: hard + +"classnames@npm:*, classnames@npm:^2.2.1, classnames@npm:^2.2.5, classnames@npm:^2.3.1, classnames@npm:^2.3.2, classnames@npm:^2.5.1": version: 2.5.1 resolution: "classnames@npm:2.5.1" checksum: 10/58eb394e8817021b153bb6e7d782cfb667e4ab390cb2e9dac2fc7c6b979d1cc2b2a733093955fc5c94aa79ef5c8c89f11ab77780894509be6afbb91dddd79d15 @@ -11339,19 +11855,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cli-color@npm:^2.0.0": - version: 2.0.3 - resolution: "cli-color@npm:2.0.3" - dependencies: - d: "npm:^1.0.1" - es5-ext: "npm:^0.10.61" - es6-iterator: "npm:^2.0.3" - memoizee: "npm:^0.4.15" - timers-ext: "npm:^0.1.7" - checksum: 10/35244ba10cd7e5e38df02fbe54128dd11362f0114fdcaf44ee5a59c6af8b7680258fee4954de114cc3f824ed5bf7337270098b15e05bde6ae3877a4f67558b41 - languageName: node - linkType: hard - "cli-cursor@npm:3.1.0, cli-cursor@npm:^3.1.0": version: 3.1.0 resolution: "cli-cursor@npm:3.1.0" @@ -11495,14 +11998,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"collect-v8-coverage@npm:^1.0.0": - version: 1.0.2 - resolution: "collect-v8-coverage@npm:1.0.2" - checksum: 10/30ea7d5c9ee51f2fdba4901d4186c5b7114a088ef98fd53eda3979da77eed96758a2cae81cc6d97e239aaea6065868cf908b24980663f7b7e96aa291b3e12fa4 +"collect-v8-coverage@npm:^1.0.2": + version: 1.0.3 + resolution: "collect-v8-coverage@npm:1.0.3" + checksum: 10/656443261fb7b79cf79e89cba4b55622b07c1d4976c630829d7c5c585c73cda1c2ff101f316bfb19bb9e2c58d724c7db1f70a21e213dcd14099227c5e6019860 languageName: node linkType: hard -"color-convert@npm:^1.9.0, color-convert@npm:^1.9.3": +"color-convert@npm:^1.9.0": version: 1.9.3 resolution: "color-convert@npm:1.9.3" dependencies: @@ -11520,6 +12023,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"color-convert@npm:^3.1.3": + version: 3.1.3 + resolution: "color-convert@npm:3.1.3" + dependencies: + color-name: "npm:^2.0.0" + checksum: 10/36b9b99c138f90eb11a28d1ad911054a9facd6cffde4f00dc49a34ebde7cae28454b2285ede64f273b6a8df9c3228b80e4352f4471978fa8b5005fe91341a67b + languageName: node + linkType: hard + "color-name@npm:1.1.3": version: 1.1.3 resolution: "color-name@npm:1.1.3" @@ -11527,20 +12039,26 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"color-name@npm:^1.0.0, color-name@npm:~1.1.4": +"color-name@npm:^2.0.0": + version: 2.1.0 + resolution: "color-name@npm:2.1.0" + checksum: 10/eb014f71d87408e318e95d3f554f188370d354ba8e0ffa4341d0fd19de391bfe2bc96e563d4f6614644d676bc24f475560dffee3fe310c2d6865d007410a9a2b + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" checksum: 10/b0445859521eb4021cd0fb0cc1a75cecf67fceecae89b63f62b201cca8d345baf8b952c966862a9d9a2632987d4f6581f0ec8d957dfacece86f0a7919316f610 languageName: node linkType: hard -"color-string@npm:^1.6.0": - version: 1.9.1 - resolution: "color-string@npm:1.9.1" +"color-string@npm:^2.1.3": + version: 2.1.4 + resolution: "color-string@npm:2.1.4" dependencies: - color-name: "npm:^1.0.0" - simple-swizzle: "npm:^0.2.2" - checksum: 10/72aa0b81ee71b3f4fb1ac9cd839cdbd7a011a7d318ef58e6cb13b3708dca75c7e45029697260488709f1b1c7ac4e35489a87e528156c1e365917d1c4ccb9b9cd + color-name: "npm:^2.0.0" + checksum: 10/689a8688ac3cd55247792c83a9db9bfe675343c7412fedba1eb748ac6a8867dd2bb3d406e309ebfe90336809ee5067c7f2cccfbd10133c5cc9ef1dba5aad58f2 languageName: node linkType: hard @@ -11553,13 +12071,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"color@npm:^3.1.3": - version: 3.2.1 - resolution: "color@npm:3.2.1" +"color@npm:^5.0.2": + version: 5.0.3 + resolution: "color@npm:5.0.3" dependencies: - color-convert: "npm:^1.9.3" - color-string: "npm:^1.6.0" - checksum: 10/bf70438e0192f4f62f4bfbb303e7231289e8cc0d15ff6b6cbdb722d51f680049f38d4fdfc057a99cb641895cf5e350478c61d98586400b060043afc44285e7ae + color-convert: "npm:^3.1.3" + color-string: "npm:^2.1.3" + checksum: 10/88063ee058b995e5738092b5aa58888666275d1e967333f3814ff4fa334ce9a9e71de78a16fb1838f17c80793ea87f4878c20192037662809fe14eab2d474fd9 languageName: node linkType: hard @@ -11577,13 +12095,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"colorspace@npm:1.1.x": - version: 1.1.4 - resolution: "colorspace@npm:1.1.4" - dependencies: - color: "npm:^3.1.3" - text-hex: "npm:1.0.x" - checksum: 10/bb3934ef3c417e961e6d03d7ca60ea6e175947029bfadfcdb65109b01881a1c0ecf9c2b0b59abcd0ee4a0d7c1eae93beed01b0e65848936472270a0b341ebce8 +"colorjs.io@npm:^0.5.0": + version: 0.5.2 + resolution: "colorjs.io@npm:0.5.2" + checksum: 10/a6f6345865b177d19481008cb299c46ec9ff1fd206f472cd9ef69ddbca65832c81237b19fdcd24f3f9540c3e6343a22eb486cd800f5eab9815ce7c98c16a0f0e languageName: node linkType: hard @@ -11686,14 +12201,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"compare-versions@npm:4.1.4": - version: 4.1.4 - resolution: "compare-versions@npm:4.1.4" - checksum: 10/0c4f0d943477b824234f5c6600ea7404a86ef506c696b9d91ee67979bd32c08371a8b6532cc17e6e17cf2916e46ef16d499dce70245a4f6786c3c055afcea697 +"compare-versions@npm:6.1.1": + version: 6.1.1 + resolution: "compare-versions@npm:6.1.1" + checksum: 10/9325c0fadfba81afa0ec17e6fc2ef823ba785c693089698b8d9374e5460509f1916a88591644d4cb4045c9a58e47fafbcc0724fe8bf446d2a875a3d6eeddf165 languageName: node linkType: hard -"compressible@npm:~2.0.16": +"compressible@npm:~2.0.18": version: 2.0.18 resolution: "compressible@npm:2.0.18" dependencies: @@ -11703,17 +12218,17 @@ asn1@evs-broadcast/node-asn1: linkType: hard "compression@npm:^1.7.4": - version: 1.7.4 - resolution: "compression@npm:1.7.4" + version: 1.8.1 + resolution: "compression@npm:1.8.1" dependencies: - accepts: "npm:~1.3.5" - bytes: "npm:3.0.0" - compressible: "npm:~2.0.16" + bytes: "npm:3.1.2" + compressible: "npm:~2.0.18" debug: "npm:2.6.9" - on-headers: "npm:~1.0.2" - safe-buffer: "npm:5.1.2" + negotiator: "npm:~0.6.4" + on-headers: "npm:~1.1.0" + safe-buffer: "npm:5.2.1" vary: "npm:~1.1.2" - checksum: 10/469cd097908fe1d3ff146596d4c24216ad25eabb565c5456660bdcb3a14c82ebc45c23ce56e19fc642746cf407093b55ab9aa1ac30b06883b27c6c736e6383c2 + checksum: 10/e7552bfbd780f2003c6fe8decb44561f5cc6bc82f0c61e81122caff5ec656f37824084f52155b1e8ef31d7656cecbec9a2499b7a68e92e20780ffb39b479abb7 languageName: node linkType: hard @@ -11736,21 +12251,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"concurrently@npm:6.5.1": - version: 6.5.1 - resolution: "concurrently@npm:6.5.1" +"concurrently@npm:9.2.1": + version: 9.2.1 + resolution: "concurrently@npm:9.2.1" dependencies: - chalk: "npm:^4.1.0" - date-fns: "npm:^2.16.1" - lodash: "npm:^4.17.21" - rxjs: "npm:^6.6.3" - spawn-command: "npm:^0.0.2-1" - supports-color: "npm:^8.1.0" - tree-kill: "npm:^1.2.2" - yargs: "npm:^16.2.0" + chalk: "npm:4.1.2" + rxjs: "npm:7.8.2" + shell-quote: "npm:1.8.3" + supports-color: "npm:8.1.1" + tree-kill: "npm:1.2.2" + yargs: "npm:17.7.2" bin: - concurrently: bin/concurrently.js - checksum: 10/9ea52a75547418b64fd9d6a956f2f6ffc5b5262d99958b258dce4403b041e81dc79ae09dd9edeb4ba81df1fd6bf62d73e779b8a23c1a76e5464b151830bd92d8 + conc: dist/bin/concurrently.js + concurrently: dist/bin/concurrently.js + checksum: 10/2a6b1acbcdbeb478926b80fd81d0b7e075fa16d78a76ceb43f0478b8aeea1c70781379be2f7d6a2528e51fac48ce4ebb686ae2328e4b35e0b1d17234f121c700 languageName: node linkType: hard @@ -12055,19 +12569,19 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"core-js-compat@npm:^3.38.0, core-js-compat@npm:^3.38.1": - version: 3.40.0 - resolution: "core-js-compat@npm:3.40.0" +"core-js-compat@npm:^3.38.0, core-js-compat@npm:^3.48.0": + version: 3.48.0 + resolution: "core-js-compat@npm:3.48.0" dependencies: - browserslist: "npm:^4.24.3" - checksum: 10/3dd3d717b3d4ae0d9c2930d39c0f2a21ca6f195fcdd5711bda833557996c4d9f90277eab576423478e95689257e2de8d1a2623d6618084416bd224d10d5df9a4 + browserslist: "npm:^4.28.1" + checksum: 10/83c326dcfef5e174fd3f8f33c892c66e06d567ce27f323a1197a6c280c0178fe18d3e9c5fb95b00c18b98d6c53fba5c646def5fedaa77310a4297d16dfbe2029 languageName: node linkType: hard -"core-js-pure@npm:^3.30.2": - version: 3.32.2 - resolution: "core-js-pure@npm:3.32.2" - checksum: 10/360dd29b223e5d8c92ffaf7027d865419c96e9929ee4bff54afe5950fa5729d2776d43ab90c2ceb0448fc28180a2574a37aa528c2575105af0183e1d6dd9161c +"core-js-pure@npm:^3.48.0": + version: 3.48.0 + resolution: "core-js-pure@npm:3.48.0" + checksum: 10/7c624d5551252ad166b9a7df4daca354540b71bb2ce9c8df2a9ef7acb6335a7a56557bcbe2bd78e20e3a4eeeee2922ff37a22a67e978b293a2b4e5b9a7a04d9b languageName: node linkType: hard @@ -12103,7 +12617,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cosmiconfig@npm:9.0.0": +"cosmiconfig@npm:9.0.0, cosmiconfig@npm:^9.0.0": version: 9.0.0 resolution: "cosmiconfig@npm:9.0.0" dependencies: @@ -12137,7 +12651,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"create-ecdh@npm:^4.0.0": +"create-ecdh@npm:^4.0.4": version: 4.0.4 resolution: "create-ecdh@npm:4.0.4" dependencies: @@ -12147,7 +12661,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"create-hash@npm:^1.1.0, create-hash@npm:^1.1.2, create-hash@npm:^1.2.0": +"create-hash@npm:^1.1.0, create-hash@npm:^1.2.0": version: 1.2.0 resolution: "create-hash@npm:1.2.0" dependencies: @@ -12160,7 +12674,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"create-hmac@npm:^1.1.0, create-hmac@npm:^1.1.4, create-hmac@npm:^1.1.7": +"create-hmac@npm:^1.1.7": version: 1.1.7 resolution: "create-hmac@npm:1.1.7" dependencies: @@ -12174,23 +12688,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"create-jest@npm:^29.7.0": - version: 29.7.0 - resolution: "create-jest@npm:29.7.0" - dependencies: - "@jest/types": "npm:^29.6.3" - chalk: "npm:^4.0.0" - exit: "npm:^0.1.2" - graceful-fs: "npm:^4.2.9" - jest-config: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - prompts: "npm:^2.0.1" - bin: - create-jest: bin/create-jest.js - checksum: 10/847b4764451672b4174be4d5c6d7d63442ec3aa5f3de52af924e4d996d87d7801c18e125504f25232fc75840f6625b3ac85860fac6ce799b5efae7bdcaf4a2b7 - languageName: node - linkType: hard - "create-require@npm:^1.1.0, create-require@npm:^1.1.1": version: 1.1.1 resolution: "create-require@npm:1.1.1" @@ -12198,12 +12695,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cross-fetch@npm:3.1.5": - version: 3.1.5 - resolution: "cross-fetch@npm:3.1.5" +"cross-fetch@npm:4.0.0": + version: 4.0.0 + resolution: "cross-fetch@npm:4.0.0" dependencies: - node-fetch: "npm:2.6.7" - checksum: 10/5d101a3b1e6cb172f0e5e8168cbc927eeff2ef915f33ceef50fed85441df870e1fdff195b56eca36fae8b78ddba5d8e913b8927f73d11b19d27e96301438cd30 + node-fetch: "npm:^2.6.12" + checksum: 10/e231a71926644ef122d334a3a4e73d9ba3ba4b480a8a277fb9badc434c1ba905b3d60c8034e18b348361a09afbec40ba9371036801ba2b675a7b84588f9f55d8 languageName: node linkType: hard @@ -12218,22 +12715,23 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"crypto-browserify@npm:^3.11.0": - version: 3.12.0 - resolution: "crypto-browserify@npm:3.12.0" +"crypto-browserify@npm:^3.12.1": + version: 3.12.1 + resolution: "crypto-browserify@npm:3.12.1" dependencies: - browserify-cipher: "npm:^1.0.0" - browserify-sign: "npm:^4.0.0" - create-ecdh: "npm:^4.0.0" - create-hash: "npm:^1.1.0" - create-hmac: "npm:^1.1.0" - diffie-hellman: "npm:^5.0.0" - inherits: "npm:^2.0.1" - pbkdf2: "npm:^3.0.3" - public-encrypt: "npm:^4.0.0" - randombytes: "npm:^2.0.0" - randomfill: "npm:^1.0.3" - checksum: 10/5ab534474e24c8c3925bd1ec0de57c9022329cb267ca8437f1e3a7200278667c0bea0a51235030a9da3165c1885c73f51cfbece1eca31fd4a53cfea23f628c9b + browserify-cipher: "npm:^1.0.1" + browserify-sign: "npm:^4.2.3" + create-ecdh: "npm:^4.0.4" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + diffie-hellman: "npm:^5.0.3" + hash-base: "npm:~3.0.4" + inherits: "npm:^2.0.4" + pbkdf2: "npm:^3.1.2" + public-encrypt: "npm:^4.0.3" + randombytes: "npm:^2.1.0" + randomfill: "npm:^1.0.4" + checksum: 10/13da0b5f61b3e8e68fcbebf0394f2b2b4d35a0d0ba6ab762720c13391d3697ea42735260a26328a6a3d872be7d4cb5abe98a7a8f88bc93da7ba59b993331b409 languageName: node linkType: hard @@ -12511,29 +13009,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"cssom@npm:^0.5.0": - version: 0.5.0 - resolution: "cssom@npm:0.5.0" - checksum: 10/b502a315b1ce020a692036cc38cb36afa44157219b80deadfa040ab800aa9321fcfbecf02fd2e6ec87db169715e27978b4ab3701f916461e9cf7808899f23b54 - languageName: node - linkType: hard - -"cssom@npm:~0.3.6": - version: 0.3.8 - resolution: "cssom@npm:0.3.8" - checksum: 10/49eacc88077555e419646c0ea84ddc73c97e3a346ad7cb95e22f9413a9722d8964b91d781ce21d378bd5ae058af9a745402383fa4e35e9cdfd19654b63f892a9 - languageName: node - linkType: hard - -"cssstyle@npm:^2.3.0": - version: 2.3.0 - resolution: "cssstyle@npm:2.3.0" - dependencies: - cssom: "npm:~0.3.6" - checksum: 10/46f7f05a153446c4018b0454ee1464b50f606cb1803c90d203524834b7438eb52f3b173ba0891c618f380ced34ee12020675dc0052a7f1be755fe4ebc27ee977 - languageName: node - linkType: hard - "cssstyle@npm:^4.2.1": version: 4.3.0 resolution: "cssstyle@npm:4.3.0" @@ -12544,10 +13019,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"csstype@npm:^3.0.2": - version: 3.1.2 - resolution: "csstype@npm:3.1.2" - checksum: 10/1f39c541e9acd9562996d88bc9fb62d1cb234786ef11ed275567d4b2bd82e1ceacde25debc8de3d3b4871ae02c2933fa02614004c97190711caebad6347debc2 +"csstype@npm:^3.0.2, csstype@npm:^3.2.2": + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: 10/ad41baf7e2ffac65ab544d79107bf7cd1a4bb9bab9ac3302f59ab4ba655d5e30942a8ae46e10ba160c6f4ecea464cc95b975ca2fefbdeeacd6ac63f12f99fe1f languageName: node linkType: hard @@ -12940,16 +13415,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"d@npm:1, d@npm:^1.0.1": - version: 1.0.1 - resolution: "d@npm:1.0.1" - dependencies: - es5-ext: "npm:^0.10.50" - type: "npm:^1.0.1" - checksum: 10/1296e3f92e646895681c1cb564abd0eb23c29db7d62c5120a279e84e98915499a477808e9580760f09e3744c0ed7ac8f7cff98d096ba9770754f6ef0f1c97983 - languageName: node - linkType: hard - "dagre-d3-es@npm:7.0.13": version: 7.0.13 resolution: "dagre-d3-es@npm:7.0.13" @@ -12974,17 +13439,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"data-urls@npm:^3.0.2": - version: 3.0.2 - resolution: "data-urls@npm:3.0.2" - dependencies: - abab: "npm:^2.0.6" - whatwg-mimetype: "npm:^3.0.0" - whatwg-url: "npm:^11.0.0" - checksum: 10/033fc3dd0fba6d24bc9a024ddcf9923691dd24f90a3d26f6545d6a2f71ec6956f93462f2cdf2183cc46f10dc01ed3bcb36731a8208456eb1a08147e571fe2a76 - languageName: node - linkType: hard - "data-urls@npm:^5.0.0": version: 5.0.0 resolution: "data-urls@npm:5.0.0" @@ -13028,15 +13482,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"date-fns@npm:^2.0.1, date-fns@npm:^2.16.1": - version: 2.30.0 - resolution: "date-fns@npm:2.30.0" - dependencies: - "@babel/runtime": "npm:^7.21.0" - checksum: 10/70b3e8ea7aaaaeaa2cd80bd889622a4bcb5d8028b4de9162cbcda359db06e16ff6e9309e54eead5341e71031818497f19aaf9839c87d1aba1e27bb4796e758a9 - languageName: node - linkType: hard - "date-fns@npm:^4.1.0": version: 4.1.0 resolution: "date-fns@npm:4.1.0" @@ -13074,7 +13519,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.4.0, debug@npm:^4.4.1, debug@npm:^4.4.3": +"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.1, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -13131,10 +13576,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"decimal.js@npm:^10.4.2, decimal.js@npm:^10.4.3": - version: 10.5.0 - resolution: "decimal.js@npm:10.5.0" - checksum: 10/714d49cf2f2207b268221795ede330e51452b7c451a0c02a770837d2d4faed47d603a729c2aa1d952eb6c4102d999e91c9b952c1aa016db3c5cba9fc8bf4cda2 +"decimal.js@npm:^10.5.0": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 10/c0d45842d47c311d11b38ce7ccc911121953d4df3ebb1465d92b31970eb4f6738a065426a06094af59bee4b0d64e42e7c8984abd57b6767c64ea90cf90bb4a69 languageName: node linkType: hard @@ -13163,7 +13608,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"dedent@npm:1.5.3, dedent@npm:^1.0.0": +"dedent@npm:1.5.3": version: 1.5.3 resolution: "dedent@npm:1.5.3" peerDependencies: @@ -13175,17 +13620,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"deep-equal@npm:^1.1.1": - version: 1.1.2 - resolution: "deep-equal@npm:1.1.2" - dependencies: - is-arguments: "npm:^1.1.1" - is-date-object: "npm:^1.0.5" - is-regex: "npm:^1.1.4" - object-is: "npm:^1.1.5" - object-keys: "npm:^1.1.1" - regexp.prototype.flags: "npm:^1.5.1" - checksum: 10/c9d2ed2a0d93a2ee286bdb320cd51c78cd4c310b2161d1ede6476b67ca1d73860e7ff63b10927830aa4b9eca2a48073cfa54c8c4a1b2246397bda618c2138e97 +"dedent@npm:^1.6.0": + version: 1.7.1 + resolution: "dedent@npm:1.7.1" + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + checksum: 10/78785ef592e37e0b1ca7a7a5964c8f3dee1abdff46c5bb49864168579c122328f6bb55c769bc7e005046a7381c3372d3859f0f78ab083950fa146e1c24873f4f languageName: node linkType: hard @@ -13210,7 +13653,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"deepmerge@npm:^4.2.2, deepmerge@npm:^4.3.1": +"deepmerge@npm:^4.3.1": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 10/058d9e1b0ff1a154468bf3837aea436abcfea1ba1d165ddaaf48ca93765fdd01a30d33c36173da8fbbed951dd0a267602bc782fe288b0fc4b7e1e7091afc4529 @@ -13388,7 +13831,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"detect-newline@npm:^3.0.0": +"detect-newline@npm:^3.1.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" checksum: 10/ae6cd429c41ad01b164c59ea36f264a2c479598e61cba7c99da24175a7ab80ddf066420f2bec9a1c57a6bead411b4655ff15ad7d281c000a89791f48cbe939e7 @@ -13424,10 +13867,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"devtools-protocol@npm:0.0.1001819": - version: 0.0.1001819 - resolution: "devtools-protocol@npm:0.0.1001819" - checksum: 10/dbaa8282ca6050107f413848d925bd09726fe6cbc6ff6308a8f98957ee65a704f4f3ea42db2456cf7de65952ad82c9a37e00788c6229869c3545c586d86a8c53 +"devtools-protocol@npm:0.0.1551306": + version: 0.0.1551306 + resolution: "devtools-protocol@npm:0.0.1551306" + checksum: 10/47e9c4eecc2a48edfb2900dba9510adf54e21cc84524c05225869570ff923b44f5f5b19e8ecd2a33d300cefa3b2404ba53adc822d82f1d5f48af290c1918b796 languageName: node linkType: hard @@ -13441,7 +13884,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"diff-sequences@npm:^29.6.3": +"diff-sequences@npm:^29.0.0": version: 29.6.3 resolution: "diff-sequences@npm:29.6.3" checksum: 10/179daf9d2f9af5c57ad66d97cb902a538bcf8ed64963fa7aa0c329b3de3665ce2eb6ffdc2f69f29d445fa4af2517e5e55e5b6e00c00a9ae4f43645f97f7078cb @@ -13455,14 +13898,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"diff@npm:^5.0.0": - version: 5.2.2 - resolution: "diff@npm:5.2.2" - checksum: 10/8a885b38113d96138d87f6cb474ee959b7e9ab33c0c4cb1b07dcf019ec544945a2309d53d721532af020de4b3a58fb89f1026f64f42f9421aa9c3ae48a36998b - languageName: node - linkType: hard - -"diffie-hellman@npm:^5.0.0": +"diffie-hellman@npm:^5.0.3": version: 5.0.3 resolution: "diffie-hellman@npm:5.0.3" dependencies: @@ -13566,10 +14002,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"domain-browser@npm:^4.22.0": - version: 4.23.0 - resolution: "domain-browser@npm:4.23.0" - checksum: 10/56d5a969ed330a16aa6f03f26e7ba3b98e07c7ce4a77d08f987e9e424f1deca009070ed9bd24011d9b863499dcba95de4d679bba77aef346ee23230e570ab9cf +"domain-browser@npm:4.22.0": + version: 4.22.0 + resolution: "domain-browser@npm:4.22.0" + checksum: 10/3ffbaf0cae8da717698d472ca85ab52f96c538fe1fe85e5eb3351d4e7af52423ce096b8a0c51bb318e1c9ccf9c2e94b3b0f68e5923ad0aa0c623a32b641ed11c languageName: node linkType: hard @@ -13580,15 +14016,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"domexception@npm:^4.0.0": - version: 4.0.0 - resolution: "domexception@npm:4.0.0" - dependencies: - webidl-conversions: "npm:^7.0.0" - checksum: 10/4ed443227d2871d76c58d852b2e93c68e0443815b2741348f20881bedee8c1ad4f9bfc5d30c7dec433cd026b57da63407c010260b1682fef4c8847e7181ea43f - languageName: node - linkType: hard - "domhandler@npm:^4.0.0, domhandler@npm:^4.2.0, domhandler@npm:^4.3.1": version: 4.3.1 resolution: "domhandler@npm:4.3.1" @@ -13608,14 +14035,14 @@ asn1@evs-broadcast/node-asn1: linkType: hard "dompurify@npm:^3.2.4, dompurify@npm:^3.2.5": - version: 3.3.0 - resolution: "dompurify@npm:3.3.0" + version: 3.3.2 + resolution: "dompurify@npm:3.3.2" dependencies: "@types/trusted-types": "npm:^2.0.7" dependenciesMeta: "@types/trusted-types": optional: true - checksum: 10/d8782b10a0454344476936c91038d06c9450b3e3ada2ceb8f722525e6b54e64d847939b9f35bf385facd4139f0a2eaf7f5553efce351f8e9295620570875f002 + checksum: 10/3ca02559677ce6d9583a500f21ffbb6b9e88f1af99f69fa0d0d9442cddbac98810588c869f8b435addb5115492d6e49870024bca322169b941bafedb99c7f281 languageName: node linkType: hard @@ -13729,7 +14156,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ejs@npm:^3.1.10, ejs@npm:^3.1.7": +"ejs@npm:^3.1.7": version: 3.1.10 resolution: "ejs@npm:3.1.10" dependencies: @@ -13747,9 +14174,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"elastic-apm-node@npm:^4.11.0": - version: 4.11.0 - resolution: "elastic-apm-node@npm:4.11.0" +"elastic-apm-node@npm:^4.15.0": + version: 4.15.0 + resolution: "elastic-apm-node@npm:4.15.0" dependencies: "@elastic/ecs-pino-format": "npm:^1.5.0" "@opentelemetry/api": "npm:^1.4.1" @@ -13769,7 +14196,7 @@ asn1@evs-broadcast/node-asn1: fast-safe-stringify: "npm:^2.0.7" fast-stream-to-buffer: "npm:^1.0.0" http-headers: "npm:^3.0.2" - import-in-the-middle: "npm:1.12.0" + import-in-the-middle: "npm:1.14.4" json-bigint: "npm:^1.0.0" lru-cache: "npm:10.2.0" measured-reporting: "npm:^1.51.1" @@ -13781,27 +14208,27 @@ asn1@evs-broadcast/node-asn1: pino: "npm:^8.15.0" readable-stream: "npm:^3.6.2" relative-microtime: "npm:^2.0.0" - require-in-the-middle: "npm:^7.1.1" + require-in-the-middle: "npm:^8.0.0" semver: "npm:^7.5.4" shallow-clone-shim: "npm:^2.0.0" source-map: "npm:^0.8.0-beta.0" sql-summary: "npm:^1.0.1" stream-chopper: "npm:^3.0.1" unicode-byte-truncate: "npm:^1.0.0" - checksum: 10/b12aa4a4d4e89796727632b3f0b6399729e7151a63293e5e0e0bb9678f829059bce4e6ebe99bd8ef6300ea1f27ae451805b951f05a65bcccd7e9651dd3583502 + checksum: 10/6207a28ee1ab4b1d0459e2f545745377d6108a7d32587c51607f1f46bb6e08d051d67c73978ba2b089026bf22e6d1abcf44f8558a10013e6629577276688b199 languageName: node linkType: hard -"electron-to-chromium@npm:^1.5.249": - version: 1.5.255 - resolution: "electron-to-chromium@npm:1.5.255" - checksum: 10/7a8a1a0420c4eef25e9f3065e1237e92dc1844854cc11979a26712099a586ce95070b68aba891a9a5af4b2a2814261ad8be8f3176faf7e57c246650ecc378810 +"electron-to-chromium@npm:^1.5.263": + version: 1.5.286 + resolution: "electron-to-chromium@npm:1.5.286" + checksum: 10/530ae36571f3f737431dc1f97ab176d9ec38d78e7a14a78fff78540769ef139e9011200a886864111ee26d64e647136531ff004f368f5df8cdd755c45ad97649 languageName: node linkType: hard -"elliptic@npm:^6.5.3, elliptic@npm:^6.5.5": - version: 6.5.6 - resolution: "elliptic@npm:6.5.6" +"elliptic@npm:^6.5.3, elliptic@npm:^6.6.1": + version: 6.6.1 + resolution: "elliptic@npm:6.6.1" dependencies: bn.js: "npm:^4.11.9" brorand: "npm:^1.1.0" @@ -13810,7 +14237,7 @@ asn1@evs-broadcast/node-asn1: inherits: "npm:^2.0.4" minimalistic-assert: "npm:^1.0.1" minimalistic-crypto-utils: "npm:^1.0.1" - checksum: 10/09377ec924fdb37775d63e5d7e5ebb2845842e6f08880b68265b1108863e968970c4a4e1c43df622078c8262417deec9a04aeb9d34e8d09a9693e19b5454e1df + checksum: 10/dc678c9febd89a219c4008ba3a9abb82237be853d9fd171cd602c8fb5ec39927e65c6b5e7a1b2a4ea82ee8e0ded72275e7932bb2da04a5790c2638b818e4e1c5 languageName: node linkType: hard @@ -13916,13 +14343,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"enhanced-resolve@npm:^5.17.1": - version: 5.18.0 - resolution: "enhanced-resolve@npm:5.18.0" +"enhanced-resolve@npm:^5.17.1, enhanced-resolve@npm:^5.20.0": + version: 5.20.0 + resolution: "enhanced-resolve@npm:5.20.0" dependencies: graceful-fs: "npm:^4.2.4" - tapable: "npm:^2.2.0" - checksum: 10/e88463ef97b68d40d0da0cd0c572e23f43dba0be622d6d44eae5cafed05f0c5dac43e463a83a86c4f70186d029357f82b56d9e1e47e8fc91dce3d6602f8bd6ce + tapable: "npm:^2.3.0" + checksum: 10/ba22699e4b46dc1be6441c359636ebcdd5028229219a7d6ba10f39996401f950967f8297ddf3284d0ee8e33c8133a8742696154e383cc08d8bd2bf80ba87df97 languageName: node linkType: hard @@ -14148,19 +14575,19 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"es-module-lexer@npm:^1.2.1": - version: 1.3.1 - resolution: "es-module-lexer@npm:1.3.1" - checksum: 10/c6aa137c5f5865fe1d12b4edbe027ff618d3836684cda9e52ae4dec48bfc2599b25db4f1265a12228d4663e21fd0126addfb79f761d513f1a6708c37989137e3 +"es-module-lexer@npm:^2.0.0": + version: 2.0.0 + resolution: "es-module-lexer@npm:2.0.0" + checksum: 10/b075855289b5f40ee496f3d7525c5c501d029c3da15c22298a0030d625bf36d1da0768b26278f7f4bada2a602459b505888e20b77c414fba5da5619b0e84dbd1 languageName: node linkType: hard -"es-object-atoms@npm:^1.0.0": - version: 1.0.0 - resolution: "es-object-atoms@npm:1.0.0" +"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": + version: 1.1.1 + resolution: "es-object-atoms@npm:1.1.1" dependencies: es-errors: "npm:^1.3.0" - checksum: 10/f8910cf477e53c0615f685c5c96210591841850871b81924fcf256bfbaa68c254457d994a4308c60d15b20805e7f61ce6abc669375e01a5349391a8c1767584f + checksum: 10/54fe77de288451dae51c37bfbfe3ec86732dc3778f98f3eb3bdb4bf48063b2c0b8f9c93542656986149d08aa5be3204286e2276053d19582b76753f1a2728867 languageName: node linkType: hard @@ -14196,80 +14623,36 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.46, es5-ext@npm:^0.10.50, es5-ext@npm:^0.10.53, es5-ext@npm:^0.10.61, es5-ext@npm:^0.10.62, es5-ext@npm:~0.10.14, es5-ext@npm:~0.10.2, es5-ext@npm:~0.10.46": - version: 0.10.64 - resolution: "es5-ext@npm:0.10.64" - dependencies: - es6-iterator: "npm:^2.0.3" - es6-symbol: "npm:^3.1.3" - esniff: "npm:^2.0.1" - next-tick: "npm:^1.1.0" - checksum: 10/0c5d8657708b1695ddc4b06f4e0b9fbdda4d2fe46d037b6bedb49a7d1931e542ec9eecf4824d59e1d357e93229deab014bb4b86485db2d41b1d68e54439689ce - languageName: node - linkType: hard - -"es6-iterator@npm:^2.0.3": - version: 2.0.3 - resolution: "es6-iterator@npm:2.0.3" - dependencies: - d: "npm:1" - es5-ext: "npm:^0.10.35" - es6-symbol: "npm:^3.1.1" - checksum: 10/dbadecf3d0e467692815c2b438dfa99e5a97cbbecf4a58720adcb467a04220e0e36282399ba297911fd472c50ae4158fffba7ed0b7d4273fe322b69d03f9e3a5 - languageName: node - linkType: hard - -"es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3": - version: 3.1.3 - resolution: "es6-symbol@npm:3.1.3" - dependencies: - d: "npm:^1.0.1" - ext: "npm:^1.1.2" - checksum: 10/b404e5ecae1a076058aa2ba2568d87e2cb4490cb1130784b84e7b4c09c570b487d4f58ed685a08db8d350bd4916500dd3d623b26e6b3520841d30d2ebb152f8d - languageName: node - linkType: hard - -"es6-weak-map@npm:^2.0.3": - version: 2.0.3 - resolution: "es6-weak-map@npm:2.0.3" - dependencies: - d: "npm:1" - es5-ext: "npm:^0.10.46" - es6-iterator: "npm:^2.0.3" - es6-symbol: "npm:^3.1.1" - checksum: 10/5958a321cf8dfadc82b79eeaa57dc855893a4afd062b4ef5c9ded0010d3932099311272965c3d3fdd3c85df1d7236013a570e704fa6c1f159bbf979c203dd3a3 - languageName: node - linkType: hard - -"esbuild@npm:^0.24.2": - version: 0.24.2 - resolution: "esbuild@npm:0.24.2" - dependencies: - "@esbuild/aix-ppc64": "npm:0.24.2" - "@esbuild/android-arm": "npm:0.24.2" - "@esbuild/android-arm64": "npm:0.24.2" - "@esbuild/android-x64": "npm:0.24.2" - "@esbuild/darwin-arm64": "npm:0.24.2" - "@esbuild/darwin-x64": "npm:0.24.2" - "@esbuild/freebsd-arm64": "npm:0.24.2" - "@esbuild/freebsd-x64": "npm:0.24.2" - "@esbuild/linux-arm": "npm:0.24.2" - "@esbuild/linux-arm64": "npm:0.24.2" - "@esbuild/linux-ia32": "npm:0.24.2" - "@esbuild/linux-loong64": "npm:0.24.2" - "@esbuild/linux-mips64el": "npm:0.24.2" - "@esbuild/linux-ppc64": "npm:0.24.2" - "@esbuild/linux-riscv64": "npm:0.24.2" - "@esbuild/linux-s390x": "npm:0.24.2" - "@esbuild/linux-x64": "npm:0.24.2" - "@esbuild/netbsd-arm64": "npm:0.24.2" - "@esbuild/netbsd-x64": "npm:0.24.2" - "@esbuild/openbsd-arm64": "npm:0.24.2" - "@esbuild/openbsd-x64": "npm:0.24.2" - "@esbuild/sunos-x64": "npm:0.24.2" - "@esbuild/win32-arm64": "npm:0.24.2" - "@esbuild/win32-ia32": "npm:0.24.2" - "@esbuild/win32-x64": "npm:0.24.2" +"esbuild@npm:^0.27.0": + version: 0.27.2 + resolution: "esbuild@npm:0.27.2" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.2" + "@esbuild/android-arm": "npm:0.27.2" + "@esbuild/android-arm64": "npm:0.27.2" + "@esbuild/android-x64": "npm:0.27.2" + "@esbuild/darwin-arm64": "npm:0.27.2" + "@esbuild/darwin-x64": "npm:0.27.2" + "@esbuild/freebsd-arm64": "npm:0.27.2" + "@esbuild/freebsd-x64": "npm:0.27.2" + "@esbuild/linux-arm": "npm:0.27.2" + "@esbuild/linux-arm64": "npm:0.27.2" + "@esbuild/linux-ia32": "npm:0.27.2" + "@esbuild/linux-loong64": "npm:0.27.2" + "@esbuild/linux-mips64el": "npm:0.27.2" + "@esbuild/linux-ppc64": "npm:0.27.2" + "@esbuild/linux-riscv64": "npm:0.27.2" + "@esbuild/linux-s390x": "npm:0.27.2" + "@esbuild/linux-x64": "npm:0.27.2" + "@esbuild/netbsd-arm64": "npm:0.27.2" + "@esbuild/netbsd-x64": "npm:0.27.2" + "@esbuild/openbsd-arm64": "npm:0.27.2" + "@esbuild/openbsd-x64": "npm:0.27.2" + "@esbuild/openharmony-arm64": "npm:0.27.2" + "@esbuild/sunos-x64": "npm:0.27.2" + "@esbuild/win32-arm64": "npm:0.27.2" + "@esbuild/win32-ia32": "npm:0.27.2" + "@esbuild/win32-x64": "npm:0.27.2" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -14313,6 +14696,8 @@ asn1@evs-broadcast/node-asn1: optional: true "@esbuild/openbsd-x64": optional: true + "@esbuild/openharmony-arm64": + optional: true "@esbuild/sunos-x64": optional: true "@esbuild/win32-arm64": @@ -14323,7 +14708,7 @@ asn1@evs-broadcast/node-asn1: optional: true bin: esbuild: bin/esbuild - checksum: 10/95425071c9f24ff88bf61e0710b636ec0eb24ddf8bd1f7e1edef3044e1221104bbfa7bbb31c18018c8c36fa7902c5c0b843f829b981ebc89160cf5eebdaa58f4 + checksum: 10/7f1229328b0efc63c4184a61a7eb303df1e99818cc1d9e309fb92600703008e69821e8e984e9e9f54a627da14e0960d561db3a93029482ef96dc82dd267a60c2 languageName: node linkType: hard @@ -14348,6 +14733,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"escape-string-regexp@npm:5.0.0, escape-string-regexp@npm:^5.0.0": + version: 5.0.0 + resolution: "escape-string-regexp@npm:5.0.0" + checksum: 10/20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e + languageName: node + linkType: hard + "escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" @@ -14369,14 +14761,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"escape-string-regexp@npm:^5.0.0": - version: 5.0.0 - resolution: "escape-string-regexp@npm:5.0.0" - checksum: 10/20daabe197f3cb198ec28546deebcf24b3dbb1a5a269184381b3116d12f0532e06007f4bc8da25669d6a7f8efb68db0758df4cd981f57bc5b57f521a3e12c59e - languageName: node - linkType: hard - -"escodegen@npm:^2.0.0, escodegen@npm:^2.1.0": +"escodegen@npm:^2.1.0": version: 2.1.0 resolution: "escodegen@npm:2.1.0" dependencies: @@ -14405,25 +14790,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"eslint-compat-utils@npm:^0.6.0": - version: 0.6.4 - resolution: "eslint-compat-utils@npm:0.6.4" - dependencies: - semver: "npm:^7.5.4" - peerDependencies: - eslint: ">=6.0.0" - checksum: 10/97f08f4aa8d9a1bc1087aaeceab46a5fa65a6d70703c1a2f2cd533562381208fdd0a293ce0f63ad607f1e697ddb348ef1076b02f5afa83c70f4a07ca0dcec90e - languageName: node - linkType: hard - -"eslint-config-prettier@npm:^10.0.1": - version: 10.0.1 - resolution: "eslint-config-prettier@npm:10.0.1" +"eslint-config-prettier@npm:^10.1.8": + version: 10.1.8 + resolution: "eslint-config-prettier@npm:10.1.8" peerDependencies: eslint: ">=7.0.0" bin: - eslint-config-prettier: build/bin/cli.js - checksum: 10/ba6875df0fc4fd3c7c6e2ec9c2e6a224462f7afc662f4cf849775c598a3571c1be136a9b683b12971653b3dcf3f31472aaede3076524b46ec9a77582630158e5 + eslint-config-prettier: bin/cli.js + checksum: 10/03f8e6ea1a6a9b8f9eeaf7c8c52a96499ec4b275b9ded33331a6cc738ed1d56de734097dbd0091f136f0e84bc197388bd8ec22a52a4658105883f8c8b7d8921a languageName: node linkType: hard @@ -14440,9 +14814,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"eslint-plugin-jest@npm:^28.11.0": - version: 28.11.0 - resolution: "eslint-plugin-jest@npm:28.11.0" +"eslint-plugin-jest@npm:^28.14.0": + version: 28.14.0 + resolution: "eslint-plugin-jest@npm:28.14.0" dependencies: "@typescript-eslint/utils": "npm:^6.0.0 || ^7.0.0 || ^8.0.0" peerDependencies: @@ -14454,51 +14828,52 @@ asn1@evs-broadcast/node-asn1: optional: true jest: optional: true - checksum: 10/7f3896ec2dc03110688bb9f359a7aa1ba1a6d9a60ffbc3642361c4aaf55afcba9ce36b6609b20b1507028c2170ffe29b0f3e9cc9b7fe12fdd233740a2f9ce0a1 + checksum: 10/6032497bd97d6dd010450d5fdf535b8613a2789f4f83764ae04361c48d06d92f3d9b2e4350914b8fd857b6e611ba2b5282a1133ab8ec51b3e7053f9d336058e6 languageName: node linkType: hard -"eslint-plugin-n@npm:^17.15.1": - version: 17.15.1 - resolution: "eslint-plugin-n@npm:17.15.1" +"eslint-plugin-n@npm:^17.23.2": + version: 17.23.2 + resolution: "eslint-plugin-n@npm:17.23.2" dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.1" + "@eslint-community/eslint-utils": "npm:^4.5.0" enhanced-resolve: "npm:^5.17.1" eslint-plugin-es-x: "npm:^7.8.0" get-tsconfig: "npm:^4.8.1" globals: "npm:^15.11.0" + globrex: "npm:^0.1.2" ignore: "npm:^5.3.2" - minimatch: "npm:^9.0.5" semver: "npm:^7.6.3" + ts-declaration-location: "npm:^1.0.6" peerDependencies: eslint: ">=8.23.0" - checksum: 10/43fc161949fa0346ac7063a30580cd0db27e216b8e6a48d73d0bf4f10b88e9b65f263399843b3fe2087f766f264d16f0cbe8f2f898591516842201dc115a2d21 + checksum: 10/67a15908b27fe5f9aee97d280c3e869debef58fc941b285ace6dab0baabe5263086f90d72a059f9e6efe0a427b77d42912892146eb8c882d085733c7cb068ead languageName: node linkType: hard -"eslint-plugin-prettier@npm:^5.2.3": - version: 5.2.3 - resolution: "eslint-plugin-prettier@npm:5.2.3" +"eslint-plugin-prettier@npm:^5.5.5": + version: 5.5.5 + resolution: "eslint-plugin-prettier@npm:5.5.5" dependencies: - prettier-linter-helpers: "npm:^1.0.0" - synckit: "npm:^0.9.1" + prettier-linter-helpers: "npm:^1.0.1" + synckit: "npm:^0.11.12" peerDependencies: "@types/eslint": ">=8.0.0" eslint: ">=8.0.0" - eslint-config-prettier: "*" + eslint-config-prettier: ">= 7.0.0 <10.0.0 || >=10.1.0" prettier: ">=3.0.0" peerDependenciesMeta: "@types/eslint": optional: true eslint-config-prettier: optional: true - checksum: 10/6444a0b89f3e2a6b38adce69761133f8539487d797f1655b3fa24f93a398be132c4f68f87041a14740b79202368d5782aa1dffd2bd7a3ea659f263d6796acf15 + checksum: 10/36c22c2fa2fd7c61ed292af1280e1d8f94dfe1671eacc5a503a249ca4b27fd226dbf6a1820457d611915926946f42729488d2dc7a5c320601e6cf1fad0d28f66 languageName: node linkType: hard -"eslint-plugin-react@npm:^7.37.4": - version: 7.37.4 - resolution: "eslint-plugin-react@npm:7.37.4" +"eslint-plugin-react@npm:^7.37.5": + version: 7.37.5 + resolution: "eslint-plugin-react@npm:7.37.5" dependencies: array-includes: "npm:^3.1.8" array.prototype.findlast: "npm:^1.2.5" @@ -14510,7 +14885,7 @@ asn1@evs-broadcast/node-asn1: hasown: "npm:^2.0.2" jsx-ast-utils: "npm:^2.4.1 || ^3.0.0" minimatch: "npm:^3.1.2" - object.entries: "npm:^1.1.8" + object.entries: "npm:^1.1.9" object.fromentries: "npm:^2.0.8" object.values: "npm:^1.2.1" prop-types: "npm:^15.8.1" @@ -14520,22 +14895,24 @@ asn1@evs-broadcast/node-asn1: string.prototype.repeat: "npm:^1.0.0" peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - checksum: 10/c538c10665c87cb90a0bcc4efe53a758570db10997d079d31474a9760116ef5584648fa22403d889ca672df8071bda10b40434ea0499e5ee8360bc5c8aba1679 + checksum: 10/ee1bd4e0ec64f29109d5a625bb703d179c82e0159c86c3f1b52fc1209d2994625a137dae303c333fb308a2e38315e44066d5204998177e31974382f9fda25d5c languageName: node linkType: hard -"eslint-plugin-yml@npm:^1.16.0": - version: 1.16.0 - resolution: "eslint-plugin-yml@npm:1.16.0" +"eslint-plugin-yml@npm:^3.1.2": + version: 3.1.2 + resolution: "eslint-plugin-yml@npm:3.1.2" dependencies: + "@eslint/core": "npm:^1.0.1" + "@eslint/plugin-kit": "npm:^0.6.0" debug: "npm:^4.3.2" - eslint-compat-utils: "npm:^0.6.0" - lodash: "npm:^4.17.21" + diff-sequences: "npm:^29.0.0" + escape-string-regexp: "npm:5.0.0" natural-compare: "npm:^1.4.0" - yaml-eslint-parser: "npm:^1.2.1" + yaml-eslint-parser: "npm:^2.0.0" peerDependencies: - eslint: ">=6.0.0" - checksum: 10/523f3016098f2de340a68c1fa11228734b281191d3391b1ab8812c1c446455841f35815b4c4f036c2ab459a5f7b0c6496dd5bc57de912632d8db9da44e45eb44 + eslint: ">=9.38.0" + checksum: 10/8be6d3353c21b7e276b76430fcff1ab83f2fcca491e58b4ba399738c3ce7196e82f55c98f88dc73061aa462cc6f868008dc294445524f2a2cb58457c6ddf0489 languageName: node linkType: hard @@ -14559,21 +14936,28 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.0.0, eslint-visitor-keys@npm:^3.4.3": +"eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10/3f357c554a9ea794b094a09bd4187e5eacd1bc0d0653c3adeb87962c548e6a1ab8f982b86963ae1337f5d976004146536dcee5d0e2806665b193fbfbf1a9231b languageName: node linkType: hard -"eslint-visitor-keys@npm:^4.2.0, eslint-visitor-keys@npm:^4.2.1": +"eslint-visitor-keys@npm:^4.2.1": version: 4.2.1 resolution: "eslint-visitor-keys@npm:4.2.1" checksum: 10/3ee00fc6a7002d4b0ffd9dc99e13a6a7882c557329e6c25ab254220d71e5c9c4f89dca4695352949ea678eb1f3ba912a18ef8aac0a7fe094196fd92f441bfce2 languageName: node linkType: hard -"eslint@npm:^9.18.0": +"eslint-visitor-keys@npm:^5.0.0": + version: 5.0.0 + resolution: "eslint-visitor-keys@npm:5.0.0" + checksum: 10/05334d637c73d02f644b8dbfd6f555f049a229654b543b4b701944051072808d944368164c8b291cecb60e157a54d05f221eb45945a1bdd06c3e0e298ddb4678 + languageName: node + linkType: hard + +"eslint@npm:^9.39.2": version: 9.39.2 resolution: "eslint@npm:9.39.2" dependencies: @@ -14622,18 +15006,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"esniff@npm:^2.0.1": - version: 2.0.1 - resolution: "esniff@npm:2.0.1" - dependencies: - d: "npm:^1.0.1" - es5-ext: "npm:^0.10.62" - event-emitter: "npm:^0.3.5" - type: "npm:^2.7.2" - checksum: 10/f6a2abd2f8c5fe57c5fcf53e5407c278023313d0f6c3a92688e7122ab9ac233029fd424508a196ae5bc561aa1f67d23f4e2435b1a0d378030f476596129056ac - languageName: node - linkType: hard - "espree@npm:^10.0.1, espree@npm:^10.4.0": version: 10.4.0 resolution: "espree@npm:10.4.0" @@ -14727,12 +15099,11 @@ asn1@evs-broadcast/node-asn1: linkType: hard "estree-util-value-to-estree@npm:^3.0.1": - version: 3.1.1 - resolution: "estree-util-value-to-estree@npm:3.1.1" + version: 3.5.0 + resolution: "estree-util-value-to-estree@npm:3.5.0" dependencies: "@types/estree": "npm:^1.0.0" - is-plain-obj: "npm:^4.0.0" - checksum: 10/31e87547ee99aa9f34ffb373badb74ed7bc8156a10acd0a77f88c27b6706630f73d60a6834365cdc9d22e24280cdb5cba557ff3dbc5118f888f996cfa2dd6f58 + checksum: 10/b8fc4db7a70d7af5c1ae9d611fc7802022e88fece351ddc557a2db3aa3c7d65eb79e4499f845b9783054cb6826b489ed17c178b09d50ca182c17c53d07a79b83 languageName: node linkType: hard @@ -14800,16 +15171,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"event-emitter@npm:^0.3.5": - version: 0.3.5 - resolution: "event-emitter@npm:0.3.5" - dependencies: - d: "npm:1" - es5-ext: "npm:~0.10.14" - checksum: 10/a7f5ea80029193f4869782d34ef7eb43baa49cd397013add1953491b24588468efbe7e3cc9eb87d53f33397e7aab690fd74c079ec440bf8b12856f6bdb6e9396 - languageName: node - linkType: hard - "event-target-shim@npm:^5.0.0": version: 5.0.1 resolution: "event-target-shim@npm:5.0.1" @@ -14831,6 +15192,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"events-universal@npm:^1.0.0": + version: 1.0.1 + resolution: "events-universal@npm:1.0.1" + dependencies: + bare-events: "npm:^2.7.0" + checksum: 10/71b2e6079b4dc030c613ef73d99f1acb369dd3ddb6034f49fd98b3e2c6632cde9f61c15fb1351004339d7c79672252a4694ecc46a6124dc794b558be50a83867 + languageName: node + linkType: hard + "events@npm:^3.0.0, events@npm:^3.2.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" @@ -14873,7 +15243,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"execa@npm:5.1.1, execa@npm:^5.0.0": +"execa@npm:5.1.1, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" dependencies: @@ -14897,23 +15267,24 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"exit@npm:^0.1.2": - version: 0.1.2 - resolution: "exit@npm:0.1.2" - checksum: 10/387555050c5b3c10e7a9e8df5f43194e95d7737c74532c409910e585d5554eaff34960c166643f5e23d042196529daad059c292dcf1fb61b8ca878d3677f4b87 +"exit-x@npm:^0.2.2": + version: 0.2.2 + resolution: "exit-x@npm:0.2.2" + checksum: 10/ee043053e6c1e237adf5ad9c4faf9f085b606f64a4ff859e2b138fab63fe642711d00c9af452a9134c4c92c55f752e818bfabab78c24d345022db163f3137027 languageName: node linkType: hard -"expect@npm:^29.0.0, expect@npm:^29.7.0": - version: 29.7.0 - resolution: "expect@npm:29.7.0" +"expect@npm:30.2.0, expect@npm:^30.0.0": + version: 30.2.0 + resolution: "expect@npm:30.2.0" dependencies: - "@jest/expect-utils": "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - jest-matcher-utils: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - checksum: 10/63f97bc51f56a491950fb525f9ad94f1916e8a014947f8d8445d3847a665b5471b768522d659f5e865db20b6c2033d2ac10f35fcbd881a4d26407a4f6f18451a + "@jest/expect-utils": "npm:30.2.0" + "@jest/get-type": "npm:30.1.0" + jest-matcher-utils: "npm:30.2.0" + jest-message-util: "npm:30.2.0" + jest-mock: "npm:30.2.0" + jest-util: "npm:30.2.0" + checksum: 10/cf98ab45ab2e9f2fb9943a3ae0097f72d63a94be179a19fd2818d8fdc3b7681d31cc8ef540606eb8dd967d9c44d73fef263a614e9de260c22943ffb122ad66fd languageName: node linkType: hard @@ -14970,15 +15341,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ext@npm:^1.1.2": - version: 1.7.0 - resolution: "ext@npm:1.7.0" - dependencies: - type: "npm:^2.7.2" - checksum: 10/666a135980b002df0e75c8ac6c389140cdc59ac953db62770479ee2856d58ce69d2f845e5f2586716350b725400f6945e51e9159573158c39f369984c72dcd84 - languageName: node - linkType: hard - "extend-shallow@npm:^2.0.1": version: 2.0.1 resolution: "extend-shallow@npm:2.0.1" @@ -14995,18 +15357,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"external-editor@npm:^3.0.3": - version: 3.1.0 - resolution: "external-editor@npm:3.1.0" - dependencies: - chardet: "npm:^0.7.0" - iconv-lite: "npm:^0.4.24" - tmp: "npm:^0.0.33" - checksum: 10/776dff1d64a1d28f77ff93e9e75421a81c062983fd1544279d0a32f563c0b18c52abbb211f31262e2827e48edef5c9dc8f960d06dd2d42d1654443b88568056b - languageName: node - linkType: hard - -"extract-zip@npm:2.0.1": +"extract-zip@npm:^2.0.1": version: 2.0.1 resolution: "extract-zip@npm:2.0.1" dependencies: @@ -15058,7 +15409,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.2": +"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": + version: 1.3.2 + resolution: "fast-fifo@npm:1.3.2" + checksum: 10/6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 + languageName: node + linkType: hard + +"fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" dependencies: @@ -15122,6 +15480,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"fast-xml-parser@npm:^5.3.0": + version: 5.3.4 + resolution: "fast-xml-parser@npm:5.3.4" + dependencies: + strnum: "npm:^2.1.0" + bin: + fxparser: src/cli/cli.js + checksum: 10/0d7e6872fed7c3065641400d43cdf24c03177f05c343bfb31df53b79f0900b085c103f647852d0b00693125aa3f0e9d8b8cfc4273b168d4da0308f857dafe830 + languageName: node + linkType: hard + "fastest-stable-stringify@npm:^2.0.2": version: 2.0.2 resolution: "fastest-stable-stringify@npm:2.0.2" @@ -15156,7 +15525,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fb-watchman@npm:^2.0.0": +"fb-watchman@npm:^2.0.2": version: 2.0.2 resolution: "fb-watchman@npm:2.0.2" dependencies: @@ -15202,13 +15571,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fflate@npm:^0.8.2": - version: 0.8.2 - resolution: "fflate@npm:0.8.2" - checksum: 10/2bd26ba6d235d428de793c6a0cd1aaa96a06269ebd4e21b46c8fd1bd136abc631acf27e188d47c3936db090bf3e1ede11d15ce9eae9bffdc4bfe1b9dc66ca9cb - languageName: node - linkType: hard - "figures@npm:3.2.0, figures@npm:^3.0.0, figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -15239,15 +15601,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"file-type@npm:20.5.0": - version: 20.5.0 - resolution: "file-type@npm:20.5.0" +"file-type@npm:21.3.0": + version: 21.3.0 + resolution: "file-type@npm:21.3.0" dependencies: - "@tokenizer/inflate": "npm:^0.2.6" - strtok3: "npm:^10.2.0" - token-types: "npm:^6.0.0" + "@tokenizer/inflate": "npm:^0.4.1" + strtok3: "npm:^10.3.4" + token-types: "npm:^6.1.1" uint8array-extras: "npm:^1.4.0" - checksum: 10/1cc1ccd7cf76086e10b65cba88c708e0653676fbae900107deeb91c46de011acd1492200bf47e75cddf395de27dbe8584ca042f4cfa4a1efdf933644b7143f1d + checksum: 10/8eb8707f34d0a0fd6c2d2b223edf1ed6235cb00a44a90216741be9e812ae08dc8497dbf4e2dd63e629728509cdf361070ed32ae2280724744463ab459da81a83 languageName: node linkType: hard @@ -15419,6 +15781,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"follow-redirects@npm:^1.15.11": + version: 1.15.11 + resolution: "follow-redirects@npm:1.15.11" + peerDependenciesMeta: + debug: + optional: true + checksum: 10/07372fd74b98c78cf4d417d68d41fdaa0be4dcacafffb9e67b1e3cf090bc4771515e65020651528faab238f10f9b9c0d9707d6c1574a6c0387c5de1042cde9ba + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -15459,30 +15831,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"form-data@npm:^2.5.0": - version: 2.5.5 - resolution: "form-data@npm:2.5.5" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.2" - mime-types: "npm:^2.1.35" - safe-buffer: "npm:^5.2.1" - checksum: 10/4b6a8d07bb67089da41048e734215f68317a8e29dd5385a972bf5c458a023313c69d3b5d6b8baafbb7f808fa9881e0e2e030ffe61e096b3ddc894c516401271d - languageName: node - linkType: hard - -"form-data@npm:^4.0.0, form-data@npm:^4.0.1, form-data@npm:^4.0.4": - version: 4.0.4 - resolution: "form-data@npm:4.0.4" +"form-data@npm:^4.0.4, form-data@npm:^4.0.5": + version: 4.0.5 + resolution: "form-data@npm:4.0.5" dependencies: asynckit: "npm:^0.4.0" combined-stream: "npm:^1.0.8" es-set-tostringtag: "npm:^2.1.0" hasown: "npm:^2.0.2" mime-types: "npm:^2.1.12" - checksum: 10/a4b62e21932f48702bc468cc26fb276d186e6b07b557e3dd7cc455872bdbb82db7db066844a64ad3cf40eaf3a753c830538183570462d3649fdfd705601cbcfb + checksum: 10/52ecd6e927c8c4e215e68a7ad5e0f7c1031397439672fd9741654b4a94722c4182e74cc815b225dcb5be3f4180f36428f67c6dd39eaa98af0dcfdd26c00c19cd languageName: node linkType: hard @@ -15514,12 +15872,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"framer-motion@npm:^12.4.7": - version: 12.4.7 - resolution: "framer-motion@npm:12.4.7" +"framer-motion@npm:^12.31.0": + version: 12.31.0 + resolution: "framer-motion@npm:12.31.0" dependencies: - motion-dom: "npm:^12.4.5" - motion-utils: "npm:^12.0.0" + motion-dom: "npm:^12.30.1" + motion-utils: "npm:^12.29.2" tslib: "npm:^2.4.0" peerDependencies: "@emotion/is-prop-valid": "*" @@ -15532,7 +15890,7 @@ asn1@evs-broadcast/node-asn1: optional: true react-dom: optional: true - checksum: 10/d277e75f1ed8af69f145f263758aa046083a2c0b4f9a5e48911f3847d38d7c3bdb361e97876f635ae58d5bdb4b9cc50f6e8c631b8e225c6d8233584f106116e2 + checksum: 10/8cd76953b5e4e81e69b7bbec699cd5c913df87897148cd0f9fe85fa2f1c4e2768fbaeb6e40be9cf6d3f4b17a5a8024351d7e0ba93a9cdf9d8358c5031c5cdecd languageName: node linkType: hard @@ -15559,14 +15917,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fs-extra@npm:11.3.0, fs-extra@npm:^11.1.1, fs-extra@npm:^11.2.0": - version: 11.3.0 - resolution: "fs-extra@npm:11.3.0" +"fs-extra@npm:11.3.3, fs-extra@npm:^11.1.1, fs-extra@npm:^11.2.0": + version: 11.3.3 + resolution: "fs-extra@npm:11.3.3" dependencies: graceful-fs: "npm:^4.2.0" jsonfile: "npm:^6.0.1" universalify: "npm:^2.0.0" - checksum: 10/c9fe7b23dded1efe7bbae528d685c3206477e20cc60e9aaceb3f024f9b9ff2ee1f62413c161cb88546cc564009ab516dec99e9781ba782d869bb37e4fe04a97f + checksum: 10/daeaefafbebe8fa6efd2fb96fc926f2c952be5877811f00a6794f0d64e0128e3d0d93368cd328f8f063b45deacf385c40e3d931aa46014245431cd2f4f89c67a languageName: node linkType: hard @@ -15618,7 +15976,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": +"fsevents@npm:^2.3.3, fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -15628,7 +15986,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": +"fsevents@patch:fsevents@npm%3A^2.3.3#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -15681,6 +16039,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"generator-function@npm:^2.0.0": + version: 2.0.1 + resolution: "generator-function@npm:2.0.1" + checksum: 10/eb7e7eb896c5433f3d40982b2ccacdb3dd990dd3499f14040e002b5d54572476513be8a2e6f9609f6e41ab29f2c4469307611ddbfc37ff4e46b765c326663805 + languageName: node + linkType: hard + "gensync@npm:^1.0.0-beta.1, gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -15695,21 +16060,24 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7": - version: 1.2.7 - resolution: "get-intrinsic@npm:1.2.7" +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.4, get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6, get-intrinsic@npm:^1.2.7, get-intrinsic@npm:^1.3.0": + version: 1.3.1 + resolution: "get-intrinsic@npm:1.3.1" dependencies: - call-bind-apply-helpers: "npm:^1.0.1" + async-function: "npm:^1.0.0" + async-generator-function: "npm:^1.0.0" + call-bind-apply-helpers: "npm:^1.0.2" es-define-property: "npm:^1.0.1" es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" + es-object-atoms: "npm:^1.1.1" function-bind: "npm:^1.1.2" - get-proto: "npm:^1.0.0" + generator-function: "npm:^2.0.0" + get-proto: "npm:^1.0.1" gopd: "npm:^1.2.0" has-symbols: "npm:^1.1.0" hasown: "npm:^2.0.2" math-intrinsics: "npm:^1.1.0" - checksum: 10/4f7149c9a826723f94c6d49f70bcb3df1d3f9213994fab3668f12f09fa72074681460fb29ebb6f135556ec6372992d63802386098791a8f09cfa6f27090fa67b + checksum: 10/bb579dda84caa4a3a41611bdd483dade7f00f246f2a7992eb143c5861155290df3fdb48a8406efa3dfb0b434e2c8fafa4eebd469e409d0439247f85fc3fa2cc1 languageName: node linkType: hard @@ -15758,13 +16126,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"get-stdin@npm:^8.0.0": - version: 8.0.0 - resolution: "get-stdin@npm:8.0.0" - checksum: 10/40128b6cd25781ddbd233344f1a1e4006d4284906191ed0a7d55ec2c1a3e44d650f280b2c9eeab79c03ac3037da80257476c0e4e5af38ddfb902d6ff06282d77 - languageName: node - linkType: hard - "get-stdin@npm:^9.0.0": version: 9.0.0 resolution: "get-stdin@npm:9.0.0" @@ -15915,17 +16276,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"glob-promise@npm:^3.4.0": - version: 3.4.0 - resolution: "glob-promise@npm:3.4.0" - dependencies: - "@types/glob": "npm:*" - peerDependencies: - glob: "*" - checksum: 10/84a2c076e7581c9f8aa7a8a151ad5f9352c4118ba03c5673ecfcf540f4c53aa75f8d32fe493c2286d471dccd7a75932b9bfe97bf782564c1f4a50b9c7954e3b6 - languageName: node - linkType: hard - "glob-to-regex.js@npm:^1.0.1": version: 1.2.0 resolution: "glob-to-regex.js@npm:1.2.0" @@ -15942,19 +16292,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"glob@npm:9.3.5, glob@npm:^9.2.0": - version: 9.3.5 - resolution: "glob@npm:9.3.5" +"glob@npm:13.0.0, glob@npm:^13.0.0": + version: 13.0.0 + resolution: "glob@npm:13.0.0" dependencies: - fs.realpath: "npm:^1.0.0" - minimatch: "npm:^8.0.2" - minipass: "npm:^4.2.4" - path-scurry: "npm:^1.6.1" - checksum: 10/e5fa8a58adf53525bca42d82a1fad9e6800032b7e4d372209b80cfdca524dd9a7dbe7d01a92d7ed20d89c572457f12c250092bc8817cb4f1c63efefdf9b658c0 + minimatch: "npm:^10.1.1" + minipass: "npm:^7.1.2" + path-scurry: "npm:^2.0.0" + checksum: 10/de390721d29ee1c9ea41e40ec2aa0de2cabafa68022e237dc4297665a5e4d650776f2573191984ea1640aba1bf0ea34eddef2d8cbfbfc2ad24b5fb0af41d8846 languageName: node linkType: hard -"glob@npm:^10.2.2": +"glob@npm:^10.2.2, glob@npm:^10.3.10": version: 10.5.0 resolution: "glob@npm:10.5.0" dependencies: @@ -15970,7 +16319,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"glob@npm:^11.0.0, glob@npm:^11.0.3": +"glob@npm:^11.0.3": version: 11.1.0 resolution: "glob@npm:11.1.0" dependencies: @@ -15986,18 +16335,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"glob@npm:^13.0.0": - version: 13.0.0 - resolution: "glob@npm:13.0.0" - dependencies: - minimatch: "npm:^10.1.1" - minipass: "npm:^7.1.2" - path-scurry: "npm:^2.0.0" - checksum: 10/de390721d29ee1c9ea41e40ec2aa0de2cabafa68022e237dc4297665a5e4d650776f2573191984ea1640aba1bf0ea34eddef2d8cbfbfc2ad24b5fb0af41d8846 - languageName: node - linkType: hard - -"glob@npm:^7.0.5, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7": +"glob@npm:^7.0.5, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.7": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -16033,13 +16371,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"globals@npm:^11.1.0": - version: 11.12.0 - resolution: "globals@npm:11.12.0" - checksum: 10/9f054fa38ff8de8fa356502eb9d2dae0c928217b8b5c8de1f09f5c9b6c8a96d8b9bd3afc49acbcd384a98a81fea713c859e1b09e214c60509517bb8fc2bc13c2 - languageName: node - linkType: hard - "globals@npm:^14.0.0": version: 14.0.0 resolution: "globals@npm:14.0.0" @@ -16047,13 +16378,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"globals@npm:^15.11.0, globals@npm:^15.14.0, globals@npm:^15.15.0": +"globals@npm:^15.11.0, globals@npm:^15.15.0": version: 15.15.0 resolution: "globals@npm:15.15.0" checksum: 10/7f561c87b2fd381b27fc2db7df8a4ea7a9bb378667b8a7193e61fd2ca3a876479174e2a303a74345fbea6e1242e16db48915c1fd3bf35adcf4060a795b425e18 languageName: node linkType: hard +"globals@npm:^17.3.0": + version: 17.3.0 + resolution: "globals@npm:17.3.0" + checksum: 10/44ba2b7db93eb6a2531dfba09219845e21f2e724a4f400eb59518b180b7d5bcf7f65580530e3d3023d7dc2bdbacf5d265fd87c393f567deb9a2b0472b51c9d5e + languageName: node + linkType: hard + "globalthis@npm:^1.0.3, globalthis@npm:^1.0.4": version: 1.0.4 resolution: "globalthis@npm:1.0.4" @@ -16157,13 +16495,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"graphemer@npm:^1.4.0": - version: 1.4.0 - resolution: "graphemer@npm:1.4.0" - checksum: 10/6dd60dba97007b21e3a829fab3f771803cc1292977fe610e240ea72afd67e5690ac9eeaafc4a99710e78962e5936ab5a460787c2a1180f1cb0ccfac37d29f897 - languageName: node - linkType: hard - "gray-matter@npm:^4.0.3": version: 4.0.3 resolution: "gray-matter@npm:4.0.3" @@ -16176,13 +16507,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"gud@npm:^1.0.0": - version: 1.0.0 - resolution: "gud@npm:1.0.0" - checksum: 10/3e2eb37cf794364077c18f036d6aa259c821c7fd188f2b7935cb00d589d82a41e0ebb1be809e1a93679417f62f1ad0513e745c3cf5329596e489aef8c5e5feae - languageName: node - linkType: hard - "gzip-size@npm:^6.0.0": version: 6.0.0 resolution: "gzip-size@npm:6.0.0" @@ -16206,7 +16530,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"handlebars@npm:^4.7.7": +"handlebars@npm:^4.7.7, handlebars@npm:^4.7.8": version: 4.7.8 resolution: "handlebars@npm:4.7.8" dependencies: @@ -16309,24 +16633,25 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"hash-base@npm:^3.0.0": - version: 3.1.0 - resolution: "hash-base@npm:3.1.0" +"hash-base@npm:^3.0.0, hash-base@npm:^3.1.2": + version: 3.1.2 + resolution: "hash-base@npm:3.1.2" dependencies: inherits: "npm:^2.0.4" - readable-stream: "npm:^3.6.0" - safe-buffer: "npm:^5.2.0" - checksum: 10/26b7e97ac3de13cb23fc3145e7e3450b0530274a9562144fc2bf5c1e2983afd0e09ed7cc3b20974ba66039fad316db463da80eb452e7373e780cbee9a0d2f2dc + readable-stream: "npm:^2.3.8" + safe-buffer: "npm:^5.2.1" + to-buffer: "npm:^1.2.1" + checksum: 10/f2100420521ec77736ebd9279f2c0b3ab2820136a2fa408ea36f3201d3f6984cda166806e6a0287f92adf179430bedfbdd74348ac351e24a3eff9f01a8c406b0 languageName: node linkType: hard -"hash-base@npm:~3.0": - version: 3.0.4 - resolution: "hash-base@npm:3.0.4" +"hash-base@npm:~3.0.4": + version: 3.0.5 + resolution: "hash-base@npm:3.0.5" dependencies: - inherits: "npm:^2.0.1" - safe-buffer: "npm:^5.0.1" - checksum: 10/878465a0dfcc33cce195c2804135352c590d6d10980adc91a9005fd377e77f2011256c2b7cfce472e3f2e92d561d1bf3228d2da06348a9017ce9a258b3b49764 + inherits: "npm:^2.0.4" + safe-buffer: "npm:^5.2.1" + checksum: 10/6a82675a5de2ea9347501bbe655a2334950c7ec972fd9810ae9529e06aeab8f7e8ef68fc2112e5e6f0745561a7e05326efca42ad59bb5fd116537f5f8b0a216d languageName: node linkType: hard @@ -16610,15 +16935,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"html-encoding-sniffer@npm:^3.0.0": - version: 3.0.0 - resolution: "html-encoding-sniffer@npm:3.0.0" - dependencies: - whatwg-encoding: "npm:^2.0.0" - checksum: 10/707a812ec2acaf8bb5614c8618dc81e2fb6b4399d03e95ff18b65679989a072f4e919b9bef472039301a1bbfba64063ba4c79ea6e851c653ac9db80dbefe8fe5 - languageName: node - linkType: hard - "html-encoding-sniffer@npm:^4.0.0": version: 4.0.0 resolution: "html-encoding-sniffer@npm:4.0.0" @@ -16761,7 +17077,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"http-errors@npm:2.0.0, http-errors@npm:^2.0.0": +"http-errors@npm:2.0.0": version: 2.0.0 resolution: "http-errors@npm:2.0.0" dependencies: @@ -16774,6 +17090,19 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"http-errors@npm:^2.0.0, http-errors@npm:^2.0.1": + version: 2.0.1 + resolution: "http-errors@npm:2.0.1" + dependencies: + depd: "npm:~2.0.0" + inherits: "npm:~2.0.4" + setprototypeof: "npm:~1.2.0" + statuses: "npm:~2.0.2" + toidentifier: "npm:~1.0.1" + checksum: 10/9fe31bc0edf36566c87048aed1d3d0cbe03552564adc3541626a0613f542d753fbcb13bdfcec0a3a530dbe1714bb566c89d46244616b66bddd26ac413b06a207 + languageName: node + linkType: hard + "http-errors@npm:~1.6.2": version: 1.6.3 resolution: "http-errors@npm:1.6.3" @@ -16892,7 +17221,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"https-proxy-agent@npm:5.0.1, https-proxy-agent@npm:^5.0.0, https-proxy-agent@npm:^5.0.1": +"https-proxy-agent@npm:^5.0.0": version: 5.0.1 resolution: "https-proxy-agent@npm:5.0.1" dependencies: @@ -16945,21 +17274,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"i18next-browser-languagedetector@npm:^6.1.8": - version: 6.1.8 - resolution: "i18next-browser-languagedetector@npm:6.1.8" +"i18next-browser-languagedetector@npm:^8.2.0": + version: 8.2.0 + resolution: "i18next-browser-languagedetector@npm:8.2.0" dependencies: - "@babel/runtime": "npm:^7.19.0" - checksum: 10/a55e3fb432bbc361c7b37760d3a5496bfe54429d71c65802c7358570d03c04b9788650e377ba551d97f6ed4640b925f674a14164174e17fad035b25958f17cfa + "@babel/runtime": "npm:^7.23.2" + checksum: 10/b2e78feb256e92b219cd378b38af00404b8f667473f886d64c53588df0592bce145ad29ad7d8238c75f78a9c79f02c84d400569dd5c0ab94c751ac62a4e1f8dd languageName: node linkType: hard -"i18next-http-backend@npm:^1.4.5": - version: 1.4.5 - resolution: "i18next-http-backend@npm:1.4.5" +"i18next-http-backend@npm:^3.0.2": + version: 3.0.2 + resolution: "i18next-http-backend@npm:3.0.2" dependencies: - cross-fetch: "npm:3.1.5" - checksum: 10/9be57bc5f92dcd2fc63cfbe0618fb0ce8d8666720fbe60973cd13a9694d6ad0b4c0dd4ebb1347cf13bf6ea48493e045969a215b2b0920491f448841a9fbca0d2 + cross-fetch: "npm:4.0.0" + checksum: 10/fd78b755e4050b33cb0367a8542e80fa70b47eef02d856cce6730bde6faa1c77e304a5dc116b31dbe70695a62347332b873215b25f498ffb15d4ddb716f4ccd2 languageName: node linkType: hard @@ -16972,7 +17301,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": +"iconv-lite@npm:0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" dependencies: @@ -17047,7 +17376,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1, ignore@npm:^5.3.2": +"ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.2": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 @@ -17092,9 +17421,9 @@ asn1@evs-broadcast/node-asn1: linkType: hard "immutable@npm:^5.0.2": - version: 5.0.3 - resolution: "immutable@npm:5.0.3" - checksum: 10/9aca1c783951bb204d7036fbcefac6dd42e7c8ad77ff54b38c5fc0924e6e16ce2d123c95db47c1170ba63dd3f6fc7aa74a29be7adef984031936c4cd1e9e8554 + version: 5.1.5 + resolution: "immutable@npm:5.1.5" + checksum: 10/7aec2740239772ec8e92e793c991bd809203a97694f4ff3a18e50e28f9a6b02393ad033d87b458037bdf8c0ea37d4446d640e825f6171df3405cf6cf300ce028 languageName: node linkType: hard @@ -17108,15 +17437,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"import-in-the-middle@npm:1.12.0": - version: 1.12.0 - resolution: "import-in-the-middle@npm:1.12.0" +"import-in-the-middle@npm:1.14.4": + version: 1.14.4 + resolution: "import-in-the-middle@npm:1.14.4" dependencies: - acorn: "npm:^8.8.2" + acorn: "npm:^8.14.0" acorn-import-attributes: "npm:^1.9.5" cjs-module-lexer: "npm:^1.2.2" module-details-from-path: "npm:^1.0.3" - checksum: 10/73f3f0ad8c3fceb90bcf308e84609290fe912af32a4be12fce2bf1fde28a0cb12d7219e15e8fe9e8d7ceafcb115a49a66566c2fd973d0a08e33437b00dfce3f9 + checksum: 10/96b657cfe33dda86cc1160446039b1ff115154a0242ff26b275177621e12f88ba2b23df5f15e1fa8e5cba57ee8f8d02d353df0d2ec1b08d3a3503e3e4e987ab3 languageName: node linkType: hard @@ -17127,7 +17456,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"import-local@npm:3.1.0, import-local@npm:^3.0.2": +"import-local@npm:3.1.0": version: 3.1.0 resolution: "import-local@npm:3.1.0" dependencies: @@ -17139,6 +17468,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"import-local@npm:^3.2.0": + version: 3.2.0 + resolution: "import-local@npm:3.2.0" + dependencies: + pkg-dir: "npm:^4.2.0" + resolve-cwd: "npm:^3.0.0" + bin: + import-local-fixture: fixtures/cli.js + checksum: 10/0b0b0b412b2521739fbb85eeed834a3c34de9bc67e670b3d0b86248fc460d990a7b116ad056c084b87a693ef73d1f17268d6a5be626bb43c998a8b1c8a230004 + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -17184,10 +17525,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"influx@npm:^5.9.7": - version: 5.9.7 - resolution: "influx@npm:5.9.7" - checksum: 10/09ee08fc8ae963a45f60d4e6558df7231bc8891bc35720f378fc8399a9177e12d3d4d6784685345a206ebbe3d6c48f7b99c83ed94916f219f7d9ce065647d774 +"influx@npm:^5.12.0": + version: 5.12.0 + resolution: "influx@npm:5.12.0" + checksum: 10/3e0ec79775f444174a126d496b38515f703db605aed89acabac9796fa08b2d4d173361e8fe42fd69d692a4af0a5c48b11dfce211c73d0f2e8d160d2048e0bcba languageName: node linkType: hard @@ -17282,15 +17623,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"inquirer@npm:8.2.6": - version: 8.2.6 - resolution: "inquirer@npm:8.2.6" +"inquirer@npm:8.2.7": + version: 8.2.7 + resolution: "inquirer@npm:8.2.7" dependencies: + "@inquirer/external-editor": "npm:^1.0.0" ansi-escapes: "npm:^4.2.1" chalk: "npm:^4.1.1" cli-cursor: "npm:^3.1.0" cli-width: "npm:^3.0.0" - external-editor: "npm:^3.0.3" figures: "npm:^3.0.0" lodash: "npm:^4.17.21" mute-stream: "npm:0.0.8" @@ -17301,7 +17642,7 @@ asn1@evs-broadcast/node-asn1: strip-ansi: "npm:^6.0.0" through: "npm:^2.3.6" wrap-ansi: "npm:^6.0.1" - checksum: 10/f642b9e5a94faaba54f277bdda2af0e0a6b592bd7f88c60e1614b5795b19336c7025e0c2923915d5f494f600a02fe8517413779a794415bb79a9563b061d68ab + checksum: 10/526fb5ca55a29decda9b67c7b2bd437730152104c6e7c5f0d7ade90af6dc999371e1602ce86eb4a39ee3d91993501cddec32e4fe3f599723f2b653b02b685e3b languageName: node linkType: hard @@ -17408,13 +17749,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-arrayish@npm:^0.3.1": - version: 0.3.2 - resolution: "is-arrayish@npm:0.3.2" - checksum: 10/81a78d518ebd8b834523e25d102684ee0f7e98637136d3bdc93fd09636350fa06f1d8ca997ea28143d4d13cb1b69c0824f082db0ac13e1ab3311c10ffea60ade - languageName: node - linkType: hard - "is-async-function@npm:^2.0.0": version: 2.0.0 resolution: "is-async-function@npm:2.0.0" @@ -17479,6 +17813,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"is-core-module@npm:^2.16.1": + version: 2.16.1 + resolution: "is-core-module@npm:2.16.1" + dependencies: + hasown: "npm:^2.0.2" + checksum: 10/452b2c2fb7f889cbbf7e54609ef92cf6c24637c568acc7e63d166812a0fb365ae8a504c333a29add8bdb1686704068caa7f4e4b639b650dde4f00a038b8941fb + languageName: node + linkType: hard + "is-data-view@npm:^1.0.1, is-data-view@npm:^1.0.2": version: 1.0.2 resolution: "is-data-view@npm:1.0.2" @@ -17562,7 +17905,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-generator-fn@npm:^2.0.0": +"is-generator-fn@npm:^2.1.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" checksum: 10/a6ad5492cf9d1746f73b6744e0c43c0020510b59d56ddcb78a91cbc173f09b5e6beff53d75c9c5a29feb618bfef2bf458e025ecf3a57ad2268e2fb2569f56215 @@ -17744,13 +18087,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-promise@npm:^2.2.2": - version: 2.2.2 - resolution: "is-promise@npm:2.2.2" - checksum: 10/18bf7d1c59953e0ad82a1ed963fb3dc0d135c8f299a14f89a17af312fc918373136e56028e8831700e1933519630cc2fd4179a777030330fde20d34e96f40c78 - languageName: node - linkType: hard - "is-reference@npm:^3.0.0": version: 3.0.2 resolution: "is-reference@npm:3.0.2" @@ -17760,7 +18096,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-regex@npm:^1.1.4, is-regex@npm:^1.2.1": +"is-regex@npm:^1.2.1": version: 1.2.1 resolution: "is-regex@npm:1.2.1" dependencies: @@ -18011,29 +18347,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"istanbul-lib-instrument@npm:^5.0.4": - version: 5.2.1 - resolution: "istanbul-lib-instrument@npm:5.2.1" - dependencies: - "@babel/core": "npm:^7.12.3" - "@babel/parser": "npm:^7.14.7" - "@istanbuljs/schema": "npm:^0.1.2" - istanbul-lib-coverage: "npm:^3.2.0" - semver: "npm:^6.3.0" - checksum: 10/bbc4496c2f304d799f8ec22202ab38c010ac265c441947f075c0f7d46bd440b45c00e46017cf9053453d42182d768b1d6ed0e70a142c95ab00df9843aa5ab80e - languageName: node - linkType: hard - -"istanbul-lib-instrument@npm:^6.0.0": - version: 6.0.0 - resolution: "istanbul-lib-instrument@npm:6.0.0" +"istanbul-lib-instrument@npm:^6.0.0, istanbul-lib-instrument@npm:^6.0.2": + version: 6.0.3 + resolution: "istanbul-lib-instrument@npm:6.0.3" dependencies: - "@babel/core": "npm:^7.12.3" - "@babel/parser": "npm:^7.14.7" - "@istanbuljs/schema": "npm:^0.1.2" + "@babel/core": "npm:^7.23.9" + "@babel/parser": "npm:^7.23.9" + "@istanbuljs/schema": "npm:^0.1.3" istanbul-lib-coverage: "npm:^3.2.0" semver: "npm:^7.5.4" - checksum: 10/a52efe2170ac2deeaaacc84d10fe8de41d97264a86e57df77e05c1e72227a333280f640836137b28fda802a2c71b2affb00a703979e6f7a462cc80047a6aff21 + checksum: 10/aa5271c0008dfa71b6ecc9ba1e801bf77b49dc05524e8c30d58aaf5b9505e0cd12f25f93165464d4266a518c5c75284ecb598fbd89fec081ae77d2c9d3327695 languageName: node linkType: hard @@ -18048,14 +18371,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"istanbul-lib-source-maps@npm:^4.0.0": - version: 4.0.1 - resolution: "istanbul-lib-source-maps@npm:4.0.1" +"istanbul-lib-source-maps@npm:^5.0.0": + version: 5.0.6 + resolution: "istanbul-lib-source-maps@npm:5.0.6" dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.23" debug: "npm:^4.1.1" istanbul-lib-coverage: "npm:^3.0.0" - source-map: "npm:^0.6.1" - checksum: 10/5526983462799aced011d776af166e350191b816821ea7bcf71cab3e5272657b062c47dc30697a22a43656e3ced78893a42de677f9ccf276a28c913190953b82 + checksum: 10/569dd0a392ee3464b1fe1accbaef5cc26de3479eacb5b91d8c67ebb7b425d39fd02247d85649c3a0e9c29b600809fa60b5af5a281a75a89c01f385b1e24823a2 languageName: node linkType: hard @@ -18143,110 +18466,114 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jest-changed-files@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-changed-files@npm:29.7.0" +"jest-changed-files@npm:30.2.0": + version: 30.2.0 + resolution: "jest-changed-files@npm:30.2.0" dependencies: - execa: "npm:^5.0.0" - jest-util: "npm:^29.7.0" + execa: "npm:^5.1.1" + jest-util: "npm:30.2.0" p-limit: "npm:^3.1.0" - checksum: 10/3d93742e56b1a73a145d55b66e96711fbf87ef89b96c2fab7cfdfba8ec06612591a982111ca2b712bb853dbc16831ec8b43585a2a96b83862d6767de59cbf83d + checksum: 10/ff2275ed5839b88c12ffa66fdc5c17ba02d3e276be6b558bed92872c282d050c3fdd1a275a81187cbe35c16d6d40337b85838772836463c7a2fbd1cba9785ca0 languageName: node linkType: hard -"jest-circus@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-circus@npm:29.7.0" +"jest-circus@npm:30.2.0": + version: 30.2.0 + resolution: "jest-circus@npm:30.2.0" dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/expect": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" + "@jest/environment": "npm:30.2.0" + "@jest/expect": "npm:30.2.0" + "@jest/test-result": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - chalk: "npm:^4.0.0" + chalk: "npm:^4.1.2" co: "npm:^4.6.0" - dedent: "npm:^1.0.0" - is-generator-fn: "npm:^2.0.0" - jest-each: "npm:^29.7.0" - jest-matcher-utils: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-runtime: "npm:^29.7.0" - jest-snapshot: "npm:^29.7.0" - jest-util: "npm:^29.7.0" + dedent: "npm:^1.6.0" + is-generator-fn: "npm:^2.1.0" + jest-each: "npm:30.2.0" + jest-matcher-utils: "npm:30.2.0" + jest-message-util: "npm:30.2.0" + jest-runtime: "npm:30.2.0" + jest-snapshot: "npm:30.2.0" + jest-util: "npm:30.2.0" p-limit: "npm:^3.1.0" - pretty-format: "npm:^29.7.0" - pure-rand: "npm:^6.0.0" + pretty-format: "npm:30.2.0" + pure-rand: "npm:^7.0.0" slash: "npm:^3.0.0" - stack-utils: "npm:^2.0.3" - checksum: 10/716a8e3f40572fd0213bcfc1da90274bf30d856e5133af58089a6ce45089b63f4d679bd44e6be9d320e8390483ebc3ae9921981993986d21639d9019b523123d + stack-utils: "npm:^2.0.6" + checksum: 10/68bfc65d92385db1017643988215e4ff5af0b10bcab86fb749a063be6bb7d5eb556dc53dd21bedf833a19aa6ae1a781a8d27b2bea25562de02d294b3017435a9 languageName: node linkType: hard -"jest-cli@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-cli@npm:29.7.0" +"jest-cli@npm:30.2.0": + version: 30.2.0 + resolution: "jest-cli@npm:30.2.0" dependencies: - "@jest/core": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - chalk: "npm:^4.0.0" - create-jest: "npm:^29.7.0" - exit: "npm:^0.1.2" - import-local: "npm:^3.0.2" - jest-config: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-validate: "npm:^29.7.0" - yargs: "npm:^17.3.1" + "@jest/core": "npm:30.2.0" + "@jest/test-result": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + chalk: "npm:^4.1.2" + exit-x: "npm:^0.2.2" + import-local: "npm:^3.2.0" + jest-config: "npm:30.2.0" + jest-util: "npm:30.2.0" + jest-validate: "npm:30.2.0" + yargs: "npm:^17.7.2" peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true bin: - jest: bin/jest.js - checksum: 10/6cc62b34d002c034203065a31e5e9a19e7c76d9e8ef447a6f70f759c0714cb212c6245f75e270ba458620f9c7b26063cd8cf6cd1f7e3afd659a7cc08add17307 + jest: ./bin/jest.js + checksum: 10/1cc8304f0e2608801c84cdecce9565a6178f668a6475aed3767a1d82cc539915f98e7404d7c387510313684011dc3095c15397d6725f73aac80fbd96c4155faa languageName: node linkType: hard -"jest-config@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-config@npm:29.7.0" +"jest-config@npm:30.2.0": + version: 30.2.0 + resolution: "jest-config@npm:30.2.0" dependencies: - "@babel/core": "npm:^7.11.6" - "@jest/test-sequencer": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - babel-jest: "npm:^29.7.0" - chalk: "npm:^4.0.0" - ci-info: "npm:^3.2.0" - deepmerge: "npm:^4.2.2" - glob: "npm:^7.1.3" - graceful-fs: "npm:^4.2.9" - jest-circus: "npm:^29.7.0" - jest-environment-node: "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - jest-regex-util: "npm:^29.6.3" - jest-resolve: "npm:^29.7.0" - jest-runner: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-validate: "npm:^29.7.0" - micromatch: "npm:^4.0.4" + "@babel/core": "npm:^7.27.4" + "@jest/get-type": "npm:30.1.0" + "@jest/pattern": "npm:30.0.1" + "@jest/test-sequencer": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + babel-jest: "npm:30.2.0" + chalk: "npm:^4.1.2" + ci-info: "npm:^4.2.0" + deepmerge: "npm:^4.3.1" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.11" + jest-circus: "npm:30.2.0" + jest-docblock: "npm:30.2.0" + jest-environment-node: "npm:30.2.0" + jest-regex-util: "npm:30.0.1" + jest-resolve: "npm:30.2.0" + jest-runner: "npm:30.2.0" + jest-util: "npm:30.2.0" + jest-validate: "npm:30.2.0" + micromatch: "npm:^4.0.8" parse-json: "npm:^5.2.0" - pretty-format: "npm:^29.7.0" + pretty-format: "npm:30.2.0" slash: "npm:^3.0.0" strip-json-comments: "npm:^3.1.1" peerDependencies: "@types/node": "*" + esbuild-register: ">=3.4.0" ts-node: ">=9.0.0" peerDependenciesMeta: "@types/node": optional: true + esbuild-register: + optional: true ts-node: optional: true - checksum: 10/6bdf570e9592e7d7dd5124fc0e21f5fe92bd15033513632431b211797e3ab57eaa312f83cc6481b3094b72324e369e876f163579d60016677c117ec4853cf02b + checksum: 10/296786b0a3d62de77e2f691f208d54ab541c1a73f87747d922eda643c6f25b89125ef3150170c07a6c8a316a30c15428e46237d499f688b0777f38de8a61ad16 languageName: node linkType: hard -"jest-diff@npm:>=30.0.0 < 31, jest-diff@npm:^30.0.2": +"jest-diff@npm:30.2.0, jest-diff@npm:>=30.0.0 < 31, jest-diff@npm:^30.0.2": version: 30.2.0 resolution: "jest-diff@npm:30.2.0" dependencies: @@ -18258,168 +18585,147 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jest-diff@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-diff@npm:29.7.0" - dependencies: - chalk: "npm:^4.0.0" - diff-sequences: "npm:^29.6.3" - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.7.0" - checksum: 10/6f3a7eb9cd9de5ea9e5aa94aed535631fa6f80221832952839b3cb59dd419b91c20b73887deb0b62230d06d02d6b6cf34ebb810b88d904bb4fe1e2e4f0905c98 - languageName: node - linkType: hard - -"jest-docblock@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-docblock@npm:29.7.0" +"jest-docblock@npm:30.2.0": + version: 30.2.0 + resolution: "jest-docblock@npm:30.2.0" dependencies: - detect-newline: "npm:^3.0.0" - checksum: 10/8d48818055bc96c9e4ec2e217a5a375623c0d0bfae8d22c26e011074940c202aa2534a3362294c81d981046885c05d304376afba9f2874143025981148f3e96d + detect-newline: "npm:^3.1.0" + checksum: 10/e01a7d1193947ed0f9713c26bfc7852e51cb758cafec807e5665a0a8d582473a43778bee099f8aa5c70b2941963e5341f4b10bd86b036a4fa3bcec0f4c04e099 languageName: node linkType: hard -"jest-each@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-each@npm:29.7.0" +"jest-each@npm:30.2.0": + version: 30.2.0 + resolution: "jest-each@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" - chalk: "npm:^4.0.0" - jest-get-type: "npm:^29.6.3" - jest-util: "npm:^29.7.0" - pretty-format: "npm:^29.7.0" - checksum: 10/bd1a077654bdaa013b590deb5f7e7ade68f2e3289180a8c8f53bc8a49f3b40740c0ec2d3a3c1aee906f682775be2bebbac37491d80b634d15276b0aa0f2e3fda + "@jest/get-type": "npm:30.1.0" + "@jest/types": "npm:30.2.0" + chalk: "npm:^4.1.2" + jest-util: "npm:30.2.0" + pretty-format: "npm:30.2.0" + checksum: 10/f95e7dc1cef4b6a77899325702a214834ae25d01276cc31279654dc7e04f63c1925a37848dd16a0d16508c0fd3d182145f43c10af93952b7a689df3aeac198e9 languageName: node linkType: hard -"jest-environment-jsdom@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-environment-jsdom@npm:29.7.0" +"jest-environment-jsdom@npm:^30.2.0": + version: 30.2.0 + resolution: "jest-environment-jsdom@npm:30.2.0" dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/fake-timers": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - "@types/jsdom": "npm:^20.0.0" + "@jest/environment": "npm:30.2.0" + "@jest/environment-jsdom-abstract": "npm:30.2.0" + "@types/jsdom": "npm:^21.1.7" "@types/node": "npm:*" - jest-mock: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jsdom: "npm:^20.0.0" + jsdom: "npm:^26.1.0" peerDependencies: - canvas: ^2.5.0 + canvas: ^3.0.0 peerDependenciesMeta: canvas: optional: true - checksum: 10/23bbfc9bca914baef4b654f7983175a4d49b0f515a5094ebcb8f819f28ec186f53c0ba06af1855eac04bab1457f4ea79dae05f70052cf899863e8096daa6e0f5 + checksum: 10/bb3768b7efc2eefb81b9deb1e23898cc74e4813d6d54872ed40d830eefc08c619eb0b2817f0af5d52061e0beb16681e8384d660a2aee4919e91349195ecb2904 languageName: node linkType: hard -"jest-environment-node@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-environment-node@npm:29.7.0" +"jest-environment-node@npm:30.2.0": + version: 30.2.0 + resolution: "jest-environment-node@npm:30.2.0" dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/fake-timers": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" + "@jest/environment": "npm:30.2.0" + "@jest/fake-timers": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - jest-mock: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - checksum: 10/9cf7045adf2307cc93aed2f8488942e39388bff47ec1df149a997c6f714bfc66b2056768973770d3f8b1bf47396c19aa564877eb10ec978b952c6018ed1bd637 - languageName: node - linkType: hard - -"jest-get-type@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-get-type@npm:29.6.3" - checksum: 10/88ac9102d4679d768accae29f1e75f592b760b44277df288ad76ce5bf038c3f5ce3719dea8aa0f035dac30e9eb034b848ce716b9183ad7cc222d029f03e92205 + jest-mock: "npm:30.2.0" + jest-util: "npm:30.2.0" + jest-validate: "npm:30.2.0" + checksum: 10/7918bfea7367bd3e12dbbc4ea5afb193b5c47e480a6d1382512f051e2f028458fc9f5ef2f6260737ad41a0b1894661790ff3aaf3cbb4148a33ce2ce7aec64847 languageName: node linkType: hard -"jest-haste-map@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-haste-map@npm:29.7.0" +"jest-haste-map@npm:30.2.0": + version: 30.2.0 + resolution: "jest-haste-map@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" - "@types/graceful-fs": "npm:^4.1.3" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - anymatch: "npm:^3.0.3" - fb-watchman: "npm:^2.0.0" - fsevents: "npm:^2.3.2" - graceful-fs: "npm:^4.2.9" - jest-regex-util: "npm:^29.6.3" - jest-util: "npm:^29.7.0" - jest-worker: "npm:^29.7.0" - micromatch: "npm:^4.0.4" + anymatch: "npm:^3.1.3" + fb-watchman: "npm:^2.0.2" + fsevents: "npm:^2.3.3" + graceful-fs: "npm:^4.2.11" + jest-regex-util: "npm:30.0.1" + jest-util: "npm:30.2.0" + jest-worker: "npm:30.2.0" + micromatch: "npm:^4.0.8" walker: "npm:^1.0.8" dependenciesMeta: fsevents: optional: true - checksum: 10/8531b42003581cb18a69a2774e68c456fb5a5c3280b1b9b77475af9e346b6a457250f9d756bfeeae2fe6cbc9ef28434c205edab9390ee970a919baddfa08bb85 + checksum: 10/a88be6b0b672144aa30fe2d72e630d639c8d8729ee2cef84d0f830eac2005ac021cd8354f8ed8ecd74223f6a8b281efb62f466f5c9e01ed17650e38761051f4c languageName: node linkType: hard -"jest-leak-detector@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-leak-detector@npm:29.7.0" +"jest-leak-detector@npm:30.2.0": + version: 30.2.0 + resolution: "jest-leak-detector@npm:30.2.0" dependencies: - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.7.0" - checksum: 10/e3950e3ddd71e1d0c22924c51a300a1c2db6cf69ec1e51f95ccf424bcc070f78664813bef7aed4b16b96dfbdeea53fe358f8aeaaea84346ae15c3735758f1605 + "@jest/get-type": "npm:30.1.0" + pretty-format: "npm:30.2.0" + checksum: 10/c430d6ed7910b2174738fbdca4ea64cbfe805216414c0d143c1090148f1389fec99d0733c0a8ed0a86709c89b4a4085b4749ac3a2cbc7deaf3ca87457afd24fc languageName: node linkType: hard -"jest-matcher-utils@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-matcher-utils@npm:29.7.0" +"jest-matcher-utils@npm:30.2.0": + version: 30.2.0 + resolution: "jest-matcher-utils@npm:30.2.0" dependencies: - chalk: "npm:^4.0.0" - jest-diff: "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - pretty-format: "npm:^29.7.0" - checksum: 10/981904a494299cf1e3baed352f8a3bd8b50a8c13a662c509b6a53c31461f94ea3bfeffa9d5efcfeb248e384e318c87de7e3baa6af0f79674e987482aa189af40 + "@jest/get-type": "npm:30.1.0" + chalk: "npm:^4.1.2" + jest-diff: "npm:30.2.0" + pretty-format: "npm:30.2.0" + checksum: 10/f3f1ecf68ca63c9d1d80a175637a8fc655edfd1ee83220f6e3f6bd464ecbe2f93148fdd440a5a5e5a2b0b2cc8ee84ddc3dcef58a6dbc66821c792f48d260c6d4 languageName: node linkType: hard -"jest-message-util@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-message-util@npm:29.7.0" +"jest-message-util@npm:30.2.0": + version: 30.2.0 + resolution: "jest-message-util@npm:30.2.0" dependencies: - "@babel/code-frame": "npm:^7.12.13" - "@jest/types": "npm:^29.6.3" - "@types/stack-utils": "npm:^2.0.0" - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" - micromatch: "npm:^4.0.4" - pretty-format: "npm:^29.7.0" + "@babel/code-frame": "npm:^7.27.1" + "@jest/types": "npm:30.2.0" + "@types/stack-utils": "npm:^2.0.3" + chalk: "npm:^4.1.2" + graceful-fs: "npm:^4.2.11" + micromatch: "npm:^4.0.8" + pretty-format: "npm:30.2.0" slash: "npm:^3.0.0" - stack-utils: "npm:^2.0.3" - checksum: 10/31d53c6ed22095d86bab9d14c0fa70c4a92c749ea6ceece82cf30c22c9c0e26407acdfbdb0231435dc85a98d6d65ca0d9cbcd25cd1abb377fe945e843fb770b9 + stack-utils: "npm:^2.0.6" + checksum: 10/e29ec76e8c8e4da5f5b25198be247535626ccf3a940e93fdd51fc6a6bcf70feaa2921baae3806182a090431d90b08c939eb13fb64249b171d2e9ae3a452a8fd2 languageName: node linkType: hard -"jest-mock-extended@npm:^3.0.7": - version: 3.0.7 - resolution: "jest-mock-extended@npm:3.0.7" +"jest-mock-extended@npm:^4.0.0": + version: 4.0.0 + resolution: "jest-mock-extended@npm:4.0.0" dependencies: - ts-essentials: "npm:^10.0.0" + ts-essentials: "npm:^10.0.2" peerDependencies: - jest: ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0 + "@jest/globals": ^28.0.0 || ^29.0.0 || ^30.0.0 + jest: ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0 || ^30.0.0 typescript: ^3.0.0 || ^4.0.0 || ^5.0.0 - checksum: 10/7d5fb9d4ad07dbed9d4f1dd011eb26ca20d9ca4aab3c807749f761220315ef8a6bdf767b1ce1e68ae10405e35ba899c4fcee55cf327deb2d9950910e818f40fa + checksum: 10/b2c1f8d28d671acabfc6f84ec0081e2a3793eb32dbb8a28950e235b9c0b691b0f920181b1852c2a37db8d6d19b828042f16615851faaf485b8ba6857050d6c79 languageName: node linkType: hard -"jest-mock@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-mock@npm:29.7.0" +"jest-mock@npm:30.2.0": + version: 30.2.0 + resolution: "jest-mock@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - jest-util: "npm:^29.7.0" - checksum: 10/ae51d1b4f898724be5e0e52b2268a68fcd876d9b20633c864a6dd6b1994cbc48d62402b0f40f3a1b669b30ebd648821f086c26c08ffde192ced951ff4670d51c + jest-util: "npm:30.2.0" + checksum: 10/cde9b56805f90bf811a9231873ee88a0fb83bf4bf50972ae76960725da65220fcb119688f2e90e1ef33fbfd662194858d7f43809d881f1c41bb55d94e62adeab languageName: node linkType: hard -"jest-pnp-resolver@npm:^1.2.2": +"jest-pnp-resolver@npm:^1.2.3": version: 1.2.3 resolution: "jest-pnp-resolver@npm:1.2.3" peerDependencies: @@ -18431,128 +18737,143 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jest-regex-util@npm:^29.6.3": - version: 29.6.3 - resolution: "jest-regex-util@npm:29.6.3" - checksum: 10/0518beeb9bf1228261695e54f0feaad3606df26a19764bc19541e0fc6e2a3737191904607fb72f3f2ce85d9c16b28df79b7b1ec9443aa08c3ef0e9efda6f8f2a +"jest-regex-util@npm:30.0.1": + version: 30.0.1 + resolution: "jest-regex-util@npm:30.0.1" + checksum: 10/fa8dac80c3e94db20d5e1e51d1bdf101cf5ede8f4e0b8f395ba8b8ea81e71804ffd747452a6bb6413032865de98ac656ef8ae43eddd18d980b6442a2764ed562 languageName: node linkType: hard -"jest-resolve-dependencies@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-resolve-dependencies@npm:29.7.0" +"jest-resolve-dependencies@npm:30.2.0": + version: 30.2.0 + resolution: "jest-resolve-dependencies@npm:30.2.0" dependencies: - jest-regex-util: "npm:^29.6.3" - jest-snapshot: "npm:^29.7.0" - checksum: 10/1e206f94a660d81e977bcfb1baae6450cb4a81c92e06fad376cc5ea16b8e8c6ea78c383f39e95591a9eb7f925b6a1021086c38941aa7c1b8a6a813c2f6e93675 + jest-regex-util: "npm:30.0.1" + jest-snapshot: "npm:30.2.0" + checksum: 10/0ff1a574f8c07f2e54a4ac8ab17aea00dfe2982e99b03fbd44f4211a94b8e5a59fdc43a59f9d6c0578a10a7b56a0611ad5ab40e4893973ff3f40dd414433b194 languageName: node linkType: hard -"jest-resolve@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-resolve@npm:29.7.0" +"jest-resolve@npm:30.2.0": + version: 30.2.0 + resolution: "jest-resolve@npm:30.2.0" dependencies: - chalk: "npm:^4.0.0" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.7.0" - jest-pnp-resolver: "npm:^1.2.2" - jest-util: "npm:^29.7.0" - jest-validate: "npm:^29.7.0" - resolve: "npm:^1.20.0" - resolve.exports: "npm:^2.0.0" + chalk: "npm:^4.1.2" + graceful-fs: "npm:^4.2.11" + jest-haste-map: "npm:30.2.0" + jest-pnp-resolver: "npm:^1.2.3" + jest-util: "npm:30.2.0" + jest-validate: "npm:30.2.0" slash: "npm:^3.0.0" - checksum: 10/faa466fd9bc69ea6c37a545a7c6e808e073c66f46ab7d3d8a6ef084f8708f201b85d5fe1799789578b8b47fa1de47b9ee47b414d1863bc117a49e032ba77b7c7 + unrs-resolver: "npm:^1.7.11" + checksum: 10/e1f03da6811a946f5d885ea739a973975d099cc760641f9e1f90ac9c6621408538ba1e909f789d45d6e8d2411b78fb09230f16f15669621aa407aed7511fdf01 languageName: node linkType: hard -"jest-runner@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-runner@npm:29.7.0" +"jest-runner@npm:30.2.0": + version: 30.2.0 + resolution: "jest-runner@npm:30.2.0" dependencies: - "@jest/console": "npm:^29.7.0" - "@jest/environment": "npm:^29.7.0" - "@jest/test-result": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" + "@jest/console": "npm:30.2.0" + "@jest/environment": "npm:30.2.0" + "@jest/test-result": "npm:30.2.0" + "@jest/transform": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - chalk: "npm:^4.0.0" + chalk: "npm:^4.1.2" emittery: "npm:^0.13.1" - graceful-fs: "npm:^4.2.9" - jest-docblock: "npm:^29.7.0" - jest-environment-node: "npm:^29.7.0" - jest-haste-map: "npm:^29.7.0" - jest-leak-detector: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-resolve: "npm:^29.7.0" - jest-runtime: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - jest-watcher: "npm:^29.7.0" - jest-worker: "npm:^29.7.0" + exit-x: "npm:^0.2.2" + graceful-fs: "npm:^4.2.11" + jest-docblock: "npm:30.2.0" + jest-environment-node: "npm:30.2.0" + jest-haste-map: "npm:30.2.0" + jest-leak-detector: "npm:30.2.0" + jest-message-util: "npm:30.2.0" + jest-resolve: "npm:30.2.0" + jest-runtime: "npm:30.2.0" + jest-util: "npm:30.2.0" + jest-watcher: "npm:30.2.0" + jest-worker: "npm:30.2.0" p-limit: "npm:^3.1.0" source-map-support: "npm:0.5.13" - checksum: 10/9d8748a494bd90f5c82acea99be9e99f21358263ce6feae44d3f1b0cd90991b5df5d18d607e73c07be95861ee86d1cbab2a3fc6ca4b21805f07ac29d47c1da1e + checksum: 10/d3706aa70e64a7ef8b38360d34ea6c261ba4d0b42136d7fb603c4fa71c24fa81f22c39ed2e39ee0db2363a42827810291f3ceb6a299e5996b41d701ad9b24184 languageName: node linkType: hard -"jest-runtime@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-runtime@npm:29.7.0" - dependencies: - "@jest/environment": "npm:^29.7.0" - "@jest/fake-timers": "npm:^29.7.0" - "@jest/globals": "npm:^29.7.0" - "@jest/source-map": "npm:^29.6.3" - "@jest/test-result": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" +"jest-runtime@npm:30.2.0": + version: 30.2.0 + resolution: "jest-runtime@npm:30.2.0" + dependencies: + "@jest/environment": "npm:30.2.0" + "@jest/fake-timers": "npm:30.2.0" + "@jest/globals": "npm:30.2.0" + "@jest/source-map": "npm:30.0.1" + "@jest/test-result": "npm:30.2.0" + "@jest/transform": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - chalk: "npm:^4.0.0" - cjs-module-lexer: "npm:^1.0.0" - collect-v8-coverage: "npm:^1.0.0" - glob: "npm:^7.1.3" - graceful-fs: "npm:^4.2.9" - jest-haste-map: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-mock: "npm:^29.7.0" - jest-regex-util: "npm:^29.6.3" - jest-resolve: "npm:^29.7.0" - jest-snapshot: "npm:^29.7.0" - jest-util: "npm:^29.7.0" + chalk: "npm:^4.1.2" + cjs-module-lexer: "npm:^2.1.0" + collect-v8-coverage: "npm:^1.0.2" + glob: "npm:^10.3.10" + graceful-fs: "npm:^4.2.11" + jest-haste-map: "npm:30.2.0" + jest-message-util: "npm:30.2.0" + jest-mock: "npm:30.2.0" + jest-regex-util: "npm:30.0.1" + jest-resolve: "npm:30.2.0" + jest-snapshot: "npm:30.2.0" + jest-util: "npm:30.2.0" slash: "npm:^3.0.0" strip-bom: "npm:^4.0.0" - checksum: 10/59eb58eb7e150e0834a2d0c0d94f2a0b963ae7182cfa6c63f2b49b9c6ef794e5193ef1634e01db41420c36a94cefc512cdd67a055cd3e6fa2f41eaf0f82f5a20 + checksum: 10/81a3a9951420863f001e74c510bf35b85ae983f636f43ee1ffa1618b5a8ddafb681bc2810f71814bc8c8373e9593c89576b2325daf3c765e50057e48d5941df3 languageName: node linkType: hard -"jest-snapshot@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-snapshot@npm:29.7.0" - dependencies: - "@babel/core": "npm:^7.11.6" - "@babel/generator": "npm:^7.7.2" - "@babel/plugin-syntax-jsx": "npm:^7.7.2" - "@babel/plugin-syntax-typescript": "npm:^7.7.2" - "@babel/types": "npm:^7.3.3" - "@jest/expect-utils": "npm:^29.7.0" - "@jest/transform": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - babel-preset-current-node-syntax: "npm:^1.0.0" - chalk: "npm:^4.0.0" - expect: "npm:^29.7.0" - graceful-fs: "npm:^4.2.9" - jest-diff: "npm:^29.7.0" - jest-get-type: "npm:^29.6.3" - jest-matcher-utils: "npm:^29.7.0" - jest-message-util: "npm:^29.7.0" - jest-util: "npm:^29.7.0" - natural-compare: "npm:^1.4.0" - pretty-format: "npm:^29.7.0" - semver: "npm:^7.5.3" - checksum: 10/cb19a3948256de5f922d52f251821f99657339969bf86843bd26cf3332eae94883e8260e3d2fba46129a27c3971c1aa522490e460e16c7fad516e82d10bbf9f8 +"jest-snapshot@npm:30.2.0": + version: 30.2.0 + resolution: "jest-snapshot@npm:30.2.0" + dependencies: + "@babel/core": "npm:^7.27.4" + "@babel/generator": "npm:^7.27.5" + "@babel/plugin-syntax-jsx": "npm:^7.27.1" + "@babel/plugin-syntax-typescript": "npm:^7.27.1" + "@babel/types": "npm:^7.27.3" + "@jest/expect-utils": "npm:30.2.0" + "@jest/get-type": "npm:30.1.0" + "@jest/snapshot-utils": "npm:30.2.0" + "@jest/transform": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + babel-preset-current-node-syntax: "npm:^1.2.0" + chalk: "npm:^4.1.2" + expect: "npm:30.2.0" + graceful-fs: "npm:^4.2.11" + jest-diff: "npm:30.2.0" + jest-matcher-utils: "npm:30.2.0" + jest-message-util: "npm:30.2.0" + jest-util: "npm:30.2.0" + pretty-format: "npm:30.2.0" + semver: "npm:^7.7.2" + synckit: "npm:^0.11.8" + checksum: 10/119390b49f397ed622ba7c375fc15f97af67c4fc49a34cf829c86ee732be2b06ad3c7171c76bb842a0e84a234783f1a4c721909aa316fbe00c6abc7c5962dfbc languageName: node linkType: hard -"jest-util@npm:^29.0.0, jest-util@npm:^29.7.0": +"jest-util@npm:30.2.0": + version: 30.2.0 + resolution: "jest-util@npm:30.2.0" + dependencies: + "@jest/types": "npm:30.2.0" + "@types/node": "npm:*" + chalk: "npm:^4.1.2" + ci-info: "npm:^4.2.0" + graceful-fs: "npm:^4.2.11" + picomatch: "npm:^4.0.2" + checksum: 10/cf2f2fb83417ea69f9992121561c95cf4e9aad7946819b771b8b52addf78811101b33b51d0a39fa0c305f2751dab262feed7699de052659ff03d51827c8862f5 + languageName: node + linkType: hard + +"jest-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-util@npm:29.7.0" dependencies: @@ -18566,33 +18887,46 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jest-validate@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-validate@npm:29.7.0" +"jest-validate@npm:30.2.0": + version: 30.2.0 + resolution: "jest-validate@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" - camelcase: "npm:^6.2.0" - chalk: "npm:^4.0.0" - jest-get-type: "npm:^29.6.3" + "@jest/get-type": "npm:30.1.0" + "@jest/types": "npm:30.2.0" + camelcase: "npm:^6.3.0" + chalk: "npm:^4.1.2" leven: "npm:^3.1.0" - pretty-format: "npm:^29.7.0" - checksum: 10/8ee1163666d8eaa16d90a989edba2b4a3c8ab0ffaa95ad91b08ca42b015bfb70e164b247a5b17f9de32d096987cada63ed8491ab82761bfb9a28bc34b27ae161 + pretty-format: "npm:30.2.0" + checksum: 10/61e66c6df29a1e181f8de063678dd2096bb52cc8a8ead3c9a3f853d54eca458ad04c7fb81931d9274affb67d0504a91a2a520456a139a26665810c3bf039b677 languageName: node linkType: hard -"jest-watcher@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-watcher@npm:29.7.0" +"jest-watcher@npm:30.2.0": + version: 30.2.0 + resolution: "jest-watcher@npm:30.2.0" dependencies: - "@jest/test-result": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" + "@jest/test-result": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - ansi-escapes: "npm:^4.2.1" - chalk: "npm:^4.0.0" + ansi-escapes: "npm:^4.3.2" + chalk: "npm:^4.1.2" emittery: "npm:^0.13.1" - jest-util: "npm:^29.7.0" - string-length: "npm:^4.0.1" - checksum: 10/4f616e0345676631a7034b1d94971aaa719f0cd4a6041be2aa299be437ea047afd4fe05c48873b7963f5687a2f6c7cbf51244be8b14e313b97bfe32b1e127e55 + jest-util: "npm:30.2.0" + string-length: "npm:^4.0.2" + checksum: 10/fa38d06dcc59dbbd6a9ff22dea499d3c81ed376d9993b82d01797a99bf466d48641a99b9f3670a4b5480ca31144c5e017b96b7059e4d7541358fb48cf517a2db + languageName: node + linkType: hard + +"jest-worker@npm:30.2.0": + version: 30.2.0 + resolution: "jest-worker@npm:30.2.0" + dependencies: + "@types/node": "npm:*" + "@ungap/structured-clone": "npm:^1.3.0" + jest-util: "npm:30.2.0" + merge-stream: "npm:^2.0.0" + supports-color: "npm:^8.1.1" + checksum: 10/9354b0c71c80173f673da6bbc0ddaad26e4395b06532f7332e0c1e93e855b873b10139b040e01eda77f3dc5a0b67613e2bd7c56c4947ee771acfc3611de2ca29 languageName: node linkType: hard @@ -18607,7 +18941,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jest-worker@npm:^29.4.3, jest-worker@npm:^29.7.0": +"jest-worker@npm:^29.4.3": version: 29.7.0 resolution: "jest-worker@npm:29.7.0" dependencies: @@ -18619,22 +18953,22 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jest@npm:^29.7.0": - version: 29.7.0 - resolution: "jest@npm:29.7.0" +"jest@npm:^30.2.0": + version: 30.2.0 + resolution: "jest@npm:30.2.0" dependencies: - "@jest/core": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" - import-local: "npm:^3.0.2" - jest-cli: "npm:^29.7.0" + "@jest/core": "npm:30.2.0" + "@jest/types": "npm:30.2.0" + import-local: "npm:^3.2.0" + jest-cli: "npm:30.2.0" peerDependencies: node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 peerDependenciesMeta: node-notifier: optional: true bin: - jest: bin/jest.js - checksum: 10/97023d78446098c586faaa467fbf2c6b07ff06e2c85a19e3926adb5b0effe9ac60c4913ae03e2719f9c01ae8ffd8d92f6b262cedb9555ceeb5d19263d8c6362a + jest: ./bin/jest.js + checksum: 10/61c9d100750e4354cd7305d1f3ba253ffde4deaf12cb4be4d42d54f2dd5986e383a39c4a8691dbdc3839c69094a52413ed36f1886540ac37b71914a990b810d0 languageName: node linkType: hard @@ -18704,53 +19038,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jsdom@npm:^20.0.0": - version: 20.0.3 - resolution: "jsdom@npm:20.0.3" - dependencies: - abab: "npm:^2.0.6" - acorn: "npm:^8.8.1" - acorn-globals: "npm:^7.0.0" - cssom: "npm:^0.5.0" - cssstyle: "npm:^2.3.0" - data-urls: "npm:^3.0.2" - decimal.js: "npm:^10.4.2" - domexception: "npm:^4.0.0" - escodegen: "npm:^2.0.0" - form-data: "npm:^4.0.0" - html-encoding-sniffer: "npm:^3.0.0" - http-proxy-agent: "npm:^5.0.0" - https-proxy-agent: "npm:^5.0.1" - is-potential-custom-element-name: "npm:^1.0.1" - nwsapi: "npm:^2.2.2" - parse5: "npm:^7.1.1" - saxes: "npm:^6.0.0" - symbol-tree: "npm:^3.2.4" - tough-cookie: "npm:^4.1.2" - w3c-xmlserializer: "npm:^4.0.0" - webidl-conversions: "npm:^7.0.0" - whatwg-encoding: "npm:^2.0.0" - whatwg-mimetype: "npm:^3.0.0" - whatwg-url: "npm:^11.0.0" - ws: "npm:^8.11.0" - xml-name-validator: "npm:^4.0.0" - peerDependencies: - canvas: ^2.5.0 - peerDependenciesMeta: - canvas: - optional: true - checksum: 10/a4cdcff5b07eed87da90b146b82936321533b5efe8124492acf7160ebd5b9cf2b3c2435683592bf1cffb479615245756efb6c173effc1906f845a86ed22af985 - languageName: node - linkType: hard - -"jsdom@npm:^26.0.0": - version: 26.0.0 - resolution: "jsdom@npm:26.0.0" +"jsdom@npm:^26.0.0, jsdom@npm:^26.1.0": + version: 26.1.0 + resolution: "jsdom@npm:26.1.0" dependencies: cssstyle: "npm:^4.2.1" data-urls: "npm:^5.0.0" - decimal.js: "npm:^10.4.3" - form-data: "npm:^4.0.1" + decimal.js: "npm:^10.5.0" html-encoding-sniffer: "npm:^4.0.0" http-proxy-agent: "npm:^7.0.2" https-proxy-agent: "npm:^7.0.6" @@ -18760,12 +19054,12 @@ asn1@evs-broadcast/node-asn1: rrweb-cssom: "npm:^0.8.0" saxes: "npm:^6.0.0" symbol-tree: "npm:^3.2.4" - tough-cookie: "npm:^5.0.0" + tough-cookie: "npm:^5.1.1" w3c-xmlserializer: "npm:^5.0.0" webidl-conversions: "npm:^7.0.0" whatwg-encoding: "npm:^3.1.1" whatwg-mimetype: "npm:^4.0.0" - whatwg-url: "npm:^14.1.0" + whatwg-url: "npm:^14.1.1" ws: "npm:^8.18.0" xml-name-validator: "npm:^5.0.0" peerDependencies: @@ -18773,7 +19067,7 @@ asn1@evs-broadcast/node-asn1: peerDependenciesMeta: canvas: optional: true - checksum: 10/8c230ee4657240bbbca6b4ebb484be53fc6a777a22a3357c80c5537222813666e3e1f54740bc13e769c461d9598ba7dac402c245949c6cef7ef7014ce6f36f01 + checksum: 10/39d78c4889cac20826393400dce1faed1666e9244fe0c8342a8f08c315375878e6be7fcfe339a33d6ff1a083bfe9e71b16d56ecf4d9a87db2da8c795925ea8c1 languageName: node linkType: hard @@ -18784,7 +19078,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jsesc@npm:^3.0.2": +"jsesc@npm:^3.0.2, jsesc@npm:~3.1.0": version: 3.1.0 resolution: "jsesc@npm:3.1.0" bin: @@ -18793,15 +19087,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jsesc@npm:~3.0.2": - version: 3.0.2 - resolution: "jsesc@npm:3.0.2" - bin: - jsesc: bin/jsesc - checksum: 10/8e5a7de6b70a8bd71f9cb0b5a7ade6a73ae6ab55e697c74cc997cede97417a3a65ed86c36f7dd6125fe49766e8386c845023d9e213916ca92c9dfdd56e2babf3 - languageName: node - linkType: hard - "json-bigint@npm:^1.0.0": version: 1.0.0 resolution: "json-bigint@npm:1.0.0" @@ -18880,37 +19165,22 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"json-schema-ref-parser@npm:^9.0.6": - version: 9.0.9 - resolution: "json-schema-ref-parser@npm:9.0.9" - dependencies: - "@apidevtools/json-schema-ref-parser": "npm:9.0.9" - checksum: 10/54f42b439abd865f9364e24f29e8e6849bae7565f40d11ef939e99e8285ad86673b7ff16f31398a15d6bff233844949b2f3f45de31d31393cc1c4d18957fc2e3 - languageName: node - linkType: hard - -"json-schema-to-typescript@npm:^10.1.5": - version: 10.1.5 - resolution: "json-schema-to-typescript@npm:10.1.5" +"json-schema-to-typescript@npm:^15.0.4": + version: 15.0.4 + resolution: "json-schema-to-typescript@npm:15.0.4" dependencies: - "@types/json-schema": "npm:^7.0.6" - "@types/lodash": "npm:^4.14.168" - "@types/prettier": "npm:^2.1.5" - cli-color: "npm:^2.0.0" - get-stdin: "npm:^8.0.0" - glob: "npm:^7.1.6" - glob-promise: "npm:^3.4.0" - is-glob: "npm:^4.0.1" - json-schema-ref-parser: "npm:^9.0.6" - json-stringify-safe: "npm:^5.0.1" - lodash: "npm:^4.17.20" - minimist: "npm:^1.2.5" - mkdirp: "npm:^1.0.4" - mz: "npm:^2.7.0" - prettier: "npm:^2.2.0" + "@apidevtools/json-schema-ref-parser": "npm:^11.5.5" + "@types/json-schema": "npm:^7.0.15" + "@types/lodash": "npm:^4.17.7" + is-glob: "npm:^4.0.3" + js-yaml: "npm:^4.1.0" + lodash: "npm:^4.17.21" + minimist: "npm:^1.2.8" + prettier: "npm:^3.2.5" + tinyglobby: "npm:^0.2.9" bin: json2ts: dist/src/cli.js - checksum: 10/2996f1a02e655720e753655f4810e65ed5f01fade56bcf6d8b20e19c40d0fc18f6a4e41164a2e3571aa68b4491a589954233c1461f1ca1a175e3230b7881447c + checksum: 10/99544c8b2e10f1487fd685357d8333e70f5eb9c1ba96fbdcc172d8cf62dc382158276ad82648a93911562f07da7c2adf7733d4608ffdeca9525d08d7930b9880 languageName: node linkType: hard @@ -19020,9 +19290,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jsonpath-plus@npm:^10.0.0": - version: 10.2.0 - resolution: "jsonpath-plus@npm:10.2.0" +"jsonpath-plus@npm:^10.0.7": + version: 10.4.0 + resolution: "jsonpath-plus@npm:10.4.0" dependencies: "@jsep-plugin/assignment": "npm:^1.3.0" "@jsep-plugin/regex": "npm:^1.0.4" @@ -19030,7 +19300,7 @@ asn1@evs-broadcast/node-asn1: bin: jsonpath: bin/jsonpath-cli.js jsonpath-plus: bin/jsonpath-cli.js - checksum: 10/3a6bd775d4348f5e014249a11abb635af2f1265d83ba716b3d633ca3f118e79c318223dd685170c50652494a492f3354163bbe4cd5554bb4d7992fecf53c4874 + checksum: 10/0ff33c7eb6500d7c8d789ce15a63ac2c46cb01b855f1c53729ca9e3833e0253af70277fc1799ebfe0b3130ddc03c127562669b999729dd11f2621b81472248d4 languageName: node linkType: hard @@ -19102,14 +19372,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"just-extend@npm:^6.2.0": - version: 6.2.0 - resolution: "just-extend@npm:6.2.0" - checksum: 10/1f487b074b9e5773befdd44dc5d1b446f01f24f7d4f1f255d51c0ef7f686e8eb5f95d983b792b9ca5c8b10cd7e60a924d64103725759eddbd7f18bcb22743f92 +"kairos-connection@npm:0.2.3": + version: 0.2.3 + resolution: "kairos-connection@npm:0.2.3" + dependencies: + kairos-lib: "npm:0.2.3" + tslib: "npm:^2.8.1" + checksum: 10/3efaf3d5775582362feb075bda46e10a7994ddd5981438b4a691b5d7aa5412d81e0f92199235ddfb17e814beceb5ffa589a30ae7295371465df7471bd668128f languageName: node linkType: hard -"kairos-lib@npm:^0.2.3": +"kairos-lib@npm:0.2.3, kairos-lib@npm:^0.2.3": version: 0.2.3 resolution: "kairos-lib@npm:0.2.3" dependencies: @@ -19182,9 +19455,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"koa@npm:^3.0.1": - version: 3.0.1 - resolution: "koa@npm:3.0.1" +"koa@npm:^3.1.1": + version: 3.1.1 + resolution: "koa@npm:3.1.1" dependencies: accepts: "npm:^1.3.8" content-disposition: "npm:~0.5.4" @@ -19204,7 +19477,7 @@ asn1@evs-broadcast/node-asn1: statuses: "npm:^2.0.1" type-is: "npm:^2.0.1" vary: "npm:^1.1.2" - checksum: 10/0e56f77f7192c10be6a3f5c4b248ec10b9b223e6894065a431df3c5c425db439fcc28ead29e7145087974550b51538790aafee6aeb5b0e26a32307d02a52bd41 + checksum: 10/b9f53e98752e73d2d3ed2df28a8062387e116d7053f3d655815ea7f1bae672f4f6afe41d6ff5f6cf429aad76aea4fd4424655bdcf6f8291a658108fe3aa2cf43 languageName: node linkType: hard @@ -19268,14 +19541,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lerna@npm:^9.0.0": - version: 9.0.3 - resolution: "lerna@npm:9.0.3" +"lerna@npm:^9.0.5": + version: 9.0.5 + resolution: "lerna@npm:9.0.5" dependencies: - "@lerna/create": "npm:9.0.3" + "@lerna/create": "npm:9.0.5" "@npmcli/arborist": "npm:9.1.6" "@npmcli/package-json": "npm:7.0.2" - "@npmcli/run-script": "npm:10.0.2" + "@npmcli/run-script": "npm:10.0.3" "@nx/devkit": "npm:>=21.5.2 < 23.0.0" "@octokit/plugin-enterprise-rest": "npm:6.0.1" "@octokit/rest": "npm:20.1.2" @@ -19312,7 +19585,7 @@ asn1@evs-broadcast/node-asn1: load-json-file: "npm:6.2.0" make-dir: "npm:4.0.0" make-fetch-happen: "npm:15.0.2" - minimatch: "npm:3.0.5" + minimatch: "npm:3.1.4" multimatch: "npm:5.0.0" npm-package-arg: "npm:13.0.1" npm-packlist: "npm:10.0.3" @@ -19328,14 +19601,14 @@ asn1@evs-broadcast/node-asn1: pify: "npm:5.0.0" read-cmd-shim: "npm:4.0.0" resolve-from: "npm:5.0.0" - rimraf: "npm:^4.4.1" + rimraf: "npm:^6.1.2" semver: "npm:7.7.2" set-blocking: "npm:^2.0.0" signal-exit: "npm:3.0.7" slash: "npm:3.0.0" ssri: "npm:12.0.0" string-width: "npm:^4.2.3" - tar: "npm:6.2.1" + tar: "npm:7.5.8" temp-dir: "npm:1.0.0" through: "npm:2.3.8" tinyglobby: "npm:0.2.12" @@ -19351,7 +19624,7 @@ asn1@evs-broadcast/node-asn1: yargs-parser: "npm:21.1.1" bin: lerna: dist/cli.js - checksum: 10/a0f16ed3a818e8dd814fc57d29b99e2cd804b1bc5246c0af635a11c055e66e481c5929c4677566eeadd27e269d7cb5a816dbadc4a433fae3bb3704b97fb14f93 + checksum: 10/c25b213edcee7267322acbad458c2a8eceb2c560fc37a5c5c8085483d945ae77ba9817223e7ab4bcef9ee57f9491de5ed891a4033eaa938e49cd9bb0e299aebb languageName: node linkType: hard @@ -19479,26 +19752,26 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "live-status-gateway@workspace:live-status-gateway" dependencies: - "@sofie-automation/blueprints-integration": "npm:1.53.0-in-development" - "@sofie-automation/corelib": "npm:1.53.0-in-development" - "@sofie-automation/live-status-gateway-api": "npm:1.53.0-in-development" - "@sofie-automation/server-core-integration": "npm:1.53.0-in-development" - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" - debug: "npm:^4.4.0" + "@sofie-automation/blueprints-integration": "npm:26.3.0-2" + "@sofie-automation/corelib": "npm:26.3.0-2" + "@sofie-automation/live-status-gateway-api": "npm:26.3.0-2" + "@sofie-automation/server-core-integration": "npm:26.3.0-2" + "@sofie-automation/shared-lib": "npm:26.3.0-2" + debug: "npm:^4.4.3" fast-clone: "npm:^1.5.13" - influx: "npm:^5.9.7" + influx: "npm:^5.12.0" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" underscore: "npm:^1.13.7" - winston: "npm:^3.17.0" - ws: "npm:^8.18.0" + winston: "npm:^3.19.0" + ws: "npm:^8.19.0" languageName: unknown linkType: soft -"load-esm@npm:1.0.2": - version: 1.0.2 - resolution: "load-esm@npm:1.0.2" - checksum: 10/1b4adb40c28c6fdbd4ca8c97942c04debddb3c93ae91413540ff5a21ca3511a651988c835cb80cad7288d1ecb869c4794b8a787ab02e09cc07ec951ad1eefcf9 +"load-esm@npm:1.0.3": + version: 1.0.3 + resolution: "load-esm@npm:1.0.3" + checksum: 10/6949e8c253dddccca2a0ded1e9e0bbbc81924439d99e3a7d0f946ba6cdcd16de22c3cef28d997fd950befda0826ca65c3f913d9ba893d50e9cfc0bbd9a2a1e90 languageName: node linkType: hard @@ -19526,10 +19799,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"loader-runner@npm:^4.2.0": - version: 4.3.0 - resolution: "loader-runner@npm:4.3.0" - checksum: 10/555ae002869c1e8942a0efd29a99b50a0ce6c3296efea95caf48f00d7f6f7f659203ed6613688b6181aa81dc76de3e65ece43094c6dffef3127fe1a84d973cd3 +"loader-runner@npm:^4.3.1": + version: 4.3.1 + resolution: "loader-runner@npm:4.3.1" + checksum: 10/d77127497c3f91fdba351e3e91156034e6e590e9f050b40df6c38ac16c54b5c903f7e2e141e09fefd046ee96b26fb50773c695ebc0aa205a4918683b124b04ba languageName: node linkType: hard @@ -19606,13 +19879,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lodash.get@npm:^4.4.2": - version: 4.4.2 - resolution: "lodash.get@npm:4.4.2" - checksum: 10/2a4925f6e89bc2c010a77a802d1ba357e17ed1ea03c2ddf6a146429f2856a216663e694a6aa3549a318cbbba3fd8b7decb392db457e6ac0b83dc745ed0a17380 - languageName: node - linkType: hard - "lodash.ismatch@npm:^4.4.0": version: 4.4.0 resolution: "lodash.ismatch@npm:4.4.0" @@ -19655,13 +19921,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lodash@npm:4.17.21": - version: 4.17.21 - resolution: "lodash@npm:4.17.21" - checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 - languageName: node - linkType: hard - "lodash@npm:^4, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:~4.17.21": version: 4.17.23 resolution: "lodash@npm:4.17.23" @@ -19739,17 +19998,22 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lottie-web@npm:5.5.9": - version: 5.5.9 - resolution: "lottie-web@npm:5.5.9" - checksum: 10/b7d05b58468fdf52a9f25581edc292d0ac47b0508d965c3debf2ee691d2a94593b61f096fe14eaefb8f09e0b869b158131aeea51541a3cff7e2dbfa5da996930 +"lottie-react@npm:^2.4.1": + version: 2.4.1 + resolution: "lottie-react@npm:2.4.1" + dependencies: + lottie-web: "npm:^5.10.2" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10/d1c54c3d90e322db988ea1dc92900a122e699e1d833368b8a817f4bd2ccc6d5600ab3cd0f34aa6e0bcbab28425f6de514b6d567e13164693cbde2c82af08fa06 languageName: node linkType: hard -"lottie-web@npm:^5.12.2": - version: 5.12.2 - resolution: "lottie-web@npm:5.12.2" - checksum: 10/cd377d54a675b37ac9359306b84097ea402dff3d74a2f45e6e0dbcff1df94b3a978e92e48fd34765754bdbb94bd2d8d4da31954d95f156e77489596b235cac91 +"lottie-web@npm:^5.10.2": + version: 5.13.0 + resolution: "lottie-web@npm:5.13.0" + checksum: 10/ccc65b91ddc569c874de265252ef41cb546798515dd63c5ee366844efd1e10335c080c483ce4305faba0cebd54c4afd6bb918fd0d6f4394dcc284fc0c3944941 languageName: node linkType: hard @@ -19822,15 +20086,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lru-queue@npm:^0.1.0": - version: 0.1.0 - resolution: "lru-queue@npm:0.1.0" - dependencies: - es5-ext: "npm:~0.10.2" - checksum: 10/55b08ee3a7dbefb7d8ee2d14e0a97c69a887f78bddd9e28a687a1944b57e09513d4b401db515279e8829d52331df12a767f3ed27ca67c3322c723cc25c06403f - languageName: node - linkType: hard - "lunr@npm:^2.3.9": version: 2.3.9 resolution: "lunr@npm:2.3.9" @@ -19948,25 +20203,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"make-fetch-happen@npm:^14.0.3": - version: 14.0.3 - resolution: "make-fetch-happen@npm:14.0.3" - dependencies: - "@npmcli/agent": "npm:^3.0.0" - cacache: "npm:^19.0.1" - http-cache-semantics: "npm:^4.1.1" - minipass: "npm:^7.0.2" - minipass-fetch: "npm:^4.0.0" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^1.0.0" - proc-log: "npm:^5.0.0" - promise-retry: "npm:^2.0.1" - ssri: "npm:^12.0.0" - checksum: 10/fce0385840b6d86b735053dfe941edc2dd6468fda80fe74da1eeff10cbd82a75760f406194f2bc2fa85b99545b2bc1f84c08ddf994b21830775ba2d1a87e8bdf - languageName: node - linkType: hard - "makeerror@npm:1.0.12": version: 1.0.12 resolution: "makeerror@npm:1.0.12" @@ -20035,8 +20271,8 @@ asn1@evs-broadcast/node-asn1: linkType: hard "markdown-it@npm:^14.1.0": - version: 14.1.0 - resolution: "markdown-it@npm:14.1.0" + version: 14.1.1 + resolution: "markdown-it@npm:14.1.1" dependencies: argparse: "npm:^2.0.1" entities: "npm:^4.4.0" @@ -20046,7 +20282,7 @@ asn1@evs-broadcast/node-asn1: uc.micro: "npm:^2.1.0" bin: markdown-it: bin/markdown-it.mjs - checksum: 10/f34f921be178ed0607ba9e3e27c733642be445e9bb6b1dba88da7aafe8ba1bc5d2f1c3aa8f3fc33b49a902da4e4c08c2feadfafb290b8c7dda766208bb6483a9 + checksum: 10/088822c8aa9346ba4af6a205f6ee0f4baae55e3314f040dc5c28c897d57d0f979840c71872b3582a6a6e572d8c851c54e323c82f4559011dfa2e96224fc20fc2 languageName: node linkType: hard @@ -20314,8 +20550,8 @@ asn1@evs-broadcast/node-asn1: linkType: hard "mdast-util-to-hast@npm:^13.0.0": - version: 13.1.0 - resolution: "mdast-util-to-hast@npm:13.1.0" + version: 13.2.1 + resolution: "mdast-util-to-hast@npm:13.2.1" dependencies: "@types/hast": "npm:^3.0.0" "@types/mdast": "npm:^4.0.0" @@ -20326,7 +20562,7 @@ asn1@evs-broadcast/node-asn1: unist-util-position: "npm:^5.0.0" unist-util-visit: "npm:^5.0.0" vfile: "npm:^6.0.0" - checksum: 10/50886f3fcbf23d74653287446f22f0b18b8f5297ae1ae74d904cd5751e47dd9e36efb9ffa81305dd136a9498a2660ba94024291887f22e06a910a5923d7dbadd + checksum: 10/8fddf5e66ea24dc85c8fe1cc2acd8fbe36e9d4f21b06322e156431fd71385eab9d2d767646f50276ca4ce3684cb967c4e226c60c3fff3428feb687ccb598fa39 languageName: node linkType: hard @@ -20433,22 +20669,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"memoizee@npm:^0.4.15": - version: 0.4.15 - resolution: "memoizee@npm:0.4.15" - dependencies: - d: "npm:^1.0.1" - es5-ext: "npm:^0.10.53" - es6-weak-map: "npm:^2.0.3" - event-emitter: "npm:^0.3.5" - is-promise: "npm:^2.2.2" - lru-queue: "npm:^0.1.0" - next-tick: "npm:^1.1.0" - timers-ext: "npm:^0.1.7" - checksum: 10/3c72cc59ae721e40980b604479e11e7d702f4167943f40f1e5c5d5da95e4b2664eec49ae533b2d41ffc938f642f145b48389ee4099e0945996fcf297e3dcb221 - languageName: node - linkType: hard - "memory-pager@npm:^1.0.2": version: 1.5.0 resolution: "memory-pager@npm:1.5.0" @@ -21043,7 +21263,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5, micromatch@npm:^4.0.8": +"micromatch@npm:^4.0.2, micromatch@npm:^4.0.5, micromatch@npm:^4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: @@ -21095,7 +21315,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.35, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -21183,7 +21403,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"minimatch@npm:10.1.1, minimatch@npm:^10.0.3, minimatch@npm:^10.1.1": +"minimatch@npm:10.1.1": version: 10.1.1 resolution: "minimatch@npm:10.1.1" dependencies: @@ -21192,48 +21412,57 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"minimatch@npm:3.0.5": - version: 3.0.5 - resolution: "minimatch@npm:3.0.5" +"minimatch@npm:3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" dependencies: brace-expansion: "npm:^1.1.7" - checksum: 10/8f9707491183a07a9542b8cf45aacb3745ba9fe6c611173fb225d7bf191e55416779aee31e17673a516a178af02d8d3d71ddd36ae3d5cc2495f627977ad1a012 + checksum: 10/e0b25b04cd4ec6732830344e5739b13f8690f8a012d73445a4a19fbc623f5dd481ef7a5827fde25954cd6026fede7574cc54dc4643c99d6c6b653d6203f94634 languageName: node linkType: hard -"minimatch@npm:3.1.2, minimatch@npm:^3.0.3, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" +"minimatch@npm:3.1.4": + version: 3.1.4 + resolution: "minimatch@npm:3.1.4" dependencies: brace-expansion: "npm:^1.1.7" - checksum: 10/e0b25b04cd4ec6732830344e5739b13f8690f8a012d73445a4a19fbc623f5dd481ef7a5827fde25954cd6026fede7574cc54dc4643c99d6c6b653d6203f94634 + checksum: 10/8d679c9df6caad31465c7681ae72b5e0f5d3b4fda6235c4473b14819f4d72ff8924ebd73ce991cc50be4b370daca51cc4d8c7fea6a3aa05108702ede115ab4c9 languageName: node linkType: hard -"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": - version: 5.1.6 - resolution: "minimatch@npm:5.1.6" +"minimatch@npm:^10.0.3, minimatch@npm:^10.1.1": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/126b36485b821daf96d33b5c821dac600cc1ab36c87e7a532594f9b1652b1fa89a1eebcaad4dff17c764dce1a7ac1531327f190fed5f97d8f6e5f889c116c429 + brace-expansion: "npm:^5.0.2" + checksum: 10/aea4874e521c55bb60744685bbffe3d152e5460f84efac3ea936e6bbe2ceba7deb93345fec3f9bb17f7b6946776073a64d40ae32bf5f298ad690308121068a1f languageName: node linkType: hard -"minimatch@npm:^8.0.2": - version: 8.0.4 - resolution: "minimatch@npm:8.0.4" +"minimatch@npm:^3.0.3, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10/b11a7ee5773cd34c1a0c8436cdbe910901018fb4b6cb47aa508a18d567f6efd2148507959e35fba798389b161b8604a2d704ccef751ea36bd4582f9852b7d63f + languageName: node + linkType: hard + +"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": + version: 5.1.9 + resolution: "minimatch@npm:5.1.9" dependencies: brace-expansion: "npm:^2.0.1" - checksum: 10/aef05598ee565e1013bc8a10f53410ac681561f901c1a084b8ecfd016c9ed919f58f4bbd5b63e05643189dfb26e8106a84f0e1ff12e4a263aa37e1cae7ce9828 + checksum: 10/23b4feb64dcb77ba93b70a72be551eb2e2677ac02178cf1ed3d38836cc4cd84802d90b77f60ef87f2bac64d270d2d8eba242e428f0554ea4e36bfdb7e9d25d0c languageName: node linkType: hard "minimatch@npm:^9.0.0, minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": - version: 9.0.5 - resolution: "minimatch@npm:9.0.5" + version: 9.0.9 + resolution: "minimatch@npm:9.0.9" dependencies: - brace-expansion: "npm:^2.0.1" - checksum: 10/dd6a8927b063aca6d910b119e1f2df6d2ce7d36eab91de83167dd136bb85e1ebff97b0d3de1cb08bd1f7e018ca170b4962479fefab5b2a69e2ae12cb2edc8348 + brace-expansion: "npm:^2.0.2" + checksum: 10/b91fad937deaffb68a45a2cb731ff3cff1c3baf9b6469c879477ed16f15c8f4ce39d63a3f75c2455107c2fdff0f3ab597d97dc09e2e93b883aafcf926ef0c8f9 languageName: node linkType: hard @@ -21255,7 +21484,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": +"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f @@ -21371,13 +21600,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"minipass@npm:^4.2.4": - version: 4.2.8 - resolution: "minipass@npm:4.2.8" - checksum: 10/e148eb6dcb85c980234cad889139ef8ddf9d5bdac534f4f0268446c8792dd4c74f4502479be48de3c1cce2f6450f6da4d0d4a86405a8a12be04c1c36b339569a - languageName: node - linkType: hard - "minipass@npm:^5.0.0": version: 5.0.0 resolution: "minipass@npm:5.0.0" @@ -21411,10 +21633,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mkdirp-classic@npm:^0.5.2": - version: 0.5.3 - resolution: "mkdirp-classic@npm:0.5.3" - checksum: 10/3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac +"mitt@npm:^3.0.1": + version: 3.0.1 + resolution: "mitt@npm:3.0.1" + checksum: 10/287c70d8e73ffc25624261a4989c783768aed95ecb60900f051d180cf83e311e3e59865bfd6e9d029cdb149dc20ba2f128a805e9429c5c4ce33b1416c65bbd14 languageName: node linkType: hard @@ -21489,7 +21711,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mongodb-connection-string-url@npm:^3.0.0": +"mongodb-connection-string-url@npm:^3.0.2": version: 3.0.2 resolution: "mongodb-connection-string-url@npm:3.0.2" dependencies: @@ -21499,20 +21721,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mongodb@npm:^6.12.0": - version: 6.13.0 - resolution: "mongodb@npm:6.13.0" +"mongodb@npm:^6.21.0": + version: 6.21.0 + resolution: "mongodb@npm:6.21.0" dependencies: - "@mongodb-js/saslprep": "npm:^1.1.9" - bson: "npm:^6.10.1" - mongodb-connection-string-url: "npm:^3.0.0" + "@mongodb-js/saslprep": "npm:^1.3.0" + bson: "npm:^6.10.4" + mongodb-connection-string-url: "npm:^3.0.2" peerDependencies: "@aws-sdk/credential-providers": ^3.188.0 "@mongodb-js/zstd": ^1.1.0 || ^2.0.0 gcp-metadata: ^5.2.0 kerberos: ^2.0.1 mongodb-client-encryption: ">=6.0.0 <7" - snappy: ^7.2.2 + snappy: ^7.3.2 socks: ^2.7.1 peerDependenciesMeta: "@aws-sdk/credential-providers": @@ -21529,7 +21751,7 @@ asn1@evs-broadcast/node-asn1: optional: true socks: optional: true - checksum: 10/769cc18eb3e34dabdbe56abd4862a1d79214fab79a96f8e8b0c67f2681dd002e88e0f8c869871b82a257297f87b318f930900eb65ad3378edc36bac5d2a7d542 + checksum: 10/28d2cab1c55c4cf58e410529ac6ae4c79a233adeb2147ba872d912819a0b496ee2dc5b9819ccbf0527618ced3b841e733b221fd1c627901e8e87ae60a8dc0553 languageName: node linkType: hard @@ -21552,36 +21774,36 @@ asn1@evs-broadcast/node-asn1: resolution: "mos-gateway@workspace:mos-gateway" dependencies: "@mos-connection/connector": "npm:^5.0.0-alpha.0" - "@sofie-automation/server-core-integration": "npm:1.53.0-in-development" - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" + "@sofie-automation/server-core-integration": "npm:26.3.0-2" + "@sofie-automation/shared-lib": "npm:26.3.0-2" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" underscore: "npm:^1.13.7" - winston: "npm:^3.17.0" + winston: "npm:^3.19.0" languageName: unknown linkType: soft -"motion-dom@npm:^12.4.5": - version: 12.4.5 - resolution: "motion-dom@npm:12.4.5" +"motion-dom@npm:^12.30.1": + version: 12.30.1 + resolution: "motion-dom@npm:12.30.1" dependencies: - motion-utils: "npm:^12.0.0" - checksum: 10/40c84519266093813a59c340324b3b7134c750075fb50abac8ed2d1cde83d50f084ddf53910fc1e82bd28d932e9a2d1c0fba49dbe8d7aa97b3f8c814b17664ad + motion-utils: "npm:^12.29.2" + checksum: 10/22af7e074b388485b8f383bc9a8a3d03dec51f2e7112a3b50b9fde7076531ec0fccbeb61c669a439eb0a535992b121a48e612901b79f139538697755c291400e languageName: node linkType: hard -"motion-utils@npm:^12.0.0": - version: 12.0.0 - resolution: "motion-utils@npm:12.0.0" - checksum: 10/e73b82cf36746f6d498bc48450d34bf8d51128279d8f542cab8a02edf13368d0b99e4208c6d60c670fac5481c60e09d590db7345e390070ed0499dd4367d1695 +"motion-utils@npm:^12.29.2": + version: 12.29.2 + resolution: "motion-utils@npm:12.29.2" + checksum: 10/ae5f9be58c07939af72334894ed1a18653d724946182a718dc3d11268ef26e63804c3f16dee5a6110596d4406b539c4513822b74f86adebef9488601c34b18b7 languageName: node linkType: hard -"motion@npm:^12.4.7": - version: 12.4.7 - resolution: "motion@npm:12.4.7" +"motion@npm:^12.31.0": + version: 12.31.0 + resolution: "motion@npm:12.31.0" dependencies: - framer-motion: "npm:^12.4.7" + framer-motion: "npm:^12.31.0" tslib: "npm:^2.4.0" peerDependencies: "@emotion/is-prop-valid": "*" @@ -21594,7 +21816,7 @@ asn1@evs-broadcast/node-asn1: optional: true react-dom: optional: true - checksum: 10/3f649dc2688bf6a5627a34193dc3b3e9d277d65ca1be2159a65a80f72c140c2f52fbdde50b876ebd0c2b00622a222048a6e72aa15eefe6038cce0192657c1d44 + checksum: 10/abb2253b2e679f3d153e472f63e29fa0f3d9d4dd6bb2f2b0c9026897dd069a2371ddc2374bf70debdd480351ad5311e830ebf98e5ea7a44588e44099381e950b languageName: node linkType: hard @@ -21665,18 +21887,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"mz@npm:^2.7.0": - version: 2.7.0 - resolution: "mz@npm:2.7.0" - dependencies: - any-promise: "npm:^1.0.0" - object-assign: "npm:^4.0.1" - thenify-all: "npm:^1.0.0" - checksum: 10/8427de0ece99a07e9faed3c0c6778820d7543e3776f9a84d22cf0ec0a8eb65f6e9aee9c9d353ff9a105ff62d33a9463c6ca638974cc652ee8140cd1e35951c87 - languageName: node - linkType: hard - -"nanoid@npm:^3.3.11, nanoid@npm:^3.3.8": +"nanoid@npm:^3.3.11": version: 3.3.11 resolution: "nanoid@npm:3.3.11" bin: @@ -21692,6 +21903,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"napi-postinstall@npm:^0.3.0": + version: 0.3.4 + resolution: "napi-postinstall@npm:0.3.4" + bin: + napi-postinstall: lib/cli.js + checksum: 10/5541381508f9e1051ff3518701c7130ebac779abb3a1ffe9391fcc3cab4cc0569b0ba0952357db3f6b12909c3bb508359a7a60261ffd795feebbdab967175832 + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -21708,13 +21928,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"negotiator@npm:0.6.3, negotiator@npm:^0.6.3": +"negotiator@npm:0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" checksum: 10/2723fb822a17ad55c93a588a4bc44d53b22855bf4be5499916ca0cab1e7165409d0b288ba2577d7b029f10ce18cf2ed8e703e5af31c984e1e2304277ef979837 languageName: node linkType: hard +"negotiator@npm:^0.6.3, negotiator@npm:~0.6.4": + version: 0.6.4 + resolution: "negotiator@npm:0.6.4" + checksum: 10/d98c04a136583afd055746168f1067d58ce4bfe6e4c73ca1d339567f81ea1f7e665b5bd1e81f4771c67b6c2ea89b21cb2adaea2b16058c7dc31317778f931dab + languageName: node + linkType: hard + "negotiator@npm:^1.0.0": version: 1.0.0 resolution: "negotiator@npm:1.0.0" @@ -21750,13 +21977,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"next-tick@npm:1, next-tick@npm:^1.1.0": - version: 1.1.0 - resolution: "next-tick@npm:1.1.0" - checksum: 10/83b5cf36027a53ee6d8b7f9c0782f2ba87f4858d977342bfc3c20c21629290a2111f8374d13a81221179603ffc4364f38374b5655d17b6a8f8a8c77bdea4fe8b - languageName: node - linkType: hard - "nimma@npm:0.2.2": version: 0.2.2 resolution: "nimma@npm:0.2.2" @@ -21776,19 +21996,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"nise@npm:^5.1.2": - version: 5.1.9 - resolution: "nise@npm:5.1.9" - dependencies: - "@sinonjs/commons": "npm:^3.0.0" - "@sinonjs/fake-timers": "npm:^11.2.2" - "@sinonjs/text-encoding": "npm:^0.7.2" - just-extend: "npm:^6.2.0" - path-to-regexp: "npm:^6.2.1" - checksum: 10/971caf7638d42a0e106eadd63f05adac1217f864b0a7e4519546aea82a0dbfac68586e7ff430704d54a01ff5dbf6cad58f5f67c067e21112a7deacd7789c2172 - languageName: node - linkType: hard - "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -21852,7 +22059,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.5, node-fetch@npm:^2.6.7": +"node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.5, node-fetch@npm:^2.6.7": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -21867,9 +22074,9 @@ asn1@evs-broadcast/node-asn1: linkType: hard "node-forge@npm:^1": - version: 1.3.1 - resolution: "node-forge@npm:1.3.1" - checksum: 10/05bab6868633bf9ad4c3b1dd50ec501c22ffd69f556cdf169a00998ca1d03e8107a6032ba013852f202035372021b845603aeccd7dfcb58cdb7430013b3daa8d + version: 1.3.3 + resolution: "node-forge@npm:1.3.3" + checksum: 10/f41c31b9296771a4b8c955d58417471712f54f324603a35f8e6cbac19d5e6eaaf5fd5fd14584dfedecbf46a05438ded6eee60a5f2f0822fc5061aaa073cfc75d languageName: node linkType: hard @@ -21895,23 +22102,23 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"node-gyp@npm:^11.0.0": - version: 11.4.2 - resolution: "node-gyp@npm:11.4.2" +"node-gyp@npm:^12.1.0": + version: 12.2.0 + resolution: "node-gyp@npm:12.2.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^14.0.3" - nopt: "npm:^8.0.0" - proc-log: "npm:^5.0.0" + make-fetch-happen: "npm:^15.0.0" + nopt: "npm:^9.0.0" + proc-log: "npm:^6.0.0" semver: "npm:^7.3.5" - tar: "npm:^7.4.3" + tar: "npm:^7.5.4" tinyglobby: "npm:^0.2.12" - which: "npm:^5.0.0" + which: "npm:^6.0.0" bin: node-gyp: bin/node-gyp.js - checksum: 10/de0fdd1a23d27976974f2480b1c5a2954180050f4d7d682b2fcd36a7c996100981fc37ba0c893d02471ccf1730240f73c3073a6a9397c5eb3bb7578ca82808ed + checksum: 10/4ebab5b77585a637315e969c2274b5520562473fe75de850639a580c2599652fb9f33959ec782ea45a2e149d8f04b548030f472eeeb3dbdf19a7f2ccbc30b908 languageName: node linkType: hard @@ -21957,9 +22164,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"node-stdlib-browser@npm:^1.2.0": - version: 1.2.0 - resolution: "node-stdlib-browser@npm:1.2.0" +"node-stdlib-browser@npm:^1.3.1": + version: 1.3.1 + resolution: "node-stdlib-browser@npm:1.3.1" dependencies: assert: "npm:^2.0.0" browser-resolve: "npm:^2.0.0" @@ -21968,8 +22175,8 @@ asn1@evs-broadcast/node-asn1: console-browserify: "npm:^1.1.0" constants-browserify: "npm:^1.0.0" create-require: "npm:^1.1.1" - crypto-browserify: "npm:^3.11.0" - domain-browser: "npm:^4.22.0" + crypto-browserify: "npm:^3.12.1" + domain-browser: "npm:4.22.0" events: "npm:^3.0.0" https-browserify: "npm:^1.0.0" isomorphic-timers-promises: "npm:^1.0.1" @@ -21985,10 +22192,10 @@ asn1@evs-broadcast/node-asn1: string_decoder: "npm:^1.0.0" timers-browserify: "npm:^2.0.4" tty-browserify: "npm:0.0.1" - url: "npm:^0.11.0" + url: "npm:^0.11.4" util: "npm:^0.12.4" vm-browserify: "npm:^1.0.1" - checksum: 10/3872da5954722fc8e8267bb58af0dbe36a85b2003e55e63e191f7cc38baf2cbff530bea42c809dfeaa0ad70c0977d0b862b4a515ad90902c1db39ff2179f9b71 + checksum: 10/5d5ace50868ef1a8ce9718a5fc64e4b6712f8be75bf6ab71f2eb7b5815f55f20507e427eac2fdb384e372f58891eb34089af3b55d3f9b5b60b547c8581a1c30e languageName: node linkType: hard @@ -22056,14 +22263,25 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"nopt@npm:^8.0.0": - version: 8.1.0 - resolution: "nopt@npm:8.1.0" +"nopt@npm:^8.0.0": + version: 8.1.0 + resolution: "nopt@npm:8.1.0" + dependencies: + abbrev: "npm:^3.0.0" + bin: + nopt: bin/nopt.js + checksum: 10/26ab456c51a96f02a9e5aa8d1b80ef3219f2070f3f3528a040e32fb735b1e651e17bdf0f1476988d3a46d498f35c65ed662d122f340d38ce4a7e71dd7b20c4bc + languageName: node + linkType: hard + +"nopt@npm:^9.0.0": + version: 9.0.0 + resolution: "nopt@npm:9.0.0" dependencies: - abbrev: "npm:^3.0.0" + abbrev: "npm:^4.0.0" bin: nopt: bin/nopt.js - checksum: 10/26ab456c51a96f02a9e5aa8d1b80ef3219f2070f3f3528a040e32fb735b1e651e17bdf0f1476988d3a46d498f35c65ed662d122f340d38ce4a7e71dd7b20c4bc + checksum: 10/56a1ccd2ad711fb5115918e2c96828703cddbe12ba2c3bd00591758f6fa30e6f47dd905c59dbfcf9b773f3a293b45996609fb6789ae29d6bfcc3cf3a6f7d9fda languageName: node linkType: hard @@ -22299,7 +22517,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"npm-packlist@npm:10.0.3, npm-packlist@npm:^10.0.1": +"npm-packlist@npm:10.0.3": version: 10.0.3 resolution: "npm-packlist@npm:10.0.3" dependencies: @@ -22309,6 +22527,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"npm-packlist@npm:^10.0.1": + version: 10.0.2 + resolution: "npm-packlist@npm:10.0.2" + dependencies: + ignore-walk: "npm:^8.0.0" + proc-log: "npm:^5.0.0" + checksum: 10/ff5a819ccfa6139eab2d1cee732cecec9b2eade0a82134ee89648b2a2ac0815c56fbd6117f2048d46ed48dcee83ec1f709ee9acbffdef1da48be99a681253b79 + languageName: node + linkType: hard + "npm-packlist@npm:^5.1.0": version: 5.1.3 resolution: "npm-packlist@npm:5.1.3" @@ -22469,7 +22697,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"nwsapi@npm:^2.2.16, nwsapi@npm:^2.2.2": +"nwsapi@npm:^2.2.16": version: 2.2.18 resolution: "nwsapi@npm:2.2.18" checksum: 10/ce2233284abe2d5c4507089972035018f79c0a3fd00c672f7c5afad7603561c2a8e53c81bc02dcc40f4bc87414b277d932a8a96f53816ff1083abab1f5092c43 @@ -22561,7 +22789,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": +"object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: 10/fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f @@ -22636,14 +22864,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"object.entries@npm:^1.0.4, object.entries@npm:^1.1.0, object.entries@npm:^1.1.8": - version: 1.1.8 - resolution: "object.entries@npm:1.1.8" +"object.entries@npm:^1.0.4, object.entries@npm:^1.1.0, object.entries@npm:^1.1.9": + version: 1.1.9 + resolution: "object.entries@npm:1.1.9" dependencies: - call-bind: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10/2301918fbd1ee697cf6ff7cd94f060c738c0a7d92b22fd24c7c250e9b593642c9707ad2c44d339303c1439c5967d8964251cdfc855f7f6ec55db2dd79e8dc2a7 + es-object-atoms: "npm:^1.1.1" + checksum: 10/24163ab1e1e013796693fc5f5d349e8b3ac0b6a34a7edb6c17d3dd45c6a8854145780c57d302a82512c1582f63720f4b4779d6c1cfba12cbb1420b978802d8a3 languageName: node linkType: hard @@ -22709,10 +22938,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"on-headers@npm:~1.0.2": - version: 1.0.2 - resolution: "on-headers@npm:1.0.2" - checksum: 10/870766c16345855e2012e9422ba1ab110c7e44ad5891a67790f84610bd70a72b67fdd71baf497295f1d1bf38dd4c92248f825d48729c53c0eae5262fb69fa171 +"on-headers@npm:~1.1.0": + version: 1.1.0 + resolution: "on-headers@npm:1.1.0" + checksum: 10/98aa64629f986fb8cc4517dd8bede73c980e31208cba97f4442c330959f60ced3dc6214b83420491f5111fc7c4f4343abe2ea62c85f505cf041d67850f238776 languageName: node linkType: hard @@ -22800,10 +23029,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"openapi-types@npm:9.3.0": - version: 9.3.0 - resolution: "openapi-types@npm:9.3.0" - checksum: 10/7e5e26861ac4ffd5b2dda6ff98e6610682cbcf1220713f649fe62bd261d6ecd58015f9a59271ac3b3a36fb9fa67e9c2829feaf0ddcd7e983cd915ec91f5b75f6 +"openapi-types@npm:^12.1.3": + version: 12.1.3 + resolution: "openapi-types@npm:12.1.3" + checksum: 10/9d1d7ed848622b63d0a4c3f881689161b99427133054e46b8e3241e137f1c78bb0031c5d80b420ee79ac2e91d2e727ffd6fc13c553d1b0488ddc8ad389dcbef8 languageName: node linkType: hard @@ -22893,7 +23122,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"os-tmpdir@npm:^1.0.0, os-tmpdir@npm:~1.0.2": +"os-tmpdir@npm:^1.0.0": version: 1.0.2 resolution: "os-tmpdir@npm:1.0.2" checksum: 10/5666560f7b9f10182548bf7013883265be33620b1c1b4a4d405c25be2636f970c5488ff3e6c48de75b55d02bde037249fe5dbfbb4c0fb7714953d56aed062e6d @@ -23169,7 +23398,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"package-json-from-dist@npm:^1.0.0": +"package-json-from-dist@npm:^1.0.0, package-json-from-dist@npm:^1.0.1": version: 1.0.1 resolution: "package-json-from-dist@npm:1.0.1" checksum: 10/58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602 @@ -23199,35 +23428,35 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "packages@workspace:." dependencies: - "@babel/core": "npm:^7.26.7" - "@babel/plugin-transform-modules-commonjs": "npm:^7.26.3" - "@sofie-automation/code-standard-preset": "npm:^3.0.0" - "@types/amqplib": "npm:^0.10.6" + "@babel/core": "npm:^7.29.0" + "@babel/plugin-transform-modules-commonjs": "npm:^7.28.6" + "@sofie-automation/code-standard-preset": "npm:^3.2.1" + "@types/amqplib": "npm:0.10.6" "@types/debug": "npm:^4.1.12" "@types/ejson": "npm:^2.2.2" - "@types/got": "npm:^9.6.12" - "@types/jest": "npm:^29.5.14" - "@types/node": "npm:^22.10.10" + "@types/jest": "npm:^30.0.0" + "@types/node": "npm:^22.19.8" "@types/object-path": "npm:^0.11.4" "@types/underscore": "npm:^1.13.0" - babel-jest: "npm:^29.7.0" + babel-jest: "npm:^30.2.0" copyfiles: "npm:^2.4.1" - eslint: "npm:^9.18.0" - eslint-plugin-react: "npm:^7.37.4" - jest: "npm:^29.7.0" - jest-environment-jsdom: "npm:^29.7.0" - jest-mock-extended: "npm:^3.0.7" - json-schema-to-typescript: "npm:^10.1.5" - lerna: "npm:^9.0.0" + eslint: "npm:^9.39.2" + eslint-plugin-react: "npm:^7.37.5" + eslint-plugin-yml: "npm:^3.1.2" + jest: "npm:^30.2.0" + jest-environment-jsdom: "npm:^30.2.0" + jest-mock-extended: "npm:^4.0.0" + json-schema-to-typescript: "npm:^15.0.4" + lerna: "npm:^9.0.5" nodemon: "npm:^2.0.22" open-cli: "npm:^8.0.0" pinst: "npm:^3.0.0" - prettier: "npm:^3.4.2" - rimraf: "npm:^6.0.1" - semver: "npm:^7.6.3" - ts-jest: "npm:^29.2.5" + prettier: "npm:^3.8.1" + rimraf: "npm:^6.1.2" + semver: "npm:^7.7.3" + ts-jest: "npm:^29.4.6" ts-node: "npm:^10.9.2" - typedoc: "npm:^0.27.6" + typedoc: "npm:^0.27.9" typescript: "npm:~5.7.3" dependenciesMeta: esbuild: @@ -23348,17 +23577,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.7": - version: 5.1.7 - resolution: "parse-asn1@npm:5.1.7" +"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.9": + version: 5.1.9 + resolution: "parse-asn1@npm:5.1.9" dependencies: asn1.js: "npm:^4.10.1" browserify-aes: "npm:^1.2.0" evp_bytestokey: "npm:^1.0.3" - hash-base: "npm:~3.0" - pbkdf2: "npm:^3.1.2" + pbkdf2: "npm:^3.1.5" safe-buffer: "npm:^5.2.1" - checksum: 10/f82c079f4d9a4d33159c7682f9c516680f4d659fde8060697a6b3c1be4795976e826d53a1e5751a81ddc800e9c6d6fa4629b59f6d1f3241ac8447a00c89a67d3 + checksum: 10/bc3d616a8076fa8a9a34cab8af6905859a1bafd0c49c98132acc7d29b779c2b81d4a8fc610f5bedc9770cc4bfc323f7c939ad7413e9df6ba60cb931010c42f52 languageName: node linkType: hard @@ -23468,7 +23696,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"parse5@npm:^7.0.0, parse5@npm:^7.1.1, parse5@npm:^7.2.1": +"parse5@npm:^7.0.0, parse5@npm:^7.2.1": version: 7.2.1 resolution: "parse5@npm:7.2.1" dependencies: @@ -23626,7 +23854,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"path-scurry@npm:^1.11.1, path-scurry@npm:^1.6.1": +"path-scurry@npm:^1.11.1": version: 1.11.1 resolution: "path-scurry@npm:1.11.1" dependencies: @@ -23660,33 +23888,19 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"path-to-regexp@npm:8.2.0": - version: 8.2.0 - resolution: "path-to-regexp@npm:8.2.0" - checksum: 10/23378276a172b8ba5f5fb824475d1818ca5ccee7bbdb4674701616470f23a14e536c1db11da9c9e6d82b82c556a817bbf4eee6e41b9ed20090ef9427cbb38e13 +"path-to-regexp@npm:8.3.0, path-to-regexp@npm:^8.3.0": + version: 8.3.0 + resolution: "path-to-regexp@npm:8.3.0" + checksum: 10/568f148fc64f5fd1ecebf44d531383b28df924214eabf5f2570dce9587a228e36c37882805ff02d71c6209b080ea3ee6a4d2b712b5df09741b67f1f3cf91e55a languageName: node linkType: hard "path-to-regexp@npm:^1.7.0": - version: 1.8.0 - resolution: "path-to-regexp@npm:1.8.0" + version: 1.9.0 + resolution: "path-to-regexp@npm:1.9.0" dependencies: isarray: "npm:0.0.1" - checksum: 10/45a01690f72919163cf89714e31a285937b14ad54c53734c826363fcf7beba9d9d0f2de802b4986b1264374562d6a3398a2e5289753a764e3a256494f1e52add - languageName: node - linkType: hard - -"path-to-regexp@npm:^6.2.1": - version: 6.2.2 - resolution: "path-to-regexp@npm:6.2.2" - checksum: 10/f7d11c1a9e02576ce0294f4efdc523c11b73894947afdf7b23a0d0f7c6465d7a7772166e770ddf1495a8017cc0ee99e3e8a15ed7302b6b948b89a6dd4eea895e - languageName: node - linkType: hard - -"path-to-regexp@npm:^8.2.0": - version: 8.3.0 - resolution: "path-to-regexp@npm:8.3.0" - checksum: 10/568f148fc64f5fd1ecebf44d531383b28df924214eabf5f2570dce9587a228e36c37882805ff02d71c6209b080ea3ee6a4d2b712b5df09741b67f1f3cf91e55a + checksum: 10/67f0f4823f7aab356523d93a83f9f8222bdd119fa0b27a8f8b587e8e6c9825294bb4ccd16ae619def111ff3fe5d15ff8f658cdd3b0d58b9c882de6fd15bc1b76 languageName: node linkType: hard @@ -23713,16 +23927,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pbkdf2@npm:^3.0.3, pbkdf2@npm:^3.1.2": - version: 3.1.2 - resolution: "pbkdf2@npm:3.1.2" +"pbkdf2@npm:^3.1.2, pbkdf2@npm:^3.1.5": + version: 3.1.5 + resolution: "pbkdf2@npm:3.1.5" dependencies: - create-hash: "npm:^1.1.2" - create-hmac: "npm:^1.1.4" - ripemd160: "npm:^2.0.1" - safe-buffer: "npm:^5.0.1" - sha.js: "npm:^2.4.8" - checksum: 10/40bdf30df1c9bb1ae41ec50c11e480cf0d36484b7c7933bf55e4451d1d0e3f09589df70935c56e7fccc5702779a0d7b842d012be8c08a187b44eb24d55bb9460 + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + ripemd160: "npm:^2.0.3" + safe-buffer: "npm:^5.2.1" + sha.js: "npm:^2.4.12" + to-buffer: "npm:^1.2.1" + checksum: 10/ce1c9a2ebbc843c86090ec6cac6d07429dece7c1fdb87437ce6cf869d0429cc39cab61bc34215585f4a00d8009862df45e197fbd54f3508ccba8ff312a88261b languageName: node linkType: hard @@ -23751,7 +23966,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": +"picocolors@npm:1.1.1, picocolors@npm:^1.0.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" checksum: 10/e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 @@ -23847,14 +24062,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pirates@npm:^4.0.4": - version: 4.0.6 - resolution: "pirates@npm:4.0.6" - checksum: 10/d02dda76f4fec1cbdf395c36c11cf26f76a644f9f9a1bfa84d3167d0d3154d5289aacc72677aa20d599bb4a6937a471de1b65c995e2aea2d8687cbcd7e43ea5f +"pirates@npm:^4.0.7": + version: 4.0.7 + resolution: "pirates@npm:4.0.7" + checksum: 10/2427f371366081ae42feb58214f04805d6b41d6b84d74480ebcc9e0ddbd7105a139f7c653daeaf83ad8a1a77214cf07f64178e76de048128fec501eab3305a96 languageName: node linkType: hard -"pkg-dir@npm:4.2.0, pkg-dir@npm:^4.2.0": +"pkg-dir@npm:^4.2.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" dependencies: @@ -23919,14 +24134,14 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": "npm:1.53.0-in-development" - "@sofie-automation/shared-lib": "npm:1.53.0-in-development" - debug: "npm:^4.4.0" - influx: "npm:^5.9.7" - timeline-state-resolver: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" + "@sofie-automation/server-core-integration": "npm:26.3.0-2" + "@sofie-automation/shared-lib": "npm:26.3.0-2" + debug: "npm:^4.4.3" + influx: "npm:^5.12.0" + timeline-state-resolver: "npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0" tslib: "npm:^2.8.1" underscore: "npm:^1.13.7" - winston: "npm:^3.17.0" + winston: "npm:^3.19.0" languageName: unknown linkType: soft @@ -23954,13 +24169,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"popper.js@npm:^1.14.1, popper.js@npm:^1.14.4": - version: 1.16.1 - resolution: "popper.js@npm:1.16.1" - checksum: 10/71338c86faf9b66ce60c3cdd7fb2ed742944e5d2765a188f269239fee2980aa6223b77b41302d1b6eb7d724e611092f9a2576d0048ac2071b605291abc72c0cf - languageName: node - linkType: hard - "possible-typed-array-names@npm:^1.0.0": version: 1.0.0 resolution: "possible-typed-array-names@npm:1.0.0" @@ -24778,7 +24986,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"postcss@npm:^8.4.21, postcss@npm:^8.4.24, postcss@npm:^8.4.33, postcss@npm:^8.4.49, postcss@npm:^8.5.4": +"postcss@npm:^8.4.21, postcss@npm:^8.4.24, postcss@npm:^8.4.33, postcss@npm:^8.5.4, postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" dependencies: @@ -24796,30 +25004,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"prettier-linter-helpers@npm:^1.0.0": - version: 1.0.0 - resolution: "prettier-linter-helpers@npm:1.0.0" +"prettier-linter-helpers@npm:^1.0.1": + version: 1.0.1 + resolution: "prettier-linter-helpers@npm:1.0.1" dependencies: fast-diff: "npm:^1.1.2" - checksum: 10/00ce8011cf6430158d27f9c92cfea0a7699405633f7f1d4a45f07e21bf78e99895911cbcdc3853db3a824201a7c745bd49bfea8abd5fb9883e765a90f74f8392 + checksum: 10/2dc35f5036a35f4c4f5e645887edda1436acb63687a7f12b2383e0a6f3c1f76b8a0a4709fe4d82e19157210feb5984b159bb714d43290022911ab53d606474ec languageName: node linkType: hard -"prettier@npm:^2.2.0": - version: 2.8.8 - resolution: "prettier@npm:2.8.8" - bin: - prettier: bin-prettier.js - checksum: 10/00cdb6ab0281f98306cd1847425c24cbaaa48a5ff03633945ab4c701901b8e96ad558eb0777364ffc312f437af9b5a07d0f45346266e8245beaf6247b9c62b24 - languageName: node - linkType: hard - -"prettier@npm:^3.4.2": - version: 3.4.2 - resolution: "prettier@npm:3.4.2" +"prettier@npm:^3.2.5, prettier@npm:^3.8.1": + version: 3.8.1 + resolution: "prettier@npm:3.8.1" bin: prettier: bin/prettier.cjs - checksum: 10/a3e806fb0b635818964d472d35d27e21a4e17150c679047f5501e1f23bd4aa806adf660f0c0d35214a210d5d440da6896c2e86156da55f221a57938278dc326e + checksum: 10/3da1cf8c1ef9bea828aa618553696c312e951f810bee368f6887109b203f18ee869fe88f66e65f9cf60b7cb1f2eae859892c860a300c062ff8ec69c381fc8dbd languageName: node linkType: hard @@ -24833,7 +25032,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pretty-format@npm:30.2.0": +"pretty-format@npm:30.2.0, pretty-format@npm:^30.0.0": version: 30.2.0 resolution: "pretty-format@npm:30.2.0" dependencies: @@ -24855,17 +25054,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": - version: 29.7.0 - resolution: "pretty-format@npm:29.7.0" - dependencies: - "@jest/schemas": "npm:^29.6.3" - ansi-styles: "npm:^5.0.0" - react-is: "npm:^18.0.0" - checksum: 10/dea96bc83c83cd91b2bfc55757b6b2747edcaac45b568e46de29deee80742f17bc76fe8898135a70d904f4928eafd8bb693cd1da4896e8bdd3c5e82cadf1d2bb - languageName: node - linkType: hard - "pretty-time@npm:^1.1.0": version: 1.1.0 resolution: "pretty-time@npm:1.1.0" @@ -24886,9 +25074,9 @@ asn1@evs-broadcast/node-asn1: linkType: hard "prismjs@npm:^1.29.0": - version: 1.29.0 - resolution: "prismjs@npm:1.29.0" - checksum: 10/2080db382c2dde0cfc7693769e89b501ef1bfc8ff4f8d25c07fd4c37ca31bc443f6133d5b7c145a73309dc396e829ddb7cc18560026d862a887ae08864ef6b07 + version: 1.30.0 + resolution: "prismjs@npm:1.30.0" + checksum: 10/6b48a2439a82e5c6882f48ebc1564c3890e16463ba17ac10c3ad4f62d98dea5b5c915b172b63b83023a70ad4f5d7be3e8a60304420db34a161fae69dd4e3e2da languageName: node linkType: hard @@ -24948,7 +25136,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"progress@npm:2.0.3": +"progress@npm:^2.0.3": version: 2.0.3 resolution: "progress@npm:2.0.3" checksum: 10/e6f0bcb71f716eee9dfac0fe8a2606e3704d6a64dd93baaf49fbadbc8499989a610fe14cf1bc6f61b6d6653c49408d94f4a94e124538084efd8e4cf525e0293d @@ -25017,7 +25205,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"prompts@npm:^2.0.1, prompts@npm:^2.4.2": +"prompts@npm:^2.4.2": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -25110,7 +25298,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"proxy-agent@npm:6.5.0": +"proxy-agent@npm:6.5.0, proxy-agent@npm:^6.5.0": version: 6.5.0 resolution: "proxy-agent@npm:6.5.0" dependencies: @@ -25126,20 +25314,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"proxy-from-env@npm:1.1.0, proxy-from-env@npm:^1.1.0": +"proxy-from-env@npm:^1.1.0": version: 1.1.0 resolution: "proxy-from-env@npm:1.1.0" checksum: 10/f0bb4a87cfd18f77bc2fba23ae49c3b378fb35143af16cc478171c623eebe181678f09439707ad80081d340d1593cd54a33a0113f3ccb3f4bc9451488780ee23 languageName: node linkType: hard -"psl@npm:^1.1.33": - version: 1.9.0 - resolution: "psl@npm:1.9.0" - checksum: 10/d07879d4bfd0ac74796306a8e5a36a93cfb9c4f4e8ee8e63fbb909066c192fe1008cd8f12abd8ba2f62ca28247949a20c8fb32e1d18831d9e71285a1569720f9 - languageName: node - linkType: hard - "pstree.remy@npm:^1.1.8": version: 1.1.8 resolution: "pstree.remy@npm:1.1.8" @@ -25147,7 +25328,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"public-encrypt@npm:^4.0.0": +"public-encrypt@npm:^4.0.3": version: 4.0.3 resolution: "public-encrypt@npm:4.0.3" dependencies: @@ -25192,7 +25373,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.1": +"punycode@npm:^2.1.0, punycode@npm:^2.3.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10/febdc4362bead22f9e2608ff0171713230b57aff9dddc1c273aa2a651fbd366f94b7d6a71d78342a7c0819906750351ca7f2edd26ea41b626d87d6a13d1bd059 @@ -25208,34 +25389,45 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"puppeteer@npm:^14.1.0": - version: 14.4.1 - resolution: "puppeteer@npm:14.4.1" +"puppeteer-core@npm:24.36.1": + version: 24.36.1 + resolution: "puppeteer-core@npm:24.36.1" dependencies: - cross-fetch: "npm:3.1.5" - debug: "npm:4.3.4" - devtools-protocol: "npm:0.0.1001819" - extract-zip: "npm:2.0.1" - https-proxy-agent: "npm:5.0.1" - pkg-dir: "npm:4.2.0" - progress: "npm:2.0.3" - proxy-from-env: "npm:1.1.0" - rimraf: "npm:3.0.2" - tar-fs: "npm:2.1.1" - unbzip2-stream: "npm:1.4.3" - ws: "npm:8.7.0" - checksum: 10/c93330635b453f38a5dc32eeb9725d672636e21412a0a770967033b26056574e0c65539aef374519eb4dd00b2ceb21fb4ceac2e042d8f872ec30033953a7921e + "@puppeteer/browsers": "npm:2.11.2" + chromium-bidi: "npm:13.0.1" + debug: "npm:^4.4.3" + devtools-protocol: "npm:0.0.1551306" + typed-query-selector: "npm:^2.12.0" + webdriver-bidi-protocol: "npm:0.4.0" + ws: "npm:^8.19.0" + checksum: 10/806abbad570ecc77dfec95843fbc6626bc357995ac047012bcc0906ebc99292221c43ef70391f63edcbdc22136da6d42bc3655f1fb24a280cec0eab75467eec1 languageName: node linkType: hard -"pure-rand@npm:^6.0.0": - version: 6.0.3 - resolution: "pure-rand@npm:6.0.3" - checksum: 10/68e6ebbc918d0022870cc436c26fd07b8ae6a71acc9aa83145d6e2ec0022e764926cbffc70c606fd25213c3b7234357d10458939182fb6568c2a364d1098cf34 +"puppeteer@npm:^24.4.0": + version: 24.36.1 + resolution: "puppeteer@npm:24.36.1" + dependencies: + "@puppeteer/browsers": "npm:2.11.2" + chromium-bidi: "npm:13.0.1" + cosmiconfig: "npm:^9.0.0" + devtools-protocol: "npm:0.0.1551306" + puppeteer-core: "npm:24.36.1" + typed-query-selector: "npm:^2.12.0" + bin: + puppeteer: lib/cjs/puppeteer/node/cli.js + checksum: 10/3efb73a448099e12bf57217d031f072cb41eeaa4517d0276b2f2e504e40675a61295d09f5209d8a59b2d3e6dc9e4baf84cf350cf60bd7284c8b0f4432aecc0fd + languageName: node + linkType: hard + +"pure-rand@npm:^7.0.0": + version: 7.0.1 + resolution: "pure-rand@npm:7.0.1" + checksum: 10/c61a576fda5032ec9763ecb000da4a8f19263b9e2f9ae9aa2759c8fbd9dc6b192b2ce78391ebd41abb394a5fedb7bcc4b03c9e6141ac8ab20882dd5717698b80 languageName: node linkType: hard -"qs@npm:6.13.0, qs@npm:^6.11.2": +"qs@npm:6.13.0": version: 6.13.0 resolution: "qs@npm:6.13.0" dependencies: @@ -25244,6 +25436,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"qs@npm:^6.12.3": + version: 6.14.1 + resolution: "qs@npm:6.14.1" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10/34b5ab00a910df432d55180ef39c1d1375e550f098b5ec153b41787f1a6a6d7e5f9495593c3b112b77dbc6709d0ae18e55b82847a4c2bbbb0de1e8ccbb1794c5 + languageName: node + linkType: hard + "quansync@npm:^0.2.11": version: 0.2.11 resolution: "quansync@npm:0.2.11" @@ -25329,7 +25530,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"randomfill@npm:^1.0.3": +"randomfill@npm:^1.0.4": version: 1.0.4 resolution: "randomfill@npm:1.0.4" dependencies: @@ -25436,9 +25637,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-bootstrap@npm:^2.10.9": - version: 2.10.9 - resolution: "react-bootstrap@npm:2.10.9" +"react-bootstrap@npm:^2.10.10": + version: 2.10.10 + resolution: "react-bootstrap@npm:2.10.10" dependencies: "@babel/runtime": "npm:^7.24.7" "@restart/hooks": "npm:^0.4.9" @@ -25460,32 +25661,25 @@ asn1@evs-broadcast/node-asn1: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/aaa923af1cf724471f8c9dd7aa365b0e5556cb3aa63b023912353b6874d7f274b4362a9b97df91fb0c04e998c7a49fa8c5e071f3ba468e888afa26b2d0d27ca2 - languageName: node - linkType: hard - -"react-circular-progressbar@npm:*, react-circular-progressbar@npm:^2.1.0": - version: 2.1.0 - resolution: "react-circular-progressbar@npm:2.1.0" - peerDependencies: - react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: 10/a2ff10b2a070dd2443d6e8a25c36e27e1c3239e43f16970907c83b5fc99f1da9887b4a7a9b29ed420ff1604f1e0415e4705186d1eccce55f68adfbb0a6d75902 + checksum: 10/0ed950e0705779312bc625a422d86981c3dbf5d53ff5a038983dee70463c66430b6f3ccac08dd8129665952745e151ff8c35c7dd48a4de89d8322e8cbc23337b languageName: node linkType: hard -"react-datepicker@npm:^3.8.0": - version: 3.8.0 - resolution: "react-datepicker@npm:3.8.0" +"react-datepicker@npm:^9.1.0": + version: 9.1.0 + resolution: "react-datepicker@npm:9.1.0" dependencies: - classnames: "npm:^2.2.6" - date-fns: "npm:^2.0.1" - prop-types: "npm:^15.7.2" - react-onclickoutside: "npm:^6.10.0" - react-popper: "npm:^1.3.8" + "@floating-ui/react": "npm:^0.27.15" + clsx: "npm:^2.1.1" + date-fns: "npm:^4.1.0" peerDependencies: - react: ^16.9.0 || ^17 - react-dom: ^16.9.0 || ^17 - checksum: 10/7b32245a0683eb56e80e7b489f24bc0f6e1daa34ebb36d50ccf9cd688d63e427675f3e41099e8caf4ea91df8854bc84229e37addc1b397a5b9cbd6454a367600 + date-fns-tz: ^3.0.0 + react: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc + peerDependenciesMeta: + date-fns-tz: + optional: true + checksum: 10/d1b2e7c2f90abef05e1669b096005a9130482600781fe15d8fdaf1441bd070376ac7ab5c0da07f9af2959b33e8fe8f5807365285823f6dc3f7ade4641d88ebe5 languageName: node linkType: hard @@ -25608,16 +25802,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-intersection-observer@npm:^9.15.1": - version: 9.15.1 - resolution: "react-intersection-observer@npm:9.15.1" +"react-intersection-observer@npm:^9.16.0": + version: 9.16.0 + resolution: "react-intersection-observer@npm:9.16.0" peerDependencies: react: ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: react-dom: optional: true - checksum: 10/874f5cabaa028ae2f6eaff939cb408004285a27f238a7dd0a0f803c42371b98f0d6fec1ab34c02a283ca11d7eecc7d0ba8d07c9d17661734dcd2c79c68d66fc2 + checksum: 10/ded14524d9311cfb9dd9e65eb04748d07a1868f8c40dd628bec8a8474d43ee2373604fdc1e6a7d468a8e2e680638e41b91048ab9669555d50217c5c0c51247e0 languageName: node linkType: hard @@ -25635,7 +25829,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-is@npm:^18.0.0, react-is@npm:^18.2.0, react-is@npm:^18.3.1": +"react-is@npm:^18.2.0, react-is@npm:^18.3.1": version: 18.3.1 resolution: "react-is@npm:18.3.1" checksum: 10/d5f60c87d285af24b1e1e7eaeb123ec256c3c8bdea7061ab3932e3e14685708221bf234ec50b21e10dd07f008f1b966a2730a0ce4ff67905b3872ff2042aec22 @@ -25681,41 +25875,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-moment@npm:^0.9.7": - version: 0.9.7 - resolution: "react-moment@npm:0.9.7" - peerDependencies: - moment: ^2.24.0 - prop-types: ^15.7.2 - react: ^15.6.0 || ^16.0.0 - checksum: 10/c1ae0c6a5df41e436968e0fae205abf3b28f99e1d2b990c52a15fbbc50d1ec434e97b5a7e438c6eecef02d8716da5f862990a92c3d4ccd01e157ffe2ebf08a6b - languageName: node - linkType: hard - -"react-onclickoutside@npm:^6.10.0": - version: 6.13.1 - resolution: "react-onclickoutside@npm:6.13.1" - peerDependencies: - react: ^15.5.x || ^16.x || ^17.x || ^18.x - react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x - checksum: 10/bc1f5813ad25d81602661848b19f0c15357b87e224ddf5abc2ac4502ffddbe7ee44688827b0522327868ae63ab229030d2a2f185007ecfe861f1128d3e5319e6 - languageName: node - linkType: hard - -"react-popper@npm:^1.3.8": - version: 1.3.11 - resolution: "react-popper@npm:1.3.11" - dependencies: - "@babel/runtime": "npm:^7.1.2" - "@hypnosphi/create-react-context": "npm:^0.3.1" - deep-equal: "npm:^1.1.1" - popper.js: "npm:^1.14.4" - prop-types: "npm:^15.6.1" - typed-styles: "npm:^0.0.7" - warning: "npm:^4.0.2" +"react-moment@npm:^1.2.1": + version: 1.2.1 + resolution: "react-moment@npm:1.2.1" peerDependencies: - react: 0.14.x || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/1f7115da1dd0fdca1fc266d2cefa79ed00eca560f72399a9149e9a8a4bc3e9d9fe4e2955bbbe2e3326607ceb3bd4a971e501fb0d6bbf57bf492c0072ae39c2c6 + moment: ^2.29.0 + prop-types: ^15.7.0 + react: ^16.0 || ^17.0.0 || ^18.0.0 + checksum: 10/c225ee586a8d0ea4c271787ca2af137a51977efad21bbbbd1c9649a953b5fb20a307f76c8f0be2af2f263536f34b328c00ebb75b1647db0dc1110b9d995dca65 languageName: node linkType: hard @@ -25733,10 +25900,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-refresh@npm:^0.14.2": - version: 0.14.2 - resolution: "react-refresh@npm:0.14.2" - checksum: 10/512abf97271ab8623486061be04b608c39d932e3709f9af1720b41573415fa4993d0009fa5138b6705b60a98f4102f744d4e26c952b14f41a0e455521c6be4cc +"react-refresh@npm:^0.18.0": + version: 0.18.0 + resolution: "react-refresh@npm:0.18.0" + checksum: 10/504c331c19776bf8320c23bad7f80b3a28de03301ed7523b0dd21d3f02bf2b53bbdd5aa52469b187bc90f358614b2ba303c088a0765c95f4f0a68c43a7d67b1d languageName: node linkType: hard @@ -26131,12 +26298,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"regenerate-unicode-properties@npm:^10.2.0": - version: 10.2.0 - resolution: "regenerate-unicode-properties@npm:10.2.0" +"regenerate-unicode-properties@npm:^10.2.2": + version: 10.2.2 + resolution: "regenerate-unicode-properties@npm:10.2.2" dependencies: regenerate: "npm:^1.4.2" - checksum: 10/9150eae6fe04a8c4f2ff06077396a86a98e224c8afad8344b1b656448e89e84edcd527e4b03aa5476774129eb6ad328ed684f9c1459794a935ec0cc17ce14329 + checksum: 10/5041ee31185c4700de9dd76783fab9def51c412751190d523d621db5b8e35a6c2d91f1642c12247e7d94f84b8ae388d044baac1e88fc2ba0ac215ca8dc7bed38 languageName: node linkType: hard @@ -26147,23 +26314,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"regenerator-runtime@npm:^0.14.0": - version: 0.14.0 - resolution: "regenerator-runtime@npm:0.14.0" - checksum: 10/6c19495baefcf5fbb18a281b56a97f0197b5f219f42e571e80877f095320afac0bdb31dab8f8186858e6126950068c3f17a1226437881e3e70446ea66751897c - languageName: node - linkType: hard - -"regenerator-transform@npm:^0.15.2": - version: 0.15.2 - resolution: "regenerator-transform@npm:0.15.2" - dependencies: - "@babel/runtime": "npm:^7.8.4" - checksum: 10/c4fdcb46d11bbe32605b4b9ed76b21b8d3f241a45153e9dc6f5542fed4c7744fed459f42701f650d5d5956786bf7de57547329d1c05a9df2ed9e367b9d903302 - languageName: node - linkType: hard - -"regexp.prototype.flags@npm:^1.5.1, regexp.prototype.flags@npm:^1.5.3": +"regexp.prototype.flags@npm:^1.5.3": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" dependencies: @@ -26177,17 +26328,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"regexpu-core@npm:^6.2.0": - version: 6.2.0 - resolution: "regexpu-core@npm:6.2.0" +"regexpu-core@npm:^6.3.1": + version: 6.4.0 + resolution: "regexpu-core@npm:6.4.0" dependencies: regenerate: "npm:^1.4.2" - regenerate-unicode-properties: "npm:^10.2.0" + regenerate-unicode-properties: "npm:^10.2.2" regjsgen: "npm:^0.8.0" - regjsparser: "npm:^0.12.0" + regjsparser: "npm:^0.13.0" unicode-match-property-ecmascript: "npm:^2.0.0" - unicode-match-property-value-ecmascript: "npm:^2.1.0" - checksum: 10/4d054ffcd98ca4f6ca7bf0df6598ed5e4a124264602553308add41d4fa714a0c5bcfb5bc868ac91f7060a9c09889cc21d3180a3a14c5f9c5838442806129ced3 + unicode-match-property-value-ecmascript: "npm:^2.2.1" + checksum: 10/bf5f85a502a17f127a1f922270e2ecc1f0dd071ff76a3ec9afcd6b1c2bf7eae1486d1e3b1a6d621aee8960c8b15139e6b5058a84a68e518e1a92b52e9322faf9 languageName: node linkType: hard @@ -26216,14 +26367,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"regjsparser@npm:^0.12.0": - version: 0.12.0 - resolution: "regjsparser@npm:0.12.0" +"regjsparser@npm:^0.13.0": + version: 0.13.0 + resolution: "regjsparser@npm:0.13.0" dependencies: - jsesc: "npm:~3.0.2" + jsesc: "npm:~3.1.0" bin: regjsparser: bin/parser - checksum: 10/c2d6506b3308679de5223a8916984198e0493649a67b477c66bdb875357e3785abbf3bedf7c5c2cf8967d3b3a7bdf08b7cbd39e65a70f9e1ffad584aecf5f06a + checksum: 10/eeaabd3454f59394cbb3bfeb15fd789e638040f37d0bee9071a9b0b85524ddc52b5f7aaaaa4847304c36fa37429e53d109c4dbf6b878cb5ffa4f4198c1042fb7 languageName: node linkType: hard @@ -26383,14 +26534,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"require-in-the-middle@npm:^7.1.1": - version: 7.2.0 - resolution: "require-in-the-middle@npm:7.2.0" +"require-in-the-middle@npm:^8.0.0": + version: 8.0.1 + resolution: "require-in-the-middle@npm:8.0.1" dependencies: - debug: "npm:^4.1.1" + debug: "npm:^4.3.5" module-details-from-path: "npm:^1.0.3" - resolve: "npm:^1.22.1" - checksum: 10/f77f865d5f689d8cada40c9bb947a86d2992b34ee9d3b98aaa7f643acd101ede624e5fe3e9200103900f6b772af4277ef97d08a9332160c895861dc3f801be67 + checksum: 10/4ce98c681489d383a0ffccb79b06df7a1dffbb31c13f3b713ae2c5a1967597a259e67612507ef69748d83d531bba7c9bb0477211771fe78c685e1d52b1a44b64 languageName: node linkType: hard @@ -26479,23 +26629,23 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"resolve.exports@npm:2.0.3, resolve.exports@npm:^2.0.0": +"resolve.exports@npm:2.0.3": version: 2.0.3 resolution: "resolve.exports@npm:2.0.3" checksum: 10/536efee0f30a10fac8604e6cdc7844dbc3f4313568d09f06db4f7ed8a5b8aeb8585966fe975083d1f2dfbc87cf5f8bc7ab65a5c23385c14acbb535ca79f8398a languageName: node linkType: hard -"resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.17.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:^1.3.2": - version: 1.22.8 - resolution: "resolve@npm:1.22.8" +"resolve@npm:^1.10.0, resolve@npm:^1.17.0, resolve@npm:^1.22.11, resolve@npm:^1.3.2": + version: 1.22.11 + resolution: "resolve@npm:1.22.11" dependencies: - is-core-module: "npm:^2.13.0" + is-core-module: "npm:^2.16.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/c473506ee01eb45cbcfefb68652ae5759e092e6b0fb64547feadf9736a6394f258fbc6f88e00c5ca36d5477fbb65388b272432a3600fa223062e54333c156753 + checksum: 10/e1b2e738884a08de03f97ee71494335eba8c2b0feb1de9ae065e82c48997f349f77a2b10e8817e147cf610bfabc4b1cb7891ee8eaf5bf80d4ad514a34c4fab0a languageName: node linkType: hard @@ -26521,16 +26671,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.14.2#optional!builtin, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.1#optional!builtin, resolve@patch:resolve@npm%3A^1.3.2#optional!builtin": - version: 1.22.8 - resolution: "resolve@patch:resolve@npm%3A1.22.8#optional!builtin::version=1.22.8&hash=c3c19d" +"resolve@patch:resolve@npm%3A^1.10.0#optional!builtin, resolve@patch:resolve@npm%3A^1.17.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.11#optional!builtin, resolve@patch:resolve@npm%3A^1.3.2#optional!builtin": + version: 1.22.11 + resolution: "resolve@patch:resolve@npm%3A1.22.11#optional!builtin::version=1.22.11&hash=c3c19d" dependencies: - is-core-module: "npm:^2.13.0" + is-core-module: "npm:^2.16.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/f345cd37f56a2c0275e3fe062517c650bb673815d885e7507566df589375d165bbbf4bdb6aa95600a9bc55f4744b81f452b5a63f95b9f10a72787dba3c90890a + checksum: 10/fd342cad25e52cd6f4f3d1716e189717f2522bfd6641109fe7aa372f32b5714a296ed7c238ddbe7ebb0c1ddfe0b7f71c9984171024c97cf1b2073e3e40ff71a8 languageName: node linkType: hard @@ -26605,7 +26755,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"rimraf@npm:3.0.2, rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": +"rimraf@npm:^3.0.0, rimraf@npm:^3.0.2": version: 3.0.2 resolution: "rimraf@npm:3.0.2" dependencies: @@ -26616,26 +26766,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"rimraf@npm:^4.4.1": - version: 4.4.1 - resolution: "rimraf@npm:4.4.1" - dependencies: - glob: "npm:^9.2.0" - bin: - rimraf: dist/cjs/src/bin.js - checksum: 10/218ef9122145ccce9d0a71124d36a3894537de46600b37fae7dba26ccff973251eaa98aa63c2c5855a05fa04bca7cbbd7a92d4b29f2875d2203e72530ecf6ede - languageName: node - linkType: hard - -"rimraf@npm:^6.0.1": - version: 6.0.1 - resolution: "rimraf@npm:6.0.1" +"rimraf@npm:^6.1.2": + version: 6.1.2 + resolution: "rimraf@npm:6.1.2" dependencies: - glob: "npm:^11.0.0" - package-json-from-dist: "npm:^1.0.0" + glob: "npm:^13.0.0" + package-json-from-dist: "npm:^1.0.1" bin: rimraf: dist/esm/bin.mjs - checksum: 10/0eb7edf08aa39017496c99ba675552dda11a20811ba78f8232da2ba945308c91e9cd673f95998b1a8202bc7436d33390831d23ea38ae52751038d56373ad99e2 + checksum: 10/add8e566fe903f59d7b55c6c2382320c48302778640d1951baf247b3b451af496c2dee7195c204a8c646fd6327feadd1f5b61ce68c1362d4898075a726d83cc6 languageName: node linkType: hard @@ -26648,13 +26787,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1": - version: 2.0.2 - resolution: "ripemd160@npm:2.0.2" +"ripemd160@npm:^2.0.0, ripemd160@npm:^2.0.1, ripemd160@npm:^2.0.3": + version: 2.0.3 + resolution: "ripemd160@npm:2.0.3" dependencies: - hash-base: "npm:^3.0.0" - inherits: "npm:^2.0.1" - checksum: 10/006accc40578ee2beae382757c4ce2908a826b27e2b079efdcd2959ee544ddf210b7b5d7d5e80467807604244e7388427330f5c6d4cd61e6edaddc5773ccc393 + hash-base: "npm:^3.1.2" + inherits: "npm:^2.0.4" + checksum: 10/d15d42ea0460426675e5320f86d3468ab408af95b1761cf35f8d32c0c97b4d3bb72b7226e990e643b96e1637a8ad26b343a6c7666e1a297bcab4f305a1d9d3e3 languageName: node linkType: hard @@ -26666,8 +26805,8 @@ asn1@evs-broadcast/node-asn1: linkType: hard "rollup@npm:^2.60.1": - version: 2.79.1 - resolution: "rollup@npm:2.79.1" + version: 2.80.0 + resolution: "rollup@npm:2.80.0" dependencies: fsevents: "npm:~2.3.2" dependenciesMeta: @@ -26675,34 +26814,40 @@ asn1@evs-broadcast/node-asn1: optional: true bin: rollup: dist/bin/rollup - checksum: 10/df087b701304432f30922bbee5f534ab189aa6938bd383b5686c03147e0d00cd1789ea10a462361326ce6b6ebe448ce272ad3f3cc40b82eeb3157df12f33663c - languageName: node - linkType: hard - -"rollup@npm:^4.23.0": - version: 4.34.2 - resolution: "rollup@npm:4.34.2" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.34.2" - "@rollup/rollup-android-arm64": "npm:4.34.2" - "@rollup/rollup-darwin-arm64": "npm:4.34.2" - "@rollup/rollup-darwin-x64": "npm:4.34.2" - "@rollup/rollup-freebsd-arm64": "npm:4.34.2" - "@rollup/rollup-freebsd-x64": "npm:4.34.2" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.34.2" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.34.2" - "@rollup/rollup-linux-arm64-gnu": "npm:4.34.2" - "@rollup/rollup-linux-arm64-musl": "npm:4.34.2" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.34.2" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.34.2" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.34.2" - "@rollup/rollup-linux-s390x-gnu": "npm:4.34.2" - "@rollup/rollup-linux-x64-gnu": "npm:4.34.2" - "@rollup/rollup-linux-x64-musl": "npm:4.34.2" - "@rollup/rollup-win32-arm64-msvc": "npm:4.34.2" - "@rollup/rollup-win32-ia32-msvc": "npm:4.34.2" - "@rollup/rollup-win32-x64-msvc": "npm:4.34.2" - "@types/estree": "npm:1.0.6" + checksum: 10/1150ab0f71d59e25a0fe6c0d07e49615ada1e9deba1754073be527c48c558a019fcd31d4d927a9c172593b7dc9c7c3c6871ef07fe1e575371ee24400a7c58213 + languageName: node + linkType: hard + +"rollup@npm:^4.43.0": + version: 4.59.0 + resolution: "rollup@npm:4.59.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.59.0" + "@rollup/rollup-android-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-arm64": "npm:4.59.0" + "@rollup/rollup-darwin-x64": "npm:4.59.0" + "@rollup/rollup-freebsd-arm64": "npm:4.59.0" + "@rollup/rollup-freebsd-x64": "npm:4.59.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.59.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.59.0" + "@rollup/rollup-linux-loong64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-loong64-musl": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-ppc64-musl": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-riscv64-musl": "npm:4.59.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.59.0" + "@rollup/rollup-linux-x64-musl": "npm:4.59.0" + "@rollup/rollup-openbsd-x64": "npm:4.59.0" + "@rollup/rollup-openharmony-arm64": "npm:4.59.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.59.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.59.0" + "@rollup/rollup-win32-x64-gnu": "npm:4.59.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.59.0" + "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: "@rollup/rollup-android-arm-eabi": @@ -26725,29 +26870,41 @@ asn1@evs-broadcast/node-asn1: optional: true "@rollup/rollup-linux-arm64-musl": optional: true - "@rollup/rollup-linux-loongarch64-gnu": + "@rollup/rollup-linux-loong64-gnu": + optional: true + "@rollup/rollup-linux-loong64-musl": optional: true - "@rollup/rollup-linux-powerpc64le-gnu": + "@rollup/rollup-linux-ppc64-gnu": + optional: true + "@rollup/rollup-linux-ppc64-musl": optional: true "@rollup/rollup-linux-riscv64-gnu": optional: true + "@rollup/rollup-linux-riscv64-musl": + optional: true "@rollup/rollup-linux-s390x-gnu": optional: true "@rollup/rollup-linux-x64-gnu": optional: true "@rollup/rollup-linux-x64-musl": optional: true + "@rollup/rollup-openbsd-x64": + optional: true + "@rollup/rollup-openharmony-arm64": + optional: true "@rollup/rollup-win32-arm64-msvc": optional: true "@rollup/rollup-win32-ia32-msvc": optional: true + "@rollup/rollup-win32-x64-gnu": + optional: true "@rollup/rollup-win32-x64-msvc": optional: true fsevents: optional: true bin: rollup: dist/bin/rollup - checksum: 10/c0ae28179719adea7a5883be0aa5537378ae765def94feedcd36ea36ac9623ab7ef8857c8569f31a2f5f1ec59e90b402b730b2f58bacfcb9295aa2d5141b941a + checksum: 10/728237932aad7022c0640cd126b9fe5285f2578099f22a0542229a17785320a6553b74582fa5977877541c1faf27de65ed2750bc89dbb55b525405244a46d9f1 languageName: node linkType: hard @@ -26821,7 +26978,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"rxjs@npm:7.8.2, rxjs@npm:^7.5.5, rxjs@npm:^7.8.2": +"rxjs@npm:7.8.2, rxjs@npm:^7.4.0, rxjs@npm:^7.5.5, rxjs@npm:^7.8.2": version: 7.8.2 resolution: "rxjs@npm:7.8.2" dependencies: @@ -26830,15 +26987,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"rxjs@npm:^6.6.3": - version: 6.6.7 - resolution: "rxjs@npm:6.6.7" - dependencies: - tslib: "npm:^1.9.0" - checksum: 10/c8263ebb20da80dd7a91c452b9e96a178331f402344bbb40bc772b56340fcd48d13d1f545a1e3d8e464893008c5e306cc42a1552afe0d562b1a6d4e1e6262b03 - languageName: node - linkType: hard - "safe-array-concat@npm:^1.1.3": version: 1.1.3 resolution: "safe-array-concat@npm:1.1.3" @@ -26859,7 +27007,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10/32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451 @@ -26908,9 +27056,211 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"sass@npm:^1.83.4": - version: 1.83.4 - resolution: "sass@npm:1.83.4" +"sass-embedded-all-unknown@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-all-unknown@npm:1.97.3" + dependencies: + sass: "npm:1.97.3" + conditions: (!cpu=arm | !cpu=arm64 | !cpu=riscv64 | !cpu=x64) + languageName: node + linkType: hard + +"sass-embedded-android-arm64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-android-arm64@npm:1.97.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-android-arm@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-android-arm@npm:1.97.3" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"sass-embedded-android-riscv64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-android-riscv64@npm:1.97.3" + conditions: os=android & cpu=riscv64 + languageName: node + linkType: hard + +"sass-embedded-android-x64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-android-x64@npm:1.97.3" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-darwin-arm64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-darwin-arm64@npm:1.97.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-darwin-x64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-darwin-x64@npm:1.97.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-linux-arm64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-linux-arm64@npm:1.97.3" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-linux-arm@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-linux-arm@npm:1.97.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"sass-embedded-linux-musl-arm64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-linux-musl-arm64@npm:1.97.3" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-linux-musl-arm@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-linux-musl-arm@npm:1.97.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"sass-embedded-linux-musl-riscv64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-linux-musl-riscv64@npm:1.97.3" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"sass-embedded-linux-musl-x64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-linux-musl-x64@npm:1.97.3" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-linux-riscv64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-linux-riscv64@npm:1.97.3" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"sass-embedded-linux-x64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-linux-x64@npm:1.97.3" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded-unknown-all@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-unknown-all@npm:1.97.3" + dependencies: + sass: "npm:1.97.3" + conditions: (!os=android | !os=darwin | !os=linux | !os=win32) + languageName: node + linkType: hard + +"sass-embedded-win32-arm64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-win32-arm64@npm:1.97.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"sass-embedded-win32-x64@npm:1.97.3": + version: 1.97.3 + resolution: "sass-embedded-win32-x64@npm:1.97.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"sass-embedded@npm:^1.97.3": + version: 1.97.3 + resolution: "sass-embedded@npm:1.97.3" + dependencies: + "@bufbuild/protobuf": "npm:^2.5.0" + colorjs.io: "npm:^0.5.0" + immutable: "npm:^5.0.2" + rxjs: "npm:^7.4.0" + sass-embedded-all-unknown: "npm:1.97.3" + sass-embedded-android-arm: "npm:1.97.3" + sass-embedded-android-arm64: "npm:1.97.3" + sass-embedded-android-riscv64: "npm:1.97.3" + sass-embedded-android-x64: "npm:1.97.3" + sass-embedded-darwin-arm64: "npm:1.97.3" + sass-embedded-darwin-x64: "npm:1.97.3" + sass-embedded-linux-arm: "npm:1.97.3" + sass-embedded-linux-arm64: "npm:1.97.3" + sass-embedded-linux-musl-arm: "npm:1.97.3" + sass-embedded-linux-musl-arm64: "npm:1.97.3" + sass-embedded-linux-musl-riscv64: "npm:1.97.3" + sass-embedded-linux-musl-x64: "npm:1.97.3" + sass-embedded-linux-riscv64: "npm:1.97.3" + sass-embedded-linux-x64: "npm:1.97.3" + sass-embedded-unknown-all: "npm:1.97.3" + sass-embedded-win32-arm64: "npm:1.97.3" + sass-embedded-win32-x64: "npm:1.97.3" + supports-color: "npm:^8.1.1" + sync-child-process: "npm:^1.0.2" + varint: "npm:^6.0.0" + dependenciesMeta: + sass-embedded-all-unknown: + optional: true + sass-embedded-android-arm: + optional: true + sass-embedded-android-arm64: + optional: true + sass-embedded-android-riscv64: + optional: true + sass-embedded-android-x64: + optional: true + sass-embedded-darwin-arm64: + optional: true + sass-embedded-darwin-x64: + optional: true + sass-embedded-linux-arm: + optional: true + sass-embedded-linux-arm64: + optional: true + sass-embedded-linux-musl-arm: + optional: true + sass-embedded-linux-musl-arm64: + optional: true + sass-embedded-linux-musl-riscv64: + optional: true + sass-embedded-linux-musl-x64: + optional: true + sass-embedded-linux-riscv64: + optional: true + sass-embedded-linux-x64: + optional: true + sass-embedded-unknown-all: + optional: true + sass-embedded-win32-arm64: + optional: true + sass-embedded-win32-x64: + optional: true + bin: + sass: dist/bin/sass.js + checksum: 10/4f15e28b1e0b67da63a1b13b15d0daab3746a266ab1bb0708523a4dd9b3e9fb8d7293547197a6446cbeee0ccb303b26a27a97bbdeed5a5b34bff90c7298ba899 + languageName: node + linkType: hard + +"sass@npm:1.97.3": + version: 1.97.3 + resolution: "sass@npm:1.97.3" dependencies: "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" @@ -26921,14 +27271,14 @@ asn1@evs-broadcast/node-asn1: optional: true bin: sass: sass.js - checksum: 10/9a7d1c6be1a9e711a1c561d189b9816aa7715f6d0ec0b2ec181f64163788d0caaf4741924eeadce558720b58b1de0e9b21b9dae6a0d14489c4d2a142d3f3b12e + checksum: 10/707ef8e525ed32d375e737346140d4b675f44de208df996c2df3407f5e62f3f38226ea1faf41a9fd4b068201e67b3a7e152b9e9c3b098daa847dd480c735f038 languageName: node linkType: hard -"sax@npm:>=0.6.0, sax@npm:^1.2.4": - version: 1.2.4 - resolution: "sax@npm:1.2.4" - checksum: 10/09b79ff6dc09689a24323352117c94593c69db348997b2af0edbd82fa08aba47d778055bf9616b57285bb73d25d790900c044bf631a8f10c8252412e3f3fe5dd +"sax@npm:>=0.6.0, sax@npm:^1.2.4, sax@npm:^1.5.0": + version: 1.5.0 + resolution: "sax@npm:1.5.0" + checksum: 10/9012ff37dda7a7ac5da45db2143b04036103e8bef8d586c3023afd5df6caf0ebd7f38017eee344ad2e2247eded7d38e9c42cf291d8dd91781352900ac0fd2d9f languageName: node linkType: hard @@ -26957,7 +27307,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"schema-utils@npm:^3.0.0, schema-utils@npm:^3.1.1, schema-utils@npm:^3.2.0": +"schema-utils@npm:^3.0.0": version: 3.3.0 resolution: "schema-utils@npm:3.3.0" dependencies: @@ -26968,7 +27318,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"schema-utils@npm:^4.0.0, schema-utils@npm:^4.0.1, schema-utils@npm:^4.2.0": +"schema-utils@npm:^4.0.0, schema-utils@npm:^4.0.1, schema-utils@npm:^4.2.0, schema-utils@npm:^4.3.0, schema-utils@npm:^4.3.3": version: 4.3.3 resolution: "schema-utils@npm:4.3.3" dependencies: @@ -27034,7 +27384,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"semver@npm:^6.3.0, semver@npm:^6.3.1": +"semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -27043,12 +27393,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:^7.7.2": - version: 7.7.3 - resolution: "semver@npm:7.7.3" +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.3, semver@npm:^7.7.2, semver@npm:^7.7.3": + version: 7.7.4 + resolution: "semver@npm:7.7.4" bin: semver: bin/semver.js - checksum: 10/8dbc3168e057a38fc322af909c7f5617483c50caddba135439ff09a754b20bdd6482a5123ff543dad4affa488ecf46ec5fb56d61312ad20bb140199b88dfaea9 + checksum: 10/26bdc6d58b29528f4142d29afb8526bc335f4fc04c4a10f2b98b217f277a031c66736bf82d3d3bb354a2f6a3ae50f18fd62b053c4ac3f294a3d10a61f5075b75 languageName: node linkType: hard @@ -27224,22 +27574,23 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"setprototypeof@npm:1.2.0": +"setprototypeof@npm:1.2.0, setprototypeof@npm:~1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" checksum: 10/fde1630422502fbbc19e6844346778f99d449986b2f9cdcceb8326730d2f3d9964dbcb03c02aaadaefffecd0f2c063315ebea8b3ad895914bf1afc1747fc172e languageName: node linkType: hard -"sha.js@npm:^2.4.0, sha.js@npm:^2.4.11, sha.js@npm:^2.4.8": - version: 2.4.11 - resolution: "sha.js@npm:2.4.11" +"sha.js@npm:^2.4.0, sha.js@npm:^2.4.12, sha.js@npm:^2.4.8": + version: 2.4.12 + resolution: "sha.js@npm:2.4.12" dependencies: - inherits: "npm:^2.0.1" - safe-buffer: "npm:^5.0.1" + inherits: "npm:^2.0.4" + safe-buffer: "npm:^5.2.1" + to-buffer: "npm:^1.2.0" bin: - sha.js: ./bin.js - checksum: 10/d833bfa3e0a67579a6ce6e1bc95571f05246e0a441dd8c76e3057972f2a3e098465687a4369b07e83a0375a88703577f71b5b2e966809e67ebc340dbedb478c7 + sha.js: bin.js + checksum: 10/39c0993592c2ab34eb2daae2199a2a1d502713765aecb611fd97c0c4ab7cd53e902d628e1962aaf384bafd28f55951fef46dcc78799069ce41d74b03aa13b5a7 languageName: node linkType: hard @@ -27282,22 +27633,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"shell-quote@npm:^1.8.3": +"shell-quote@npm:1.8.3, shell-quote@npm:^1.8.3": version: 1.8.3 resolution: "shell-quote@npm:1.8.3" checksum: 10/5473e354637c2bd698911224129c9a8961697486cff1fb221f234d71c153fc377674029b0223d1d3c953a68d451d79366abfe53d1a0b46ee1f28eb9ade928f4c languageName: node linkType: hard -"shuttle-webhid@npm:^0.0.2": - version: 0.0.2 - resolution: "shuttle-webhid@npm:0.0.2" +"shuttle-webhid@npm:^0.1.3": + version: 0.1.3 + resolution: "shuttle-webhid@npm:0.1.3" dependencies: - "@shuttle-lib/core": "npm:0.0.2" - "@types/w3c-web-hid": "npm:^1.0.3" - buffer: "npm:^6.0.3" - p-queue: "npm:^6.6.2" - checksum: 10/fa0d72aa0e9a8de01623d2e3394fa4ac8e0414b8daa7fdbd7249c1ef4701ee2b84da101cd2f35bc7d1ef5253e6568ab079494be9aae7a761dabaffaa8e9b6f6c + "@shuttle-lib/core": "npm:0.1.3" + "@types/w3c-web-hid": "npm:^1.0.6" + eventemitter3: "npm:^5.0.1" + checksum: 10/d0e2d92df8ab9e83aeb62652c1b316681a8a4a97caca19a0477805adf90636817ad72c19a00b16247c245200f18e7b0c9d9dc57c812738e532191bf39b522daa languageName: node linkType: hard @@ -27409,15 +27759,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"simple-swizzle@npm:^0.2.2": - version: 0.2.2 - resolution: "simple-swizzle@npm:0.2.2" - dependencies: - is-arrayish: "npm:^0.3.1" - checksum: 10/c6dffff17aaa383dae7e5c056fbf10cf9855a9f79949f20ee225c04f06ddde56323600e0f3d6797e82d08d006e93761122527438ee9531620031c08c9e0d73cc - languageName: node - linkType: hard - "simple-update-notifier@npm:^1.0.7": version: 1.1.0 resolution: "simple-update-notifier@npm:1.1.0" @@ -27427,20 +27768,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"sinon@npm:^14.0.2": - version: 14.0.2 - resolution: "sinon@npm:14.0.2" - dependencies: - "@sinonjs/commons": "npm:^2.0.0" - "@sinonjs/fake-timers": "npm:^9.1.2" - "@sinonjs/samsam": "npm:^7.0.1" - diff: "npm:^5.0.0" - nise: "npm:^5.1.2" - supports-color: "npm:^7.2.0" - checksum: 10/851ce34e0c3a20eda40fe50bfe044d074e86a9e73e00ac30c30e73da1d05c9cfa840ab2e29346940f5b804dc83cd10a3d748fcb43fd0d719dc16f2463c00c1ce - languageName: node - linkType: hard - "sirv@npm:^2.0.3": version: 2.0.3 resolution: "sirv@npm:2.0.3" @@ -27697,13 +28024,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"spawn-command@npm:^0.0.2-1": - version: 0.0.2 - resolution: "spawn-command@npm:0.0.2" - checksum: 10/f13e8c3c63abd4a0b52fb567eba5f7940d480c5ed3ec61781d38a1850f179b1196c39e6efa2bbd301f82c1bf1cd7807abc8fbd8fc8e44bcaa3975a124c0d1657 - languageName: node - linkType: hard - "spdx-compare@npm:^1.0.0": version: 1.0.0 resolution: "spdx-compare@npm:1.0.0" @@ -27897,7 +28217,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"stack-utils@npm:^2.0.3": +"stack-utils@npm:^2.0.6": version: 2.0.6 resolution: "stack-utils@npm:2.0.6" dependencies: @@ -27927,7 +28247,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"statuses@npm:^2.0.1": +"statuses@npm:^2.0.1, statuses@npm:~2.0.2": version: 2.0.2 resolution: "statuses@npm:2.0.2" checksum: 10/6927feb50c2a75b2a4caab2c565491f7a93ad3d8dbad7b1398d52359e9243a20e2ebe35e33726dee945125ef7a515e9097d8a1b910ba2bbd818265a2f6c39879 @@ -27981,6 +28301,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"streamx@npm:^2.15.0, streamx@npm:^2.21.0": + version: 2.23.0 + resolution: "streamx@npm:2.23.0" + dependencies: + events-universal: "npm:^1.0.0" + fast-fifo: "npm:^1.3.2" + text-decoder: "npm:^1.1.0" + checksum: 10/4969d7032b16497172afa2f8ac889d137764963ae564daf1611a03225dd62d9316d51de8098b5866d21722babde71353067184e7a3e9795d6dc17c902904a780 + languageName: node + linkType: hard + "strict-uri-encode@npm:^2.0.0": version: 2.0.0 resolution: "strict-uri-encode@npm:2.0.0" @@ -27988,7 +28319,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"string-length@npm:^4.0.1": +"string-length@npm:^4.0.2": version: 4.0.2 resolution: "string-length@npm:4.0.2" dependencies: @@ -28213,12 +28544,19 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"strtok3@npm:^10.2.0": - version: 10.3.1 - resolution: "strtok3@npm:10.3.1" +"strnum@npm:^2.1.0": + version: 2.1.2 + resolution: "strnum@npm:2.1.2" + checksum: 10/7d894dff385e3a5c5b29c012cf0a7ea7962a92c6a299383c3d6db945ad2b6f3e770511356a9774dbd54444c56af1dc7c435dad6466c47293c48173274dd6c631 + languageName: node + linkType: hard + +"strtok3@npm:^10.3.4": + version: 10.3.4 + resolution: "strtok3@npm:10.3.4" dependencies: "@tokenizer/token": "npm:^0.3.0" - checksum: 10/bb7950cc9ce98ec742a5db360630f0b004f16197959ae28d8c8dad4f8f0e405d71cfdc992483038ba29a0b4cbd7227618ad2492005b510d84a3fc5903df0c13f + checksum: 10/53be14a567dca149be56cb072eaa3c0fffd70d066acf800cf588b91558c6d475364ff8d550524ce0499fc4873a4b0d42ad8c542bfdb9fb39cba520ef2e2e9818 languageName: node linkType: hard @@ -28278,6 +28616,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"supports-color@npm:8.1.1, supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": + version: 8.1.1 + resolution: "supports-color@npm:8.1.1" + dependencies: + has-flag: "npm:^4.0.0" + checksum: 10/157b534df88e39c5518c5e78c35580c1eca848d7dbaf31bbe06cdfc048e22c7ff1a9d046ae17b25691128f631a51d9ec373c1b740c12ae4f0de6e292037e4282 + languageName: node + linkType: hard + "supports-color@npm:^5.3.0, supports-color@npm:^5.5.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -28287,7 +28634,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": +"supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -28296,15 +28643,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"supports-color@npm:^8.0.0, supports-color@npm:^8.1.0": - version: 8.1.1 - resolution: "supports-color@npm:8.1.1" - dependencies: - has-flag: "npm:^4.0.0" - checksum: 10/157b534df88e39c5518c5e78c35580c1eca848d7dbaf31bbe06cdfc048e22c7ff1a9d046ae17b25691128f631a51d9ec373c1b740c12ae4f0de6e292037e4282 - languageName: node - linkType: hard - "supports-preserve-symlinks-flag@npm:^1.0.0": version: 1.0.0 resolution: "supports-preserve-symlinks-flag@npm:1.0.0" @@ -28320,19 +28658,19 @@ asn1@evs-broadcast/node-asn1: linkType: hard "svgo@npm:^3.0.2, svgo@npm:^3.2.0": - version: 3.3.2 - resolution: "svgo@npm:3.3.2" + version: 3.3.3 + resolution: "svgo@npm:3.3.3" dependencies: - "@trysound/sax": "npm:0.2.0" commander: "npm:^7.2.0" css-select: "npm:^5.1.0" css-tree: "npm:^2.3.1" css-what: "npm:^6.1.0" csso: "npm:^5.0.5" picocolors: "npm:^1.0.0" + sax: "npm:^1.5.0" bin: svgo: ./bin/svgo - checksum: 10/82fdea9b938884d808506104228e4d3af0050d643d5b46ff7abc903ff47a91bbf6561373394868aaf07a28f006c4057b8fbf14bbd666298abdd7cc590d4f7700 + checksum: 10/f3c1b4d05d1704483e53515d5995af5f06a2718df85e3a8320f57bb256b8dc926b84c87a1a9b98e9d3ca1224314cc0676a803bdd03163508292f2d45c7077096 languageName: node linkType: hard @@ -28355,6 +28693,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"sync-child-process@npm:^1.0.2": + version: 1.0.2 + resolution: "sync-child-process@npm:1.0.2" + dependencies: + sync-message-port: "npm:^1.0.0" + checksum: 10/6fbdbb7b6f5730a1966d6a77cdbfe7f5cb8d1a582dab955c62c32b56dc6c432ccdbfc68027265486f8f4b1a998cc4d7ee21856e8125748bef70b8874aaedb21c + languageName: node + linkType: hard + "sync-fetch@npm:^0.5.2": version: 0.5.2 resolution: "sync-fetch@npm:0.5.2" @@ -28364,36 +28711,65 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"synckit@npm:^0.9.1": - version: 0.9.2 - resolution: "synckit@npm:0.9.2" +"sync-message-port@npm:^1.0.0": + version: 1.2.0 + resolution: "sync-message-port@npm:1.2.0" + checksum: 10/b5e58ef3f5074c8ac481ec173246da8ddf001aeb2c93c7d32ed9ff905384663c14a13fdae0ee0fb46f5592c79aa0c8851b08c3e1ea7dce51ef25c4936b22bb65 + languageName: node + linkType: hard + +"synckit@npm:^0.11.12, synckit@npm:^0.11.8": + version: 0.11.12 + resolution: "synckit@npm:0.11.12" dependencies: - "@pkgr/core": "npm:^0.1.0" - tslib: "npm:^2.6.2" - checksum: 10/d45c4288be9c0232343650643892a7edafb79152c0c08d7ae5d33ca2c296b67a0e15f8cb5c9153969612c4ea5cd5686297542384aab977db23cfa6653fe02027 + "@pkgr/core": "npm:^0.2.9" + checksum: 10/2f51978bfed81aaf0b093f596709a72c49b17909020f42b43c5549f9c0fe18b1fe29f82e41ef771172d729b32e9ce82900a85d2b87fa14d59f886d4df8d7a329 languageName: node linkType: hard -"tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": - version: 2.2.1 - resolution: "tapable@npm:2.2.1" - checksum: 10/1769336dd21481ae6347611ca5fca47add0962fd8e80466515032125eca0084a4f0ede11e65341b9c0018ef4e1cf1ad820adbb0fba7cc99865c6005734000b0a +"tabbable@npm:^6.0.0": + version: 6.4.0 + resolution: "tabbable@npm:6.4.0" + checksum: 10/0fe8fada2d97bd02058af2e0176bddca26b1100c069e0a096ac19ad8ef61bd0b4f0cf05e1dd68229b8f1cb6fe6bf4c34d50a5f4a3e26b150a92f89b7dc0a4916 languageName: node linkType: hard -"tar-fs@npm:2.1.1": - version: 2.1.1 - resolution: "tar-fs@npm:2.1.1" +"tapable@npm:^2.0.0, tapable@npm:^2.2.1, tapable@npm:^2.3.0": + version: 2.3.0 + resolution: "tapable@npm:2.3.0" + checksum: 10/496a841039960533bb6e44816a01fffc2a1eb428bb2051ecab9e87adf07f19e1f937566cbbbb09dceff31163c0ffd81baafcad84db900b601f0155dd0b37e9f2 + languageName: node + linkType: hard + +"tar-fs@npm:^3.1.1": + version: 3.1.1 + resolution: "tar-fs@npm:3.1.1" dependencies: - chownr: "npm:^1.1.1" - mkdirp-classic: "npm:^0.5.2" + bare-fs: "npm:^4.0.1" + bare-path: "npm:^3.0.0" pump: "npm:^3.0.0" - tar-stream: "npm:^2.1.4" - checksum: 10/526deae025453e825f87650808969662fbb12eb0461d033e9b447de60ec951c6c4607d0afe7ce057defe9d4e45cf80399dd74bc15f9d9e0773d5e990a78ce4ac + tar-stream: "npm:^3.1.5" + dependenciesMeta: + bare-fs: + optional: true + bare-path: + optional: true + checksum: 10/f7f7540b563e10541dc0b95f710c68fc1fccde0c1177b4d3bab2023c6d18da19d941a8697fdc1abff54914b71b6e5f2dfb0455572b5c8993b2ab76571cbbc923 languageName: node linkType: hard -"tar-stream@npm:^2.1.4, tar-stream@npm:~2.2.0": +"tar-stream@npm:^3.1.5": + version: 3.1.7 + resolution: "tar-stream@npm:3.1.7" + dependencies: + b4a: "npm:^1.6.4" + fast-fifo: "npm:^1.2.0" + streamx: "npm:^2.15.0" + checksum: 10/b21a82705a72792544697c410451a4846af1f744176feb0ff11a7c3dd0896961552e3def5e1c9a6bbee4f0ae298b8252a1f4c9381e9f991553b9e4847976f05c + languageName: node + linkType: hard + +"tar-stream@npm:~2.2.0": version: 2.2.0 resolution: "tar-stream@npm:2.2.0" dependencies: @@ -28406,7 +28782,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tar@npm:6.2.1, tar@npm:^6.1.11, tar@npm:^6.1.2": +"tar@npm:7.5.8": + version: 7.5.8 + resolution: "tar@npm:7.5.8" + dependencies: + "@isaacs/fs-minipass": "npm:^4.0.0" + chownr: "npm:^3.0.0" + minipass: "npm:^7.1.2" + minizlib: "npm:^3.1.0" + yallist: "npm:^5.0.0" + checksum: 10/5fddc22e0fd03e73d5e9e922e71d8681f85443dee4f21403059a757e186ae4004abc9a709cdc7f4143d7d75758a2935f7306b3cc193123d46b6f786dd2b99c2a + languageName: node + linkType: hard + +"tar@npm:^6.1.11, tar@npm:^6.1.2": version: 6.2.1 resolution: "tar@npm:6.2.1" dependencies: @@ -28420,16 +28809,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tar@npm:^7.4.3": - version: 7.5.6 - resolution: "tar@npm:7.5.6" +"tar@npm:^7.4.3, tar@npm:^7.5.4": + version: 7.5.11 + resolution: "tar@npm:7.5.11" dependencies: "@isaacs/fs-minipass": "npm:^4.0.0" chownr: "npm:^3.0.0" minipass: "npm:^7.1.2" minizlib: "npm:^3.1.0" yallist: "npm:^5.0.0" - checksum: 10/cf4a84d79b9327fcf765f2ea16de4702b9b8dd7dc6b1840b16e0f999628d96b81b2c7efbf83d4eb42b0164856f1db887a5a61ffef97d36cdb77cac742219f9ee + checksum: 10/fb2e77ee858a73936c68e066f4a602d428d6f812e6da0cc1e14a41f99498e4f7fd3535e355fa15157240a5538aa416026cfa6306bb0d1d1c1abf314b1f878e9a languageName: node linkType: hard @@ -28468,15 +28857,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.9": - version: 5.3.10 - resolution: "terser-webpack-plugin@npm:5.3.10" +"terser-webpack-plugin@npm:^5.3.17, terser-webpack-plugin@npm:^5.3.9": + version: 5.4.0 + resolution: "terser-webpack-plugin@npm:5.4.0" dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.20" + "@jridgewell/trace-mapping": "npm:^0.3.25" jest-worker: "npm:^27.4.5" - schema-utils: "npm:^3.1.1" - serialize-javascript: "npm:^6.0.1" - terser: "npm:^5.26.0" + schema-utils: "npm:^4.3.0" + terser: "npm:^5.31.1" peerDependencies: webpack: ^5.1.0 peerDependenciesMeta: @@ -28486,21 +28874,21 @@ asn1@evs-broadcast/node-asn1: optional: true uglify-js: optional: true - checksum: 10/fb1c2436ae1b4e983be043fa0a3d355c047b16b68f102437d08c736d7960c001e7420e2f722b9d99ce0dc70ca26a68cc63c0b82bc45f5b48671142b352a9d938 + checksum: 10/f4618b18cec5dd41fca4a53f621ea06df04ff7bb2b09d3766559284e171a91df2884083e5c143aaacee2000870b046eb7157e39d1d2d8024577395165a070094 languageName: node linkType: hard -"terser@npm:^5.10.0, terser@npm:^5.15.1, terser@npm:^5.26.0": - version: 5.30.3 - resolution: "terser@npm:5.30.3" +"terser@npm:^5.10.0, terser@npm:^5.15.1, terser@npm:^5.31.1": + version: 5.46.0 + resolution: "terser@npm:5.46.0" dependencies: "@jridgewell/source-map": "npm:^0.3.3" - acorn: "npm:^8.8.2" + acorn: "npm:^8.15.0" commander: "npm:^2.20.0" source-map-support: "npm:~0.5.20" bin: terser: bin/terser - checksum: 10/f4ee378065a327c85472f351ac232fa47ec84d4f15df7ec58c044b41e3c063cf11aaedd90dcfe9c7f2a6ef01d4aab23deb61622301170dc77d0a8b6a6a83cf5e + checksum: 10/331e4f5a165d91d16ac6a95b510d4f5ef24679e4bc9e1b4e4182e89b7245f614d24ce0def583e2ca3ca45f82ba810991e0c5b66dd4353a6e0b7082786af6bd35 languageName: node linkType: hard @@ -28515,6 +28903,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"text-decoder@npm:^1.1.0": + version: 1.2.3 + resolution: "text-decoder@npm:1.2.3" + dependencies: + b4a: "npm:^1.6.4" + checksum: 10/bcdec33c0f070aeac38e46e4cafdcd567a58473ed308bdf75260bfbd8f7dc76acbc0b13226afaec4a169d0cb44cec2ab89c57b6395ccf02e941eaebbe19e124a + languageName: node + linkType: hard + "text-extensions@npm:^1.0.0": version: 1.9.0 resolution: "text-extensions@npm:1.9.0" @@ -28529,24 +28926,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"thenify-all@npm:^1.0.0": - version: 1.6.0 - resolution: "thenify-all@npm:1.6.0" - dependencies: - thenify: "npm:>= 3.1.0 < 4" - checksum: 10/dba7cc8a23a154cdcb6acb7f51d61511c37a6b077ec5ab5da6e8b874272015937788402fd271fdfc5f187f8cb0948e38d0a42dcc89d554d731652ab458f5343e - languageName: node - linkType: hard - -"thenify@npm:>= 3.1.0 < 4": - version: 3.3.1 - resolution: "thenify@npm:3.3.1" - dependencies: - any-promise: "npm:^1.0.0" - checksum: 10/486e1283a867440a904e36741ff1a177faa827cf94d69506f7e3ae4187b9afdf9ec368b3d8da225c192bfe2eb943f3f0080594156bf39f21b57cd1411e2e7f6d - languageName: node - linkType: hard - "thingies@npm:^2.5.0": version: 2.5.0 resolution: "thingies@npm:2.5.0" @@ -28565,7 +28944,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"threadedclass@npm:^1.2.1, threadedclass@npm:^1.2.2, threadedclass@npm:^1.3.0": +"threadedclass@npm:^1.2.1, threadedclass@npm:^1.3.0": version: 1.3.0 resolution: "threadedclass@npm:1.3.0" dependencies: @@ -28594,7 +28973,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"through@npm:2, through@npm:2.3.8, through@npm:>=2.2.7 <3, through@npm:^2.3.6, through@npm:^2.3.8": +"through@npm:2, through@npm:2.3.8, through@npm:>=2.2.7 <3, through@npm:^2.3.6": version: 2.3.8 resolution: "through@npm:2.3.8" checksum: 10/5da78346f70139a7d213b65a0106f3c398d6bc5301f9248b5275f420abc2c4b1e77c2abc72d218dedc28c41efb2e7c312cb76a7730d04f9c2d37d247da3f4198 @@ -28622,43 +29001,44 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-api@npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0": - version: 10.0.0-nightly-release53-20251217-143607-df590aa96.0 - resolution: "timeline-state-resolver-api@npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" +"timeline-state-resolver-api@npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0": + version: 10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0 + resolution: "timeline-state-resolver-api@npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0" dependencies: tslib: "npm:^2.8.1" peerDependencies: - timeline-state-resolver-types: 10.0.0-nightly-release53-20251217-143607-df590aa96.0 - checksum: 10/fb174edd6694643c8ca16f851593ab09cd2ea4edbadaa70944f92bf5dad97a18aab1b5a454a353428589cb104f150f379eaba394ae57099843388c12b27c7683 + timeline-state-resolver-types: 10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0 + checksum: 10/4c1a3c19efca2ffd0a679bb2e13e418081f2b515dd49ef5bb19604bed19e8adfcf4321ba1dd38f3f741e558b6397286604b7a05ecb9e8fc4217fb3bf796b9301 languageName: node linkType: hard -"timeline-state-resolver-types@npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0": - version: 10.0.0-nightly-release53-20251217-143607-df590aa96.0 - resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" +"timeline-state-resolver-types@npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0": + version: 10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0 + resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0" dependencies: + kairos-lib: "npm:0.2.3" tslib: "npm:^2.8.1" - checksum: 10/6f17030e9f10568757b3ebaae40a54ce5220ce5c2f37d9b5d8a5d286704e4779c80fbfddae500e1952a6efdf6e76b67bc18d0151211c84eb194ae3b52f78c7bf + checksum: 10/5558f4a80ebc60f3f86e76172965fce0f415cfd76fd109f0c84ad6873521a1dbe054a60e1c8d87535b2e16df3be25e637099742b38628be9c794f1701762903a languageName: node linkType: hard -"timeline-state-resolver@npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0": - version: 10.0.0-nightly-release53-20251217-143607-df590aa96.0 - resolution: "timeline-state-resolver@npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" +"timeline-state-resolver@npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0": + version: 10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0 + resolution: "timeline-state-resolver@npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0" dependencies: "@tv2media/v-connection": "npm:^7.3.4" atem-connection: "npm:3.7.0" - atem-state: "npm:1.2.0" + atem-state: "npm:1.3.0" cacheable-lookup: "npm:^5.0.4" casparcg-connection: "npm:6.3.3" casparcg-state: "npm:3.0.4" debug: "npm:^4.4.3" deepmerge: "npm:^4.3.1" emberplus-connection: "npm:^0.3.1" - eventemitter3: "npm:^4.0.7" got: "npm:^11.8.6" hpagent: "npm:^1.2.0" hyperdeck-connection: "npm:2.0.1" + kairos-connection: "npm:0.2.3" klona: "npm:^2.0.6" obs-websocket-js: "npm:^5.0.7" osc: "npm:^2.4.5" @@ -28668,16 +29048,16 @@ asn1@evs-broadcast/node-asn1: sprintf-js: "npm:^1.1.3" superfly-timeline: "npm:^9.2.0" threadedclass: "npm:^1.3.0" - timeline-state-resolver-api: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" - timeline-state-resolver-types: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" + timeline-state-resolver-api: "npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0" + timeline-state-resolver-types: "npm:10.0.0-nightly-chore-update-atem-state-20260309-091304-dea89c910.0" tslib: "npm:^2.8.1" tv-automation-quantel-gateway-client: "npm:^3.1.7" type-fest: "npm:^3.13.1" underscore: "npm:^1.13.7" - utf-8-validate: "npm:^6.0.5" - ws: "npm:^8.18.3" + utf-8-validate: "npm:^6.0.6" + ws: "npm:^8.19.0" xml-js: "npm:^1.6.11" - checksum: 10/82b22c7945946005485c38ad8fcb94314bcba99aaf3aa549ec30326bb33548e46fe3ee5f6d48c06e6b593405458cbf1bbbda8a95793f57507f0158180e441686 + checksum: 10/8306519fb7f7ff43e9f576d5cb0f9ebc76d48309bfd02d5573f078b4fef5f66cff8e8e574baa9a09f32aefbf14df0562be3fb17da8ad3204e5edfe21f0752554 languageName: node linkType: hard @@ -28690,16 +29070,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timers-ext@npm:^0.1.7": - version: 0.1.7 - resolution: "timers-ext@npm:0.1.7" - dependencies: - es5-ext: "npm:~0.10.46" - next-tick: "npm:1" - checksum: 10/a8fffe2841ed6c3b16b2e72522ee46537c6a758294da45486c7e8ca52ff065931dd023c9f9946b87a13f48ae3dafe12678ab1f9d1ef24b6aea465762e0ffdcae - languageName: node - linkType: hard - "tiny-invariant@npm:^1.0.2": version: 1.3.1 resolution: "tiny-invariant@npm:1.3.1" @@ -28731,7 +29101,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12": +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.9": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -28766,15 +29136,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tmp@npm:^0.0.33": - version: 0.0.33 - resolution: "tmp@npm:0.0.33" - dependencies: - os-tmpdir: "npm:~1.0.2" - checksum: 10/09c0abfd165cff29b32be42bc35e80b8c64727d97dedde6550022e88fa9fd39a084660415ed8e3ebaa2aca1ee142f86df8b31d4196d4f81c774a3a20fd4b6abf - languageName: node - linkType: hard - "tmp@npm:~0.2.1": version: 0.2.5 resolution: "tmp@npm:0.2.5" @@ -28789,6 +29150,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"to-buffer@npm:^1.2.0, to-buffer@npm:^1.2.1, to-buffer@npm:^1.2.2": + version: 1.2.2 + resolution: "to-buffer@npm:1.2.2" + dependencies: + isarray: "npm:^2.0.5" + safe-buffer: "npm:^5.2.1" + typed-array-buffer: "npm:^1.0.3" + checksum: 10/69d806c20524ff1e4c44d49276bc96ff282dcae484780a3974e275dabeb75651ea430b074a2a4023701e63b3e1d87811cd82c0972f35280fe5461710e4872aba + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -28798,7 +29170,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"toidentifier@npm:1.0.1": +"toidentifier@npm:1.0.1, toidentifier@npm:~1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" checksum: 10/952c29e2a85d7123239b5cfdd889a0dde47ab0497f0913d70588f19c53f7e0b5327c95f4651e413c74b785147f9637b17410ac8c846d5d4a20a5a33eb6dc3a45 @@ -28815,13 +29187,14 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"token-types@npm:^6.0.0": - version: 6.0.0 - resolution: "token-types@npm:6.0.0" +"token-types@npm:^6.1.1": + version: 6.1.2 + resolution: "token-types@npm:6.1.2" dependencies: + "@borewit/text-codec": "npm:^0.2.1" "@tokenizer/token": "npm:^0.3.0" ieee754: "npm:^1.2.1" - checksum: 10/b541b605d602e8e6495745badb35f90ee8f997e43dc29bc51aee7e9a0bc3c6bc7372a305bd45f3e80d75223c2b6a5c7e65cb5159d8c4e49fa25cdbaae531fad4 + checksum: 10/0c7811a2da5a0ca474c795d883d871a184d1d54f67058d66084110f0b246fff66151885dbcb91d66533e776478bf57f3b4fac69ce03b805a0e1060def87947de languageName: node linkType: hard @@ -28843,19 +29216,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tough-cookie@npm:^4.1.2": - version: 4.1.4 - resolution: "tough-cookie@npm:4.1.4" - dependencies: - psl: "npm:^1.1.33" - punycode: "npm:^2.1.1" - universalify: "npm:^0.2.0" - url-parse: "npm:^1.5.3" - checksum: 10/75663f4e2cd085f16af0b217e4218772adf0617fb3227171102618a54ce0187a164e505d61f773ed7d65988f8ff8a8f935d381f87da981752c1171b076b4afac - languageName: node - linkType: hard - -"tough-cookie@npm:^5.0.0": +"tough-cookie@npm:^5.1.1": version: 5.1.2 resolution: "tough-cookie@npm:5.1.2" dependencies: @@ -28873,15 +29234,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tr46@npm:^3.0.0": - version: 3.0.0 - resolution: "tr46@npm:3.0.0" - dependencies: - punycode: "npm:^2.1.1" - checksum: 10/b09a15886cbfaee419a3469081223489051ce9dca3374dd9500d2378adedbee84a3c73f83bfdd6bb13d53657753fc0d4e20a46bfcd3f1b9057ef528426ad7ce4 - languageName: node - linkType: hard - "tr46@npm:^5.0.0": version: 5.0.0 resolution: "tr46@npm:5.0.0" @@ -28891,6 +29243,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"tr46@npm:^5.1.0": + version: 5.1.1 + resolution: "tr46@npm:5.1.1" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10/833a0e1044574da5790148fd17866d4ddaea89e022de50279967bcd6b28b4ce0d30d59eb3acf9702b60918975b3bad481400337e3a2e6326cffa5c77b874753d + languageName: node + linkType: hard + "tr46@npm:~0.0.3": version: 0.0.3 resolution: "tr46@npm:0.0.3" @@ -28907,7 +29268,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tree-kill@npm:^1.2.2": +"tree-kill@npm:1.2.2, tree-kill@npm:^1.2.2": version: 1.2.2 resolution: "tree-kill@npm:1.2.2" bin: @@ -28974,12 +29335,23 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ts-api-utils@npm:^2.0.1": - version: 2.0.1 - resolution: "ts-api-utils@npm:2.0.1" +"ts-api-utils@npm:^2.4.0": + version: 2.4.0 + resolution: "ts-api-utils@npm:2.4.0" peerDependencies: typescript: ">=4.8.4" - checksum: 10/2e68938cd5acad6b5157744215ce10cd097f9f667fd36b5fdd5efdd4b0c51063e855459d835f94f6777bb8a0f334916b6eb5c1eedab8c325feb34baa39238898 + checksum: 10/d6b2b3b6caad8d2f4ddc0c3785d22bb1a6041773335a1c71d73a5d67d11d993763fe8e4faefc4a4d03bb42b26c6126bbcf2e34826baed1def5369d0ebad358fa + languageName: node + linkType: hard + +"ts-declaration-location@npm:^1.0.6": + version: 1.0.7 + resolution: "ts-declaration-location@npm:1.0.7" + dependencies: + picomatch: "npm:^4.0.2" + peerDependencies: + typescript: ">=4.0.0" + checksum: 10/a7932fc75d41f10c16089f8f5a5c1ea49d6afca30f09c91c1df14d0a8510f72bcb9f8a395c04f060b66b855b6bd7ea4df81b335fb9d21bef402969a672a4afa7 languageName: node linkType: hard @@ -28990,37 +29362,38 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ts-essentials@npm:^10.0.0": - version: 10.0.4 - resolution: "ts-essentials@npm:10.0.4" +"ts-essentials@npm:^10.0.2": + version: 10.1.1 + resolution: "ts-essentials@npm:10.1.1" peerDependencies: typescript: ">=4.5.0" peerDependenciesMeta: typescript: optional: true - checksum: 10/f0853472370340e7752d4d4ccb18c0289b31c30526674ace288b02c77c6434b9aff04bd7cb55af406dd6f1f66b0a794bb6794c0d7c83e63aadc5d443147e6d60 + checksum: 10/ab0a468175ba6a7162aa80a55fcd936a8d830ae302f5561ca918d29a5212b4cd2e619c447bf5bc253f9d1faf186f5728c4ef8684ceaace3a7c6bbdffa54dd1bd languageName: node linkType: hard -"ts-jest@npm:^29.2.5": - version: 29.2.5 - resolution: "ts-jest@npm:29.2.5" +"ts-jest@npm:^29.4.6": + version: 29.4.6 + resolution: "ts-jest@npm:29.4.6" dependencies: bs-logger: "npm:^0.2.6" - ejs: "npm:^3.1.10" fast-json-stable-stringify: "npm:^2.1.0" - jest-util: "npm:^29.0.0" + handlebars: "npm:^4.7.8" json5: "npm:^2.2.3" lodash.memoize: "npm:^4.1.2" make-error: "npm:^1.3.6" - semver: "npm:^7.6.3" + semver: "npm:^7.7.3" + type-fest: "npm:^4.41.0" yargs-parser: "npm:^21.1.1" peerDependencies: "@babel/core": ">=7.0.0-beta.0 <8" - "@jest/transform": ^29.0.0 - "@jest/types": ^29.0.0 - babel-jest: ^29.0.0 - jest: ^29.0.0 + "@jest/transform": ^29.0.0 || ^30.0.0 + "@jest/types": ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 typescript: ">=4.3 <6" peerDependenciesMeta: "@babel/core": @@ -29033,9 +29406,11 @@ asn1@evs-broadcast/node-asn1: optional: true esbuild: optional: true + jest-util: + optional: true bin: ts-jest: cli.js - checksum: 10/f89e562816861ec4510840a6b439be6145f688b999679328de8080dc8e66481325fc5879519b662163e33b7578f35243071c38beb761af34e5fe58e3e326a958 + checksum: 10/e0ff9e13f684166d5331808b288043b8054f49a1c2970480a92ba3caec8d0ff20edd092f2a4e7a3ad8fcb9ba4d674bee10ec7ee75046d8066bbe43a7d16cf72e languageName: node linkType: hard @@ -29109,7 +29484,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tslib@npm:^1.13.0, tslib@npm:^1.14.1, tslib@npm:^1.9.0": +"tslib@npm:^1.13.0, tslib@npm:^1.14.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb @@ -29175,7 +29550,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"type-detect@npm:4.0.8, type-detect@npm:^4.0.8": +"type-detect@npm:4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" checksum: 10/5179e3b8ebc51fce1b13efb75fdea4595484433f9683bbc2dca6d99789dba4e602ab7922d2656f2ce8383987467f7770131d4a7f06a26287db0615d2f4c4ce7d @@ -29238,10 +29613,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"type-fest@npm:^4.33.0, type-fest@npm:^4.6.0, type-fest@npm:^4.7.1": - version: 4.33.0 - resolution: "type-fest@npm:4.33.0" - checksum: 10/0d179e66fa765bd0a25a785b12dc797f90f2f92bdb8c9c8a789f3fd8e5a4492444e7ef83551b3b8463aeab24fd6195761e26b03174722de636b4b75aa5726fb7 +"type-fest@npm:^4.41.0, type-fest@npm:^4.6.0, type-fest@npm:^4.7.1": + version: 4.41.0 + resolution: "type-fest@npm:4.41.0" + checksum: 10/617ace794ac0893c2986912d28b3065ad1afb484cad59297835a0807dc63286c39e8675d65f7de08fafa339afcb8fe06a36e9a188b9857756ae1e92ee8bda212 languageName: node linkType: hard @@ -29266,20 +29641,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"type@npm:^1.0.1": - version: 1.2.0 - resolution: "type@npm:1.2.0" - checksum: 10/b4d4b27d1926028be45fc5baaca205896e2a1fe9e5d24dc892046256efbe88de6acd0149e7353cd24dad596e1483e48ec60b0912aa47ca078d68cdd198b09885 - languageName: node - linkType: hard - -"type@npm:^2.7.2": - version: 2.7.2 - resolution: "type@npm:2.7.2" - checksum: 10/602f1b369fba60687fa4d0af6fcfb814075bcaf9ed3a87637fb384d9ff849e2ad15bc244a431f341374562e51a76c159527ffdb1f1f24b0f1f988f35a301c41d - languageName: node - linkType: hard - "typed-array-buffer@npm:^1.0.3": version: 1.0.3 resolution: "typed-array-buffer@npm:1.0.3" @@ -29333,10 +29694,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"typed-styles@npm:^0.0.7": - version: 0.0.7 - resolution: "typed-styles@npm:0.0.7" - checksum: 10/24704459dd5119729a5c20d156f60a1a74489e0a6a57fc6bc93a0d167c805675cc3cadd42ae5d99d7906762e951a44bca9558101353c9d37bedbe8b1e6bf6e51 +"typed-query-selector@npm:^2.12.0": + version: 2.12.0 + resolution: "typed-query-selector@npm:2.12.0" + checksum: 10/e65b646830315e63282883acb44ea48ef8da3e9a044aa69e03f3bd876d7a69baae85f71c0918456b43f7c1bc2b448f2d64a424280f9699d34be2bae582121bc9 languageName: node linkType: hard @@ -29356,9 +29717,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"typedoc@npm:^0.27.6": - version: 0.27.6 - resolution: "typedoc@npm:0.27.6" +"typedoc@npm:^0.27.9": + version: 0.27.9 + resolution: "typedoc@npm:0.27.9" dependencies: "@gerrit0/mini-shiki": "npm:^1.24.0" lunr: "npm:^2.3.9" @@ -29366,24 +29727,25 @@ asn1@evs-broadcast/node-asn1: minimatch: "npm:^9.0.5" yaml: "npm:^2.6.1" peerDependencies: - typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x + typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x bin: typedoc: bin/typedoc - checksum: 10/07a5649020f055534ada6919036a98098f27fc307e2addfd6865f4c11bd97f10167f34165fc6a0fd57dec28d9480f9ac58a4d5a3c17ec5a88296520e4f67d960 + checksum: 10/fb1e4b54849cad1628543fb24863358320b737aabba83194562360cfbcaac8684b2b18824fd623bb27a9e8dbbc0e01c0b88fedd9a642d78e7a3c108387e44d25 languageName: node linkType: hard -"typescript-eslint@npm:^8.21.0": - version: 8.30.1 - resolution: "typescript-eslint@npm:8.30.1" +"typescript-eslint@npm:^8.54.0": + version: 8.54.0 + resolution: "typescript-eslint@npm:8.54.0" dependencies: - "@typescript-eslint/eslint-plugin": "npm:8.30.1" - "@typescript-eslint/parser": "npm:8.30.1" - "@typescript-eslint/utils": "npm:8.30.1" + "@typescript-eslint/eslint-plugin": "npm:8.54.0" + "@typescript-eslint/parser": "npm:8.54.0" + "@typescript-eslint/typescript-estree": "npm:8.54.0" + "@typescript-eslint/utils": "npm:8.54.0" peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: ">=4.8.4 <5.9.0" - checksum: 10/aaa4d90abbd7631569d0d45af77fd12cd53aa3bb4e11b8f276cf4cf786ecc14b1fe99e48592f31188386bb021a31fa89b976c69bdcdd0a46dedb98744e7958f2 + typescript: ">=4.8.4 <6.0.0" + checksum: 10/21b1a27fd44716df8d2c7bac4ebd0caef196a04375fff7919dc817066017b6b8700f1e242bd065a26ac7ce0505b7a588626099e04a28142504ed4f0aae8bffb1 languageName: node linkType: hard @@ -29510,16 +29872,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"unbzip2-stream@npm:1.4.3": - version: 1.4.3 - resolution: "unbzip2-stream@npm:1.4.3" - dependencies: - buffer: "npm:^5.2.1" - through: "npm:^2.3.8" - checksum: 10/4ffc0e14f4af97400ed0f37be83b112b25309af21dd08fa55c4513e7cb4367333f63712aec010925dbe491ef6e92db1248e1e306e589f9f6a8da8b3a9c4db90b - languageName: node - linkType: hard - "uncontrollable@npm:^7.2.1": version: 7.2.1 resolution: "uncontrollable@npm:7.2.1" @@ -29551,16 +29903,16 @@ asn1@evs-broadcast/node-asn1: linkType: hard "underscore@npm:^1.13.6, underscore@npm:^1.13.7": - version: 1.13.7 - resolution: "underscore@npm:1.13.7" - checksum: 10/1ce3368dbe73d1e99678fa5d341a9682bd27316032ad2de7883901918f0f5d50e80320ccc543f53c1862ab057a818abc560462b5f83578afe2dd8dd7f779766c + version: 1.13.8 + resolution: "underscore@npm:1.13.8" + checksum: 10/b50ac5806d059cc180b1bd9adea6f7ed500021f4dc782dfc75d66a90337f6f0506623c1b37863f4a9bf64ffbeb5769b638a54b7f2f5966816189955815953139 languageName: node linkType: hard -"undici-types@npm:~6.20.0": - version: 6.20.0 - resolution: "undici-types@npm:6.20.0" - checksum: 10/583ac7bbf4ff69931d3985f4762cde2690bb607844c16a5e2fbb92ed312fe4fa1b365e953032d469fa28ba8b224e88a595f0b10a449332f83fa77c695e567dbe +"undici-types@npm:~6.21.0": + version: 6.21.0 + resolution: "undici-types@npm:6.21.0" + checksum: 10/ec8f41aa4359d50f9b59fa61fe3efce3477cc681908c8f84354d8567bb3701fafdddf36ef6bff307024d3feb42c837cf6f670314ba37fc8145e219560e473d14 languageName: node linkType: hard @@ -29598,10 +29950,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"unicode-match-property-value-ecmascript@npm:^2.1.0": - version: 2.1.0 - resolution: "unicode-match-property-value-ecmascript@npm:2.1.0" - checksum: 10/06661bc8aba2a60c7733a7044f3e13085808939ad17924ffd4f5222a650f88009eb7c09481dc9c15cfc593d4ad99bd1cde8d54042733b335672591a81c52601c +"unicode-match-property-value-ecmascript@npm:^2.2.1": + version: 2.2.1 + resolution: "unicode-match-property-value-ecmascript@npm:2.2.1" + checksum: 10/a42bebebab4c82ea6d8363e487b1fb862f82d1b54af1b67eb3fef43672939b685780f092c4f235266b90225863afa1258d57e7be3578d8986a08d8fc309aabe1 languageName: node linkType: hard @@ -29778,13 +30130,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"universalify@npm:^0.2.0": - version: 0.2.0 - resolution: "universalify@npm:0.2.0" - checksum: 10/e86134cb12919d177c2353196a4cc09981524ee87abf621f7bc8d249dbbbebaec5e7d1314b96061497981350df786e4c5128dbf442eba104d6e765bc260678b5 - languageName: node - linkType: hard - "universalify@npm:^2.0.0": version: 2.0.0 resolution: "universalify@npm:2.0.0" @@ -29799,6 +30144,73 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"unrs-resolver@npm:^1.7.11": + version: 1.11.1 + resolution: "unrs-resolver@npm:1.11.1" + dependencies: + "@unrs/resolver-binding-android-arm-eabi": "npm:1.11.1" + "@unrs/resolver-binding-android-arm64": "npm:1.11.1" + "@unrs/resolver-binding-darwin-arm64": "npm:1.11.1" + "@unrs/resolver-binding-darwin-x64": "npm:1.11.1" + "@unrs/resolver-binding-freebsd-x64": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm-gnueabihf": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm-musleabihf": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-arm64-musl": "npm:1.11.1" + "@unrs/resolver-binding-linux-ppc64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-riscv64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-riscv64-musl": "npm:1.11.1" + "@unrs/resolver-binding-linux-s390x-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-x64-gnu": "npm:1.11.1" + "@unrs/resolver-binding-linux-x64-musl": "npm:1.11.1" + "@unrs/resolver-binding-wasm32-wasi": "npm:1.11.1" + "@unrs/resolver-binding-win32-arm64-msvc": "npm:1.11.1" + "@unrs/resolver-binding-win32-ia32-msvc": "npm:1.11.1" + "@unrs/resolver-binding-win32-x64-msvc": "npm:1.11.1" + napi-postinstall: "npm:^0.3.0" + dependenciesMeta: + "@unrs/resolver-binding-android-arm-eabi": + optional: true + "@unrs/resolver-binding-android-arm64": + optional: true + "@unrs/resolver-binding-darwin-arm64": + optional: true + "@unrs/resolver-binding-darwin-x64": + optional: true + "@unrs/resolver-binding-freebsd-x64": + optional: true + "@unrs/resolver-binding-linux-arm-gnueabihf": + optional: true + "@unrs/resolver-binding-linux-arm-musleabihf": + optional: true + "@unrs/resolver-binding-linux-arm64-gnu": + optional: true + "@unrs/resolver-binding-linux-arm64-musl": + optional: true + "@unrs/resolver-binding-linux-ppc64-gnu": + optional: true + "@unrs/resolver-binding-linux-riscv64-gnu": + optional: true + "@unrs/resolver-binding-linux-riscv64-musl": + optional: true + "@unrs/resolver-binding-linux-s390x-gnu": + optional: true + "@unrs/resolver-binding-linux-x64-gnu": + optional: true + "@unrs/resolver-binding-linux-x64-musl": + optional: true + "@unrs/resolver-binding-wasm32-wasi": + optional: true + "@unrs/resolver-binding-win32-arm64-msvc": + optional: true + "@unrs/resolver-binding-win32-ia32-msvc": + optional: true + "@unrs/resolver-binding-win32-x64-msvc": + optional: true + checksum: 10/4de653508cbaae47883a896bd5cdfef0e5e87b428d62620d16fd35cd534beaebf08ebf0cf2f8b4922aa947b2fe745180facf6cc3f39ba364f7ce0f974cb06a70 + languageName: node + linkType: hard + "untildify@npm:^4.0.0": version: 4.0.0 resolution: "untildify@npm:4.0.0" @@ -29813,9 +30225,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"update-browserslist-db@npm:^1.1.4": - version: 1.1.4 - resolution: "update-browserslist-db@npm:1.1.4" +"update-browserslist-db@npm:^1.2.0": + version: 1.2.3 + resolution: "update-browserslist-db@npm:1.2.3" dependencies: escalade: "npm:^3.2.0" picocolors: "npm:^1.1.1" @@ -29823,7 +30235,7 @@ asn1@evs-broadcast/node-asn1: browserslist: ">= 4.21.0" bin: update-browserslist-db: cli.js - checksum: 10/79b2c0a31e9b837b49dc55d5cb7b77f44a69502847c7be352a44b1d35ac2032bf0e1bb7543f992809ed427bf9d32aa3f7ad41cef96198fa959c1666870174c06 + checksum: 10/059f774300efb4b084a49293143c511f3ae946d40397b5c30914e900cd5691a12b8e61b41dd54ed73d3b56c8204165a0333107dd784ccf8f8c81790bcc423175 languageName: node linkType: hard @@ -29900,7 +30312,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"url-parse@npm:^1.5.3, url-parse@npm:~1.5.10": +"url-parse@npm:~1.5.10": version: 1.5.10 resolution: "url-parse@npm:1.5.10" dependencies: @@ -29910,13 +30322,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"url@npm:^0.11.0": - version: 0.11.3 - resolution: "url@npm:0.11.3" +"url@npm:^0.11.4": + version: 0.11.4 + resolution: "url@npm:0.11.4" dependencies: punycode: "npm:^1.4.1" - qs: "npm:^6.11.2" - checksum: 10/a3a5ba64d8afb4dda111355d94073a9754b88b1de4035554c398b75f3e4d4244d5e7ae9e4554f0d91be72efd416aedbb646fbb1f3dd4cacecca45ed6c9b75145 + qs: "npm:^6.12.3" + checksum: 10/e787d070f0756518b982a4653ef6cdf4d9030d8691eee2d483344faf2b530b71d302287fa63b292299455fea5075c502a5ad5f920cb790e95605847f957a65e4 languageName: node linkType: hard @@ -29941,13 +30353,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"utf-8-validate@npm:^6.0.5": - version: 6.0.5 - resolution: "utf-8-validate@npm:6.0.5" +"utf-8-validate@npm:^6.0.6": + version: 6.0.6 + resolution: "utf-8-validate@npm:6.0.6" dependencies: node-gyp: "npm:latest" node-gyp-build: "npm:^4.3.0" - checksum: 10/8c96d342064d3f03d7acf616fe727e484825f4f5f7a455059122787306b2df1a4e23c2d27f16bf7ba21293f4ce6ab3e683b893fe7b4c74ac9d43b871c10001a0 + checksum: 10/c1fa53fe5f0e3b7bf990a8ee41d890b10218b087a4ad401519a1a6353a427172fedc29c9af36b81080ea27b311802ae37b0e857b82aaa976238904398870f465 languageName: node linkType: hard @@ -30082,6 +30494,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"varint@npm:^6.0.0": + version: 6.0.0 + resolution: "varint@npm:6.0.0" + checksum: 10/7684113c9d497c01e40396e50169c502eb2176203219b96e1c5ac965a3e15b4892bd22b7e48d87148e10fffe638130516b6dbeedd0efde2b2d0395aa1772eea7 + languageName: node + linkType: hard + "vary@npm:^1.1.2, vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -30120,15 +30539,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"vite-plugin-node-polyfills@npm:^0.23.0": - version: 0.23.0 - resolution: "vite-plugin-node-polyfills@npm:0.23.0" +"vite-plugin-node-polyfills@npm:^0.25.0": + version: 0.25.0 + resolution: "vite-plugin-node-polyfills@npm:0.25.0" dependencies: "@rollup/plugin-inject": "npm:^5.0.5" - node-stdlib-browser: "npm:^1.2.0" + node-stdlib-browser: "npm:^1.3.1" peerDependencies: - vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 - checksum: 10/91cd35b54a06c623cb660282e128d889d43b19b6edbc0316114905b488161c9877b7a8f36c2f736317c3cec1980daad74ee776629d3c8a157ad51e1a0d7ee363 + vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10/4e49d2a8143a60962559180f5aa2a8360041ed20f5782d3f8287eb7d70401f763b394caf494a7356f8dfd2806901afc6ea0a4ceb30451d846abc9ee3a508ffd6 languageName: node linkType: hard @@ -30148,23 +30567,26 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"vite@npm:^6.0.11": - version: 6.0.11 - resolution: "vite@npm:6.0.11" +"vite@npm:^7.3.1": + version: 7.3.1 + resolution: "vite@npm:7.3.1" dependencies: - esbuild: "npm:^0.24.2" + esbuild: "npm:^0.27.0" + fdir: "npm:^6.5.0" fsevents: "npm:~2.3.3" - postcss: "npm:^8.4.49" - rollup: "npm:^4.23.0" + picomatch: "npm:^4.0.3" + postcss: "npm:^8.5.6" + rollup: "npm:^4.43.0" + tinyglobby: "npm:^0.2.15" peerDependencies: - "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 + "@types/node": ^20.19.0 || >=22.12.0 jiti: ">=1.21.0" - less: "*" + less: ^4.0.0 lightningcss: ^1.21.0 - sass: "*" - sass-embedded: "*" - stylus: "*" - sugarss: "*" + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: ">=0.54.8" + sugarss: ^5.0.0 terser: ^5.16.0 tsx: ^4.8.1 yaml: ^2.4.2 @@ -30196,7 +30618,7 @@ asn1@evs-broadcast/node-asn1: optional: true bin: vite: bin/vite.js - checksum: 10/753d06b07a4d90863d3478162cfb18fa5cd7f6eb22a74525348a8fd46593a82875d0f92352c2f4833e15cb6581fc97d6ab434c0c5d83d8d58cfbbe6e7267726d + checksum: 10/62e48ffa4283b688f0049005405a004447ad38ffc99a0efea4c3aa9b7eed739f7402b43f00668c0ee5a895b684dc953d62f0722d8a92c5b2f6c95f051bceb208 languageName: node linkType: hard @@ -30263,15 +30685,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"w3c-xmlserializer@npm:^4.0.0": - version: 4.0.0 - resolution: "w3c-xmlserializer@npm:4.0.0" - dependencies: - xml-name-validator: "npm:^4.0.0" - checksum: 10/9a00c412b5496f4f040842c9520bc0aaec6e0c015d06412a91a723cd7d84ea605ab903965f546b4ecdb3eae267f5145ba08565222b1d6cb443ee488cda9a0aee - languageName: node - linkType: hard - "w3c-xmlserializer@npm:^5.0.0": version: 5.0.0 resolution: "w3c-xmlserializer@npm:5.0.0" @@ -30329,13 +30742,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"watchpack@npm:^2.4.1": - version: 2.4.1 - resolution: "watchpack@npm:2.4.1" +"watchpack@npm:^2.5.1": + version: 2.5.1 + resolution: "watchpack@npm:2.5.1" dependencies: glob-to-regexp: "npm:^0.4.1" graceful-fs: "npm:^4.1.2" - checksum: 10/0736ebd20b75d3931f9b6175c819a66dee29297c1b389b2e178bc53396a6f867ecc2fd5d87a713ae92dcb73e487daec4905beee20ca00a9e27f1184a7c2bca5e + checksum: 10/9c9cdd4a9f9ae146b10d15387f383f52589e4cc27b324da6be8e7e3e755255b062a69dd7f00eef2ce67b2c01e546aae353456e74f8c1350bba00462cc6375549 languageName: node linkType: hard @@ -30388,6 +30801,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"webdriver-bidi-protocol@npm:0.4.0": + version: 0.4.0 + resolution: "webdriver-bidi-protocol@npm:0.4.0" + checksum: 10/6caa22ce6e8820dc1b7dd76fb24d29d9e051a58f87c4a4f01887012edf6f322e6851afaa7af80c746f35f71440ee2d6018acefe9fde070168b975cb5af66f2a3 + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -30524,46 +30944,48 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"webpack-sources@npm:^3.2.3": - version: 3.2.3 - resolution: "webpack-sources@npm:3.2.3" - checksum: 10/a661f41795d678b7526ae8a88cd1b3d8ce71a7d19b6503da8149b2e667fc7a12f9b899041c1665d39e38245ed3a59ab68de648ea31040c3829aa695a5a45211d +"webpack-sources@npm:^3.3.4": + version: 3.3.4 + resolution: "webpack-sources@npm:3.3.4" + checksum: 10/714427b235b04c2d7cf229f204b9e65145ea3643da3c7b139ebfa8a51056238d1e3a2a47c3cc3fc8eab71ed4300f66405cdc7cff29cd2f7f6b71086252f81cf1 languageName: node linkType: hard "webpack@npm:^5.88.1, webpack@npm:^5.95.0": - version: 5.97.1 - resolution: "webpack@npm:5.97.1" + version: 5.105.4 + resolution: "webpack@npm:5.105.4" dependencies: "@types/eslint-scope": "npm:^3.7.7" - "@types/estree": "npm:^1.0.6" + "@types/estree": "npm:^1.0.8" + "@types/json-schema": "npm:^7.0.15" "@webassemblyjs/ast": "npm:^1.14.1" "@webassemblyjs/wasm-edit": "npm:^1.14.1" "@webassemblyjs/wasm-parser": "npm:^1.14.1" - acorn: "npm:^8.14.0" - browserslist: "npm:^4.24.0" + acorn: "npm:^8.16.0" + acorn-import-phases: "npm:^1.0.3" + browserslist: "npm:^4.28.1" chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.17.1" - es-module-lexer: "npm:^1.2.1" + enhanced-resolve: "npm:^5.20.0" + es-module-lexer: "npm:^2.0.0" eslint-scope: "npm:5.1.1" events: "npm:^3.2.0" glob-to-regexp: "npm:^0.4.1" graceful-fs: "npm:^4.2.11" json-parse-even-better-errors: "npm:^2.3.1" - loader-runner: "npm:^4.2.0" + loader-runner: "npm:^4.3.1" mime-types: "npm:^2.1.27" neo-async: "npm:^2.6.2" - schema-utils: "npm:^3.2.0" - tapable: "npm:^2.1.1" - terser-webpack-plugin: "npm:^5.3.10" - watchpack: "npm:^2.4.1" - webpack-sources: "npm:^3.2.3" + schema-utils: "npm:^4.3.3" + tapable: "npm:^2.3.0" + terser-webpack-plugin: "npm:^5.3.17" + watchpack: "npm:^2.5.1" + webpack-sources: "npm:^3.3.4" peerDependenciesMeta: webpack-cli: optional: true bin: webpack: bin/webpack.js - checksum: 10/665bd3b8c84b20f0b1f250159865e4d3e9b76c682030313d49124d5f8e96357ccdcc799dd9fe0ebf010fdb33dbc59d9863d79676a308e868e360ac98f7c09987 + checksum: 10/ae8088dd1c995fa17b920009f864138297a9ea5089bc563601f661fa4a31bb24b000cc91ae122168ce9def79c49258b8aa1021c2754c3555205c29a0d6c9cc8d languageName: node linkType: hard @@ -30615,15 +31037,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"whatwg-encoding@npm:^2.0.0": - version: 2.0.0 - resolution: "whatwg-encoding@npm:2.0.0" - dependencies: - iconv-lite: "npm:0.6.3" - checksum: 10/162d712d88fd134a4fe587e53302da812eb4215a1baa4c394dfd86eff31d0a079ff932c05233857997de07481093358d6e7587997358f49b8a580a777be22089 - languageName: node - linkType: hard - "whatwg-encoding@npm:^3.1.1": version: 3.1.1 resolution: "whatwg-encoding@npm:3.1.1" @@ -30633,13 +31046,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"whatwg-mimetype@npm:^3.0.0": - version: 3.0.0 - resolution: "whatwg-mimetype@npm:3.0.0" - checksum: 10/96f9f628c663c2ae05412c185ca81b3df54bcb921ab52fe9ebc0081c1720f25d770665401eb2338ab7f48c71568133845638e18a81ed52ab5d4dcef7d22b40ef - languageName: node - linkType: hard - "whatwg-mimetype@npm:^4.0.0": version: 4.0.0 resolution: "whatwg-mimetype@npm:4.0.0" @@ -30647,23 +31053,23 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"whatwg-url@npm:^11.0.0": - version: 11.0.0 - resolution: "whatwg-url@npm:11.0.0" +"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.0 || ^13.0.0": + version: 14.1.1 + resolution: "whatwg-url@npm:14.1.1" dependencies: - tr46: "npm:^3.0.0" + tr46: "npm:^5.0.0" webidl-conversions: "npm:^7.0.0" - checksum: 10/dfcd51c6f4bfb54685528fb10927f3fd3d7c809b5671beef4a8cdd7b1408a7abf3343a35bc71dab83a1424f1c1e92cc2700d7930d95d231df0fac361de0c7648 + checksum: 10/803bede3ec6c8f14de0d84ac6032479646b5a2b08f5a7289366c3461caed9d7888d171e2846b59798869191037562c965235c2eed6ff2e266c05a2b4a6ce0160 languageName: node linkType: hard -"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.0, whatwg-url@npm:^14.1.0 || ^13.0.0": - version: 14.1.1 - resolution: "whatwg-url@npm:14.1.1" +"whatwg-url@npm:^14.1.1": + version: 14.2.0 + resolution: "whatwg-url@npm:14.2.0" dependencies: - tr46: "npm:^5.0.0" + tr46: "npm:^5.1.0" webidl-conversions: "npm:^7.0.0" - checksum: 10/803bede3ec6c8f14de0d84ac6032479646b5a2b08f5a7289366c3461caed9d7888d171e2846b59798869191037562c965235c2eed6ff2e266c05a2b4a6ce0160 + checksum: 10/f0a95b0601c64f417c471536a2d828b4c16fe37c13662483a32f02f183ed0f441616609b0663fb791e524e8cd56d9a86dd7366b1fc5356048ccb09b576495e7c languageName: node linkType: hard @@ -30828,12 +31234,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"winston@npm:^3.17.0": - version: 3.17.0 - resolution: "winston@npm:3.17.0" +"winston@npm:^3.19.0": + version: 3.19.0 + resolution: "winston@npm:3.19.0" dependencies: "@colors/colors": "npm:^1.6.0" - "@dabh/diagnostics": "npm:^2.0.2" + "@dabh/diagnostics": "npm:^2.0.8" async: "npm:^3.2.3" is-stream: "npm:^2.0.0" logform: "npm:^2.7.0" @@ -30843,7 +31249,7 @@ asn1@evs-broadcast/node-asn1: stack-trace: "npm:0.0.x" triple-beam: "npm:^1.3.0" winston-transport: "npm:^4.9.0" - checksum: 10/220309a0ead36c1171158ab28cb9133f8597fba19c8c1c190df9329555530565b58f3af0037c1b80e0c49f7f9b6b3b01791d0c56536eb0be38678d36e316c2a3 + checksum: 10/8279e221d8017da601a725939d31d65de71504d8328051312a85b1b4d7ddc68634329f8d611fb1ff91cb797643409635f3e97ef5b4a650c587639e080af76b7b languageName: node linkType: hard @@ -30901,7 +31307,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"write-file-atomic@npm:5.0.1": +"write-file-atomic@npm:5.0.1, write-file-atomic@npm:^5.0.1": version: 5.0.1 resolution: "write-file-atomic@npm:5.0.1" dependencies: @@ -30934,7 +31340,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"write-file-atomic@npm:^4.0.0, write-file-atomic@npm:^4.0.2": +"write-file-atomic@npm:^4.0.0": version: 4.0.2 resolution: "write-file-atomic@npm:4.0.2" dependencies: @@ -30994,21 +31400,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ws@npm:8.7.0": - version: 8.7.0 - resolution: "ws@npm:8.7.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10/ec85bd9c1fb304d34fa6ca319726859f8209094618ec09b1f6ad4f274d1c5561bf633319c0166551e3ca6d8ae2cf860e73262782c32afadc52539a3e5cb00346 - languageName: node - linkType: hard - "ws@npm:^7.3.1, ws@npm:^7.5.10": version: 7.5.10 resolution: "ws@npm:7.5.10" @@ -31024,9 +31415,9 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ws@npm:^8.11.0, ws@npm:^8.13.0, ws@npm:^8.18.0, ws@npm:^8.18.3": - version: 8.18.3 - resolution: "ws@npm:8.18.3" +"ws@npm:^8.13.0, ws@npm:^8.18.0, ws@npm:^8.19.0": + version: 8.19.0 + resolution: "ws@npm:8.19.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -31035,7 +31426,7 @@ asn1@evs-broadcast/node-asn1: optional: true utf-8-validate: optional: true - checksum: 10/725964438d752f0ab0de582cd48d6eeada58d1511c3f613485b5598a83680bedac6187c765b0fe082e2d8cc4341fc57707c813ae780feee82d0c5efe6a4c61b6 + checksum: 10/26e4901e93abaf73af9f26a93707c95b4845e91a7a347ec8c569e6e9be7f9df066f6c2b817b2d685544e208207898a750b78461e6e8d810c11a370771450c31b languageName: node linkType: hard @@ -31066,13 +31457,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"xml-name-validator@npm:^4.0.0": - version: 4.0.0 - resolution: "xml-name-validator@npm:4.0.0" - checksum: 10/f9582a3f281f790344a471c207516e29e293c6041b2c20d84dd6e58832cd7c19796c47e108fd4fd4b164a5e72ad94f2268f8ace8231cde4a2c6428d6aa220f92 - languageName: node - linkType: hard - "xml-name-validator@npm:^5.0.0": version: 5.0.0 resolution: "xml-name-validator@npm:5.0.0" @@ -31146,23 +31530,22 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"yaml-eslint-parser@npm:^1.2.1": - version: 1.2.2 - resolution: "yaml-eslint-parser@npm:1.2.2" +"yaml-eslint-parser@npm:^2.0.0": + version: 2.0.0 + resolution: "yaml-eslint-parser@npm:2.0.0" dependencies: - eslint-visitor-keys: "npm:^3.0.0" - lodash: "npm:^4.17.21" + eslint-visitor-keys: "npm:^5.0.0" yaml: "npm:^2.0.0" - checksum: 10/286de5b26011ff828d726189a38b8cd942a97f3ea5f777a6c87294906c580c438079ce393566d4f490201c5cfd274aef0f878d30f83c8e929d768aa1c47fde66 + checksum: 10/5fe6e12c649399239765cc639bc9ab0ffa8630163074c1f54c84465c4f8456fa1ebd382e037016879edb47ecdb46a27b040fca091e9a611c23877629aeca5365 languageName: node linkType: hard -"yaml@npm:^2.0.0, yaml@npm:^2.6.0, yaml@npm:^2.6.1, yaml@npm:^2.8.1": - version: 2.8.1 - resolution: "yaml@npm:2.8.1" +"yaml@npm:^2.0.0, yaml@npm:^2.6.0, yaml@npm:^2.6.1, yaml@npm:^2.8.2": + version: 2.8.2 + resolution: "yaml@npm:2.8.2" bin: yaml: bin.mjs - checksum: 10/eae07b3947d405012672ec17ce27348aea7d1fa0534143355d24a43a58f5e05652157ea2182c4fe0604f0540be71f99f1173f9d61018379404507790dff17665 + checksum: 10/4eab0074da6bc5a5bffd25b9b359cf7061b771b95d1b3b571852098380db3b1b8f96e0f1f354b56cc7216aa97cea25163377ccbc33a2e9ce00316fe8d02f4539 languageName: node linkType: hard @@ -31180,7 +31563,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"yargs@npm:17.7.2, yargs@npm:^17.1.1, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2": +"yargs@npm:17.7.2, yargs@npm:^17.1.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: @@ -31248,6 +31631,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"zod@npm:^3.24.1": + version: 3.25.76 + resolution: "zod@npm:3.25.76" + checksum: 10/f0c963ec40cd96858451d1690404d603d36507c1fc9682f2dae59ab38b578687d542708a7fdbf645f77926f78c9ed558f57c3d3aa226c285f798df0c4da16995 + languageName: node + linkType: hard + "zod@npm:^4.1.8": version: 4.1.12 resolution: "zod@npm:4.1.12" diff --git a/scripts/lib.js b/scripts/lib.js index 58376c572aa..ea9fad5917d 100644 --- a/scripts/lib.js +++ b/scripts/lib.js @@ -1,9 +1,50 @@ const args = process.argv.slice(2); +// Check for --help +if (args.indexOf("--help") >= 0 || args.indexOf("-h") >= 0) { + console.log(` +Sofie Core Development Mode + +Usage: yarn dev [options] + yarn start [options] + +Note: 'yarn start' runs install + build + dev, while 'yarn dev' just runs dev mode. + All options work with both commands. + +Options: + --help, -h Show this help message + --ui-only Only watch and build UI packages (skip job-worker, gateways) + --inspect-meteor Run Meteor with Node.js inspector enabled + --verbose Enable verbose logging + --db= Use a named database directory (e.g., --db=demo) + Creates meteor/.meteor/local/db. and switches to it with a symlink + Original database is backed up to db.default on first use + Run without --db to use the currently active database + --db-list List all available database directories and show which is active + +Examples: + yarn start # Install, build, then run in dev mode + yarn dev # Run in normal dev mode (requires prior build) + yarn dev --db-list # List all available databases + yarn dev --db=testing # Use a separate database for testing + yarn dev --db=demo # Switch to demo database + yarn dev --ui-only # Only watch UI, skip backend packages + yarn dev --inspect-meteor # Debug Meteor with inspector + yarn start --db=demo # Install, build, and run with demo database +`); + process.exit(0); +} + +// Parse --db=name option +const dbArg = args.find(arg => arg.startsWith('--db=')); +const dbName = dbArg ? dbArg.split('=')[1] : null; + const config = { uiOnly: args.indexOf("--ui-only") >= 0 || false, inspectMeteor: args.indexOf("--inspect-meteor") >= 0 || false, verbose: args.indexOf("--verbose") >= 0 || false, + dbName: dbName, + dbList: args.indexOf("--db-list") >= 0 || false, }; module.exports = { diff --git a/scripts/run.mjs b/scripts/run.mjs index a2a65ff3c66..a7020c5e1f1 100644 --- a/scripts/run.mjs +++ b/scripts/run.mjs @@ -1,5 +1,6 @@ import process from "process"; import fs from "fs"; +import path from "path"; import concurrently from "concurrently"; import { config } from "./lib.js"; @@ -71,8 +72,120 @@ function hr() { return "─".repeat(process.stdout.columns ?? 40); } +function listDatabases() { + const meteorLocalDir = path.join('meteor', '.meteor', 'local'); + const dbLink = path.join(meteorLocalDir, 'db'); + + if (!fs.existsSync(meteorLocalDir)) { + console.log('No databases found (meteor/.meteor/local does not exist yet)'); + return; + } + + // Get current database + let currentDb = null; + if (fs.existsSync(dbLink)) { + const stats = fs.lstatSync(dbLink); + if (stats.isSymbolicLink()) { + const target = fs.readlinkSync(dbLink); + const match = target.match(/^db\.(.+)$/); + if (match) { + currentDb = match[1]; + } + } else { + currentDb = '(unnamed - real directory)'; + } + } + + // List all db.* directories + const files = fs.readdirSync(meteorLocalDir); + const dbDirs = files + .filter(file => file.startsWith('db.') && fs.lstatSync(path.join(meteorLocalDir, file)).isDirectory()) + .map(file => file.substring(3)); + + console.log('\nAvailable databases:'); + if (dbDirs.length === 0) { + console.log(' (none found)'); + } else { + dbDirs.sort().forEach(db => { + const marker = db === currentDb ? ' ← current' : ''; + console.log(` ${db}${marker}`); + }); + } + + if (currentDb && !dbDirs.includes(currentDb)) { + console.log(`\nCurrent: ${currentDb}`); + } + console.log(''); +} + +function switchDatabase(dbName) { + const meteorLocalDir = path.join('meteor', '.meteor', 'local'); + const dbLink = path.join(meteorLocalDir, 'db'); + const dbTarget = path.join(meteorLocalDir, `db.${dbName}`); + + // Check if we're already using this database + if (fs.existsSync(dbLink)) { + const stats = fs.lstatSync(dbLink); + if (stats.isSymbolicLink()) { + const currentTarget = fs.readlinkSync(dbLink); + if (currentTarget === `db.${dbName}`) { + console.log(`✓ Already using database: ${dbName}`); + return; + } + } + } + + // Create target directory if it doesn't exist + if (!fs.existsSync(dbTarget)) { + console.log(`Creating new database directory: ${dbName}`); + fs.mkdirSync(dbTarget, { recursive: true }); + } + + // Remove existing db link/directory + if (fs.existsSync(dbLink)) { + const stats = fs.lstatSync(dbLink); + if (stats.isSymbolicLink()) { + fs.unlinkSync(dbLink); + } else { + // It's a real directory - back it up with timestamp + const defaultDb = path.join(meteorLocalDir, 'db.default'); + if (!fs.existsSync(defaultDb)) { + console.log(`Backing up existing database to: default`); + fs.renameSync(dbLink, defaultDb); + } else { + // Default already exists, create timestamped backup instead of deleting + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19); + let backupName = path.join(meteorLocalDir, `db.backup.${timestamp}`); + // Ensure unique backup name + let suffix = 0; + while (fs.existsSync(backupName)) { + suffix++; + backupName = path.join(meteorLocalDir, `db.backup.${timestamp}.${suffix}`); + } + console.log(`Backing up existing database to: ${path.basename(backupName)}`); + fs.renameSync(dbLink, backupName); + } + } + } + + // Create symlink to target database + fs.symlinkSync(`db.${dbName}`, dbLink); + console.log(`✓ Switched to database: ${dbName}`); +} + try { - // Note: This scricpt assumes that install-and-build.mjs has been run before + // Note: This script assumes that install-and-build.mjs has been run before + + // List databases if requested + if (config.dbList) { + listDatabases(); + process.exit(0); + } + + // Switch database if requested + if (config.dbName) { + switchDatabase(config.dbName); + } // The main watching execution console.log(hr()); diff --git a/yarn.lock b/yarn.lock index f131946d836..ca740255653 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,7 +5,7 @@ __metadata: version: 8 cacheKey: 10 -"@arcanis/slice-ansi@npm:^1.0.2": +"@arcanis/slice-ansi@npm:^1.1.1": version: 1.1.1 resolution: "@arcanis/slice-ansi@npm:1.1.1" dependencies: @@ -14,22 +14,6 @@ __metadata: languageName: node linkType: hard -"@isaacs/balanced-match@npm:^4.0.1": - version: 4.0.1 - resolution: "@isaacs/balanced-match@npm:4.0.1" - checksum: 10/102fbc6d2c0d5edf8f6dbf2b3feb21695a21bc850f11bc47c4f06aa83bd8884fde3fe9d6d797d619901d96865fdcb4569ac2a54c937992c48885c5e3d9967fe8 - languageName: node - linkType: hard - -"@isaacs/brace-expansion@npm:^5.0.0": - version: 5.0.0 - resolution: "@isaacs/brace-expansion@npm:5.0.0" - dependencies: - "@isaacs/balanced-match": "npm:^4.0.1" - checksum: 10/cf3b7f206aff12128214a1df764ac8cdbc517c110db85249b945282407e3dfc5c6e66286383a7c9391a059fc8e6e6a8ca82262fc9d2590bd615376141fbebd2d - languageName: node - linkType: hard - "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -80,9 +64,9 @@ __metadata: languageName: node linkType: hard -"@snyk/dep-graph@npm:^2.3.0": - version: 2.9.0 - resolution: "@snyk/dep-graph@npm:2.9.0" +"@snyk/dep-graph@npm:^2.12.0": + version: 2.13.0 + resolution: "@snyk/dep-graph@npm:2.13.0" dependencies: event-loop-spinner: "npm:^2.1.0" lodash.clone: "npm:^4.5.0" @@ -100,10 +84,10 @@ __metadata: lodash.union: "npm:^4.6.0" lodash.values: "npm:^4.3.0" object-hash: "npm:^3.0.0" - packageurl-js: "npm:1.2.0" + packageurl-js: "npm:2.0.1" semver: "npm:^7.0.0" tslib: "npm:^2" - checksum: 10/60228f3b0999b42139907f7be67aa6769a4885ca6a291ebaa4d4f296e653f60e0e291f7dc2696c658fea76f086583cc61d67f34fc1d1e5dacf00f6ca46362d21 + checksum: 10/dc8025746ea264256a1a4a143932ad96d1134593e79351f14b189ed45d30c9bebc27e9e01c5d79ee8bc5057f0ee7565965f0ef980010472b9e491814275ff299 languageName: node linkType: hard @@ -193,13 +177,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^13.7.0": - version: 13.13.52 - resolution: "@types/node@npm:13.13.52" - checksum: 10/a1fbd080dd2462f6f0d0c10cb8328ee6b22e59941fb6beb8bca907f96e00798ce85e94320ccab3bf04f87d6c5443535a62e6896ac59c34c79a286821223e56cd - languageName: node - linkType: hard - "@types/responselike@npm:^1.0.0": version: 1.0.3 resolution: "@types/responselike@npm:1.0.3" @@ -223,72 +200,59 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/core@npm:^2.4.0": - version: 2.4.0 - resolution: "@yarnpkg/core@npm:2.4.0" +"@yarnpkg/core@npm:^4.4.1": + version: 4.5.0 + resolution: "@yarnpkg/core@npm:4.5.0" dependencies: - "@arcanis/slice-ansi": "npm:^1.0.2" + "@arcanis/slice-ansi": "npm:^1.1.1" "@types/semver": "npm:^7.1.0" "@types/treeify": "npm:^1.0.0" - "@yarnpkg/fslib": "npm:^2.4.0" - "@yarnpkg/json-proxy": "npm:^2.1.0" - "@yarnpkg/libzip": "npm:^2.2.1" - "@yarnpkg/parsers": "npm:^2.3.0" - "@yarnpkg/pnp": "npm:^2.3.2" - "@yarnpkg/shell": "npm:^2.4.1" - binjumper: "npm:^0.1.4" + "@yarnpkg/fslib": "npm:^3.1.4" + "@yarnpkg/libzip": "npm:^3.2.2" + "@yarnpkg/parsers": "npm:^3.0.3" + "@yarnpkg/shell": "npm:^4.1.3" camelcase: "npm:^5.3.1" - chalk: "npm:^3.0.0" - ci-info: "npm:^2.0.0" - clipanion: "npm:^2.6.2" - cross-spawn: "npm:7.0.3" - diff: "npm:^4.0.1" - globby: "npm:^11.0.1" + chalk: "npm:^4.1.2" + ci-info: "npm:^4.0.0" + clipanion: "npm:^4.0.0-rc.2" + cross-spawn: "npm:^7.0.3" + diff: "npm:^5.1.0" + dotenv: "npm:^16.3.1" + es-toolkit: "npm:^1.39.7" + fast-glob: "npm:^3.2.2" got: "npm:^11.7.0" - json-file-plus: "npm:^3.3.1" - lodash: "npm:^4.17.15" + hpagent: "npm:^1.2.0" micromatch: "npm:^4.0.2" - mkdirp: "npm:^0.5.1" p-limit: "npm:^2.2.0" - pluralize: "npm:^7.0.0" - pretty-bytes: "npm:^5.1.0" semver: "npm:^7.1.2" - stream-to-promise: "npm:^2.2.0" - tar-stream: "npm:^2.0.1" + strip-ansi: "npm:^6.0.0" + tar: "npm:^6.0.5" + tinylogic: "npm:^2.0.0" treeify: "npm:^1.1.0" - tslib: "npm:^1.13.0" - tunnel: "npm:^0.0.6" - checksum: 10/a963ddc09afdfd7dd40acf89bdffb0f6d134240fa2e9bd46dc1bcbe5501b96b7d65b6f30f36354386de097e0a43ea54f4dc4317ce75e2d5b3d6329e4ab0def67 - languageName: node - linkType: hard - -"@yarnpkg/fslib@npm:^2.4.0, @yarnpkg/fslib@npm:^2.5.0": - version: 2.10.4 - resolution: "@yarnpkg/fslib@npm:2.10.4" - dependencies: - "@yarnpkg/libzip": "npm:^2.3.0" - tslib: "npm:^1.13.0" - checksum: 10/c683b91a17138806f11db83af6e6aefd71f485570008effcd68cc39bf84e4243c0072c2a11d613c8289926bcd460e012b9476dd89e61018e21103bc3f42917ca + tslib: "npm:^2.4.0" + checksum: 10/438e0d0e73f22454696575436ee11f27e765df7fa6b6f9bf63e3d5f79f2b7ceb382dd7df1efbb27ca674a974a2c444a5bf346aac3875ffa6a555b26517309b71 languageName: node linkType: hard -"@yarnpkg/json-proxy@npm:^2.1.0": - version: 2.1.1 - resolution: "@yarnpkg/json-proxy@npm:2.1.1" +"@yarnpkg/fslib@npm:^3.1.2, @yarnpkg/fslib@npm:^3.1.3, @yarnpkg/fslib@npm:^3.1.4": + version: 3.1.4 + resolution: "@yarnpkg/fslib@npm:3.1.4" dependencies: - "@yarnpkg/fslib": "npm:^2.5.0" - tslib: "npm:^1.13.0" - checksum: 10/22f41ac5c3ee201132c6519da88252d5eea7eda96f554cabb1cdc4b7ff951f3b30f727b8abf457a91b2c8a4d2e7679101347e0beb606350cb4d524fea1159e60 + tslib: "npm:^2.4.0" + checksum: 10/9587a154768e61fbcf71ae745b1bf84e4ce0cbaa94163137ec88bd5005000a5b893dacbccbed7ac5f7643f957781c4f481d11ab3baf421ccafffbf43c275073d languageName: node linkType: hard -"@yarnpkg/libzip@npm:^2.2.1, @yarnpkg/libzip@npm:^2.3.0": - version: 2.3.0 - resolution: "@yarnpkg/libzip@npm:2.3.0" +"@yarnpkg/libzip@npm:^3.2.2": + version: 3.2.2 + resolution: "@yarnpkg/libzip@npm:3.2.2" dependencies: "@types/emscripten": "npm:^1.39.6" - tslib: "npm:^1.13.0" - checksum: 10/0eb147f39eab2830c29120d17e8bfba5aa15dedb940a7378070c67d4de08e9ba8d34068522e15e6b4db94ecaed4ad520e1e517588a36a348d1aa160bc36156ea + "@yarnpkg/fslib": "npm:^3.1.3" + tslib: "npm:^2.4.0" + peerDependencies: + "@yarnpkg/fslib": ^3.1.3 + checksum: 10/b6548be0a421e2390b74fd767d5f90e6da34a84af3ca28389b86d7f9e7602663f01347b5081cb93f8821877cae3ed48d2cb1cb8c35a4a4f7fc3d00109c85af0f languageName: node linkType: hard @@ -299,42 +263,31 @@ __metadata: languageName: node linkType: hard -"@yarnpkg/parsers@npm:^2.3.0": - version: 2.6.0 - resolution: "@yarnpkg/parsers@npm:2.6.0" +"@yarnpkg/parsers@npm:^3.0.3": + version: 3.0.3 + resolution: "@yarnpkg/parsers@npm:3.0.3" dependencies: js-yaml: "npm:^3.10.0" - tslib: "npm:^1.13.0" - checksum: 10/da2c22ce1271383af817b91286fd7532ca8d597a405005e777cb53e702bb7cf688b0b4637c3161351e4e76be43dba0694873cc7845cb9494b9060ddafc5bac3c + tslib: "npm:^2.4.0" + checksum: 10/379f7ff8fc1b37d3818dfeba4e18a72f8e9817bb41aab9332b50bbc843e45c9bf135563a7a06882ffb50e4cdd29c8da33c8e4f3739201de2fbcd38ecb59e3a8e languageName: node linkType: hard -"@yarnpkg/pnp@npm:^2.3.2": - version: 2.3.2 - resolution: "@yarnpkg/pnp@npm:2.3.2" +"@yarnpkg/shell@npm:^4.1.3": + version: 4.1.3 + resolution: "@yarnpkg/shell@npm:4.1.3" dependencies: - "@types/node": "npm:^13.7.0" - "@yarnpkg/fslib": "npm:^2.4.0" - tslib: "npm:^1.13.0" - checksum: 10/be736c950e888e115a50043e684326fb965ce3ba946dada4a7657faf7a2858afef6b5166a366f095b9498ced114325ae3e0341d9cea83a575938e8a8859e74ef - languageName: node - linkType: hard - -"@yarnpkg/shell@npm:^2.4.1": - version: 2.4.1 - resolution: "@yarnpkg/shell@npm:2.4.1" - dependencies: - "@yarnpkg/fslib": "npm:^2.4.0" - "@yarnpkg/parsers": "npm:^2.3.0" - clipanion: "npm:^2.6.2" - cross-spawn: "npm:7.0.3" + "@yarnpkg/fslib": "npm:^3.1.2" + "@yarnpkg/parsers": "npm:^3.0.3" + chalk: "npm:^4.1.2" + clipanion: "npm:^4.0.0-rc.2" + cross-spawn: "npm:^7.0.3" fast-glob: "npm:^3.2.2" micromatch: "npm:^4.0.2" - stream-buffers: "npm:^3.0.2" - tslib: "npm:^1.13.0" + tslib: "npm:^2.4.0" bin: shell: ./lib/cli.js - checksum: 10/ae7c07561ba4ec968b73385bbd4a9ed01a4f30b52f4fc1b46725dcbca29447e221e1c81ed1f94a6e1527397705e95f51489d3696be45a208a88c00d4c33b1da0 + checksum: 10/5994f92adf960071ac938653c5ad09746285d3fdc452fc6fdd30c3a832b612cc208e8d2274731e35957b457b168d6be524f5ce30ceb18542532d9326b422421b languageName: node linkType: hard @@ -387,13 +340,6 @@ __metadata: languageName: node linkType: hard -"any-promise@npm:^1.1.0, any-promise@npm:~1.3.0": - version: 1.3.0 - resolution: "any-promise@npm:1.3.0" - checksum: 10/6737469ba353b5becf29e4dc3680736b9caa06d300bda6548812a8fee63ae7d336d756f88572fa6b5219aed36698d808fa55f62af3e7e6845c7a1dc77d240edb - languageName: node - linkType: hard - "argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -410,20 +356,6 @@ __metadata: languageName: node linkType: hard -"array-union@npm:^2.1.0": - version: 2.1.0 - resolution: "array-union@npm:2.1.0" - checksum: 10/5bee12395cba82da674931df6d0fea23c4aa4660cb3b338ced9f828782a65caa232573e6bf3968f23e0c5eb301764a382cef2f128b170a9dc59de0e36c39f98d - languageName: node - linkType: hard - -"asap@npm:~2.0.6": - version: 2.0.6 - resolution: "asap@npm:2.0.6" - checksum: 10/b244c0458c571945e4b3be0b14eb001bea5596f9868cc50cc711dc03d58a7e953517d3f0dad81ccde3ff37d1f074701fa76a6f07d41aaa992d7204a37b915dda - languageName: node - linkType: hard - "async@npm:^3.2.2": version: 3.2.6 resolution: "async@npm:3.2.6" @@ -438,34 +370,25 @@ __metadata: concurrently: "npm:^9.2.1" husky: "npm:^9.1.7" lint-staged: "npm:^15.5.2" - rimraf: "npm:^6.1.2" - semver: "npm:^7.7.3" - snyk-nodejs-lockfile-parser: "npm:^1.60.1" + rimraf: "npm:^6.1.3" + semver: "npm:^7.7.4" + snyk-nodejs-lockfile-parser: "npm:^2.6.0" languageName: unknown linkType: soft -"base64-js@npm:^1.3.1": - version: 1.5.1 - resolution: "base64-js@npm:1.5.1" - checksum: 10/669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 - languageName: node - linkType: hard - -"binjumper@npm:^0.1.4": - version: 0.1.4 - resolution: "binjumper@npm:0.1.4" - checksum: 10/9ae6de33ca27b9cc40425227d3d6560ce63f8977855fed70788dc0492f9a048895d79617d8d8152b7b8f66f93d935f25a4bca94cc74d477c3c7cba2c15662dea +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 languageName: node linkType: hard -"bl@npm:^4.0.3": - version: 4.1.0 - resolution: "bl@npm:4.1.0" +"brace-expansion@npm:^5.0.2": + version: 5.0.4 + resolution: "brace-expansion@npm:5.0.4" dependencies: - buffer: "npm:^5.5.0" - inherits: "npm:^2.0.4" - readable-stream: "npm:^3.4.0" - checksum: 10/b7904e66ed0bdfc813c06ea6c3e35eafecb104369dbf5356d0f416af90c1546de3b74e5b63506f0629acf5e16a6f87c3798f16233dcff086e9129383aa02ab55 + balanced-match: "npm:^4.0.2" + checksum: 10/cfd57e20d8ded9578149e47ae4d3fff2b2f78d06b54a32a73057bddff65c8e9b930613f0cbcfefedf12dd117151e19d4da16367d5127c54f3bff02d8a4479bb2 languageName: node linkType: hard @@ -478,16 +401,6 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.5.0": - version: 5.7.1 - resolution: "buffer@npm:5.7.1" - dependencies: - base64-js: "npm:^1.3.1" - ieee754: "npm:^1.1.13" - checksum: 10/997434d3c6e3b39e0be479a80288875f71cd1c07d75a3855e6f08ef848a3c966023f79534e22e415ff3a5112708ce06127277ab20e527146d55c84566405c7c6 - languageName: node - linkType: hard - "cacheable-lookup@npm:^5.0.3": version: 5.0.4 resolution: "cacheable-lookup@npm:5.0.4" @@ -510,19 +423,6 @@ __metadata: languageName: node linkType: hard -"call-bind@npm:^1.0.5": - version: 1.0.7 - resolution: "call-bind@npm:1.0.7" - dependencies: - es-define-property: "npm:^1.0.0" - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - get-intrinsic: "npm:^1.2.4" - set-function-length: "npm:^1.2.1" - checksum: 10/cd6fe658e007af80985da5185bff7b55e12ef4c2b6f41829a26ed1eef254b1f1c12e3dfd5b2b068c6ba8b86aba62390842d81752e67dcbaec4f6f76e7113b6b7 - languageName: node - linkType: hard - "camelcase@npm:^5.3.1": version: 5.3.1 resolution: "camelcase@npm:5.3.1" @@ -530,7 +430,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:4.1.2": +"chalk@npm:4.1.2, chalk@npm:^4.1.2": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -540,16 +440,6 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^3.0.0": - version: 3.0.0 - resolution: "chalk@npm:3.0.0" - dependencies: - ansi-styles: "npm:^4.1.0" - supports-color: "npm:^7.1.0" - checksum: 10/37f90b31fd655fb49c2bd8e2a68aebefddd64522655d001ef417e6f955def0ed9110a867ffc878a533f2dafea5f2032433a37c8a7614969baa7f8a1cd424ddfc - languageName: node - linkType: hard - "chalk@npm:^5.4.1": version: 5.4.1 resolution: "chalk@npm:5.4.1" @@ -557,10 +447,17 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^2.0.0": +"chownr@npm:^2.0.0": version: 2.0.0 - resolution: "ci-info@npm:2.0.0" - checksum: 10/3b374666a85ea3ca43fa49aa3a048d21c9b475c96eb13c133505d2324e7ae5efd6a454f41efe46a152269e9b6a00c9edbe63ec7fa1921957165aae16625acd67 + resolution: "chownr@npm:2.0.0" + checksum: 10/c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f + languageName: node + linkType: hard + +"ci-info@npm:^4.0.0": + version: 4.4.0 + resolution: "ci-info@npm:4.4.0" + checksum: 10/dfded0c630267d89660c8abb988ac8395a382bdfefedcc03e3e2858523312c5207db777c239c34774e3fcff11f015477c19d2ac8a58ea58aa476614a2e64f434 languageName: node linkType: hard @@ -590,10 +487,14 @@ __metadata: languageName: node linkType: hard -"clipanion@npm:^2.6.2": - version: 2.6.2 - resolution: "clipanion@npm:2.6.2" - checksum: 10/f87ca32dd41a7e7898e72f425590c267818c81717c33ea52270354a3f9232a4c4d4f38a5acc0c4b52cb9f9b67962dcf3d326cd57ec2cc3d4345292f0b84e025b +"clipanion@npm:^4.0.0-rc.2": + version: 4.0.0-rc.4 + resolution: "clipanion@npm:4.0.0-rc.4" + dependencies: + typanion: "npm:^3.8.0" + peerDependencies: + typanion: "*" + checksum: 10/c3a94783318d91e6b35380a8aa4a6f166964082a51ff2df21a339266223aaab98f5986dd2c37ca7fd640ad1d233b3cd5b24aad64c51537b54ccc9c66ec070eeb languageName: node linkType: hard @@ -664,14 +565,14 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:7.0.3, cross-spawn@npm:^7.0.3": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" +"cross-spawn@npm:^7.0.3": + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" dependencies: path-key: "npm:^3.1.0" shebang-command: "npm:^2.0.0" which: "npm:^2.0.1" - checksum: 10/e1a13869d2f57d974de0d9ef7acbf69dc6937db20b918525a01dacb5032129bd552d290d886d981e99f1b624cb03657084cc87bd40f115c07ecf376821c729ce + checksum: 10/0d52657d7ae36eb130999dffff1168ec348687b48dd38e2ff59992ed916c88d328cf1d07ff4a4a10bc78de5e1c23f04b306d569e42f7a2293915c081e4dfee86 languageName: node linkType: hard @@ -703,28 +604,6 @@ __metadata: languageName: node linkType: hard -"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": - version: 1.1.4 - resolution: "define-data-property@npm:1.1.4" - dependencies: - es-define-property: "npm:^1.0.0" - es-errors: "npm:^1.3.0" - gopd: "npm:^1.0.1" - checksum: 10/abdcb2505d80a53524ba871273e5da75e77e52af9e15b3aa65d8aad82b8a3a424dad7aee2cc0b71470ac7acf501e08defac362e8b6a73cdb4309f028061df4ae - languageName: node - linkType: hard - -"define-properties@npm:^1.2.1": - version: 1.2.1 - resolution: "define-properties@npm:1.2.1" - dependencies: - define-data-property: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.0" - object-keys: "npm:^1.1.1" - checksum: 10/b4ccd00597dd46cb2d4a379398f5b19fca84a16f3374e2249201992f36b30f6835949a9429669ee6b41b6e837205a163eadd745e472069e70dfc10f03e5fcc12 - languageName: node - linkType: hard - "dependency-path@npm:^9.2.8": version: 9.2.8 resolution: "dependency-path@npm:9.2.8" @@ -737,19 +616,17 @@ __metadata: languageName: node linkType: hard -"diff@npm:^4.0.1": - version: 4.0.4 - resolution: "diff@npm:4.0.4" - checksum: 10/5019b3f5ae124ea9e95137119e1a83a59c252c75ddac873cc967832fd7a834570a58a4d58b941bdbd07832ebf98dcb232b27c561b7f5584357da6dae59bcac62 +"diff@npm:^5.1.0": + version: 5.2.2 + resolution: "diff@npm:5.2.2" + checksum: 10/8a885b38113d96138d87f6cb474ee959b7e9ab33c0c4cb1b07dcf019ec544945a2309d53d721532af020de4b3a58fb89f1026f64f42f9421aa9c3ae48a36998b languageName: node linkType: hard -"dir-glob@npm:^3.0.1": - version: 3.0.1 - resolution: "dir-glob@npm:3.0.1" - dependencies: - path-type: "npm:^4.0.0" - checksum: 10/fa05e18324510d7283f55862f3161c6759a3f2f8dbce491a2fc14c8324c498286c54282c1f0e933cb930da8419b30679389499b919122952a4f8592362ef4615 +"dotenv@npm:^16.3.1": + version: 16.6.1 + resolution: "dotenv@npm:16.6.1" + checksum: 10/1d1897144344447ffe62aa1a6d664f4cd2e0784e0aff787eeeec1940ded32f8e4b5b506d665134fc87157baa086fce07ec6383970a2b6d2e7985beaed6a4cc14 languageName: node linkType: hard @@ -776,7 +653,7 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": +"end-of-stream@npm:^1.1.0": version: 1.4.4 resolution: "end-of-stream@npm:1.4.4" dependencies: @@ -785,15 +662,6 @@ __metadata: languageName: node linkType: hard -"end-of-stream@npm:~1.1.0": - version: 1.1.0 - resolution: "end-of-stream@npm:1.1.0" - dependencies: - once: "npm:~1.3.0" - checksum: 10/9fa637e259e50e5e3634e8e14064a183bd0d407733594631362f9df596409739bef5f7064840e6725212a9edc8b4a70a5a3088ac423e8564f9dc183dd098c719 - languageName: node - linkType: hard - "environment@npm:^1.0.0": version: 1.1.0 resolution: "environment@npm:1.1.0" @@ -801,19 +669,15 @@ __metadata: languageName: node linkType: hard -"es-define-property@npm:^1.0.0": - version: 1.0.0 - resolution: "es-define-property@npm:1.0.0" - dependencies: - get-intrinsic: "npm:^1.2.4" - checksum: 10/f66ece0a887b6dca71848fa71f70461357c0e4e7249696f81bad0a1f347eed7b31262af4a29f5d726dc026426f085483b6b90301855e647aa8e21936f07293c6 - languageName: node - linkType: hard - -"es-errors@npm:^1.3.0": - version: 1.3.0 - resolution: "es-errors@npm:1.3.0" - checksum: 10/96e65d640156f91b707517e8cdc454dd7d47c32833aa3e85d79f24f9eb7ea85f39b63e36216ef0114996581969b59fe609a94e30316b08f5f4df1d44134cf8d5 +"es-toolkit@npm:^1.39.7": + version: 1.44.0 + resolution: "es-toolkit@npm:1.44.0" + dependenciesMeta: + "@trivago/prettier-plugin-sort-imports@4.3.0": + unplugged: true + prettier-plugin-sort-re-exports@0.0.1: + unplugged: true + checksum: 10/72a74c8688dec99743b3170e1e78822b983519b795643c2f04c4a8e6b49e2d6f554013e2266850de7929718b4113768912e9b8394eb6e3d6c9e28a38fb8f5317 languageName: node linkType: hard @@ -867,7 +731,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9": +"fast-glob@npm:^3.2.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -898,17 +762,12 @@ __metadata: languageName: node linkType: hard -"fs-constants@npm:^1.0.0": - version: 1.0.0 - resolution: "fs-constants@npm:1.0.0" - checksum: 10/18f5b718371816155849475ac36c7d0b24d39a11d91348cfcb308b4494824413e03572c403c86d3a260e049465518c4f0d5bd00f0371cdfcad6d4f30a85b350d - languageName: node - linkType: hard - -"function-bind@npm:^1.1.2": - version: 1.1.2 - resolution: "function-bind@npm:1.1.2" - checksum: 10/185e20d20f10c8d661d59aac0f3b63b31132d492e1b11fcc2a93cb2c47257ebaee7407c38513efd2b35cafdf972d9beb2ea4593c1e0f3bf8f2744836928d7454 +"fs-minipass@npm:^2.0.0": + version: 2.1.0 + resolution: "fs-minipass@npm:2.1.0" + dependencies: + minipass: "npm:^3.0.0" + checksum: 10/03191781e94bc9a54bd376d3146f90fe8e082627c502185dbf7b9b3032f66b0b142c1115f3b2cc5936575fc1b44845ce903dd4c21bec2a8d69f3bd56f9cee9ec languageName: node linkType: hard @@ -926,19 +785,6 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.4": - version: 1.2.4 - resolution: "get-intrinsic@npm:1.2.4" - dependencies: - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - has-proto: "npm:^1.0.1" - has-symbols: "npm:^1.0.3" - hasown: "npm:^2.0.0" - checksum: 10/85bbf4b234c3940edf8a41f4ecbd4e25ce78e5e6ad4e24ca2f77037d983b9ef943fd72f00f3ee97a49ec622a506b67db49c36246150377efcda1c9eb03e5f06d - languageName: node - linkType: hard - "get-stream@npm:^5.1.0": version: 5.2.0 resolution: "get-stream@npm:5.2.0" @@ -964,37 +810,14 @@ __metadata: languageName: node linkType: hard -"glob@npm:^13.0.0": - version: 13.0.0 - resolution: "glob@npm:13.0.0" +"glob@npm:^13.0.3": + version: 13.0.6 + resolution: "glob@npm:13.0.6" dependencies: - minimatch: "npm:^10.1.1" - minipass: "npm:^7.1.2" - path-scurry: "npm:^2.0.0" - checksum: 10/de390721d29ee1c9ea41e40ec2aa0de2cabafa68022e237dc4297665a5e4d650776f2573191984ea1640aba1bf0ea34eddef2d8cbfbfc2ad24b5fb0af41d8846 - languageName: node - linkType: hard - -"globby@npm:^11.0.1": - version: 11.1.0 - resolution: "globby@npm:11.1.0" - dependencies: - array-union: "npm:^2.1.0" - dir-glob: "npm:^3.0.1" - fast-glob: "npm:^3.2.9" - ignore: "npm:^5.2.0" - merge2: "npm:^1.4.1" - slash: "npm:^3.0.0" - checksum: 10/288e95e310227bbe037076ea81b7c2598ccbc3122d87abc6dab39e1eec309aa14f0e366a98cdc45237ffcfcbad3db597778c0068217dcb1950fef6249104e1b1 - languageName: node - linkType: hard - -"gopd@npm:^1.0.1": - version: 1.0.1 - resolution: "gopd@npm:1.0.1" - dependencies: - get-intrinsic: "npm:^1.1.3" - checksum: 10/5fbc7ad57b368ae4cd2f41214bd947b045c1a4be2f194a7be1778d71f8af9dbf4004221f3b6f23e30820eb0d052b4f819fe6ebe8221e2a3c6f0ee4ef173421ca + minimatch: "npm:^10.2.2" + minipass: "npm:^7.1.3" + path-scurry: "npm:^2.0.2" + checksum: 10/201ad69e5f0aa74e1d8c00a481581f8b8c804b6a4fbfabeeb8541f5d756932800331daeba99b58fb9e4cd67e12ba5a7eba5b82fb476691588418060b84353214 languageName: node linkType: hard @@ -1031,35 +854,10 @@ __metadata: languageName: node linkType: hard -"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": - version: 1.0.2 - resolution: "has-property-descriptors@npm:1.0.2" - dependencies: - es-define-property: "npm:^1.0.0" - checksum: 10/2d8c9ab8cebb572e3362f7d06139a4592105983d4317e68f7adba320fe6ddfc8874581e0971e899e633fd5f72e262830edce36d5a0bc863dad17ad20572484b2 - languageName: node - linkType: hard - -"has-proto@npm:^1.0.1": - version: 1.0.3 - resolution: "has-proto@npm:1.0.3" - checksum: 10/0b67c2c94e3bea37db3e412e3c41f79d59259875e636ba471e94c009cdfb1fa82bf045deeffafc7dbb9c148e36cae6b467055aaa5d9fad4316e11b41e3ba551a - languageName: node - linkType: hard - -"has-symbols@npm:^1.0.3": - version: 1.0.3 - resolution: "has-symbols@npm:1.0.3" - checksum: 10/464f97a8202a7690dadd026e6d73b1ceeddd60fe6acfd06151106f050303eaa75855aaa94969df8015c11ff7c505f196114d22f7386b4a471038da5874cf5e9b - languageName: node - linkType: hard - -"hasown@npm:^2.0.0": - version: 2.0.2 - resolution: "hasown@npm:2.0.2" - dependencies: - function-bind: "npm:^1.1.2" - checksum: 10/7898a9c1788b2862cf0f9c345a6bec77ba4a0c0983c7f19d610c382343d4f98fa260686b225dfb1f88393a66679d2ec58ee310c1d6868c081eda7918f32cc70a +"hpagent@npm:^1.2.0": + version: 1.2.0 + resolution: "hpagent@npm:1.2.0" + checksum: 10/bad186449da7e3456788a8cbae459fc6c0a855d5872a7f460c48ce4a613020d8d914839dad10047297099299c4f9e6c65a0eec3f5886af196c0a516e4ad8a845 languageName: node linkType: hard @@ -1096,20 +894,6 @@ __metadata: languageName: node linkType: hard -"ieee754@npm:^1.1.13": - version: 1.2.1 - resolution: "ieee754@npm:1.2.1" - checksum: 10/d9f2557a59036f16c282aaeb107832dc957a93d73397d89bbad4eb1130560560eb695060145e8e6b3b498b15ab95510226649a0b8f52ae06583575419fe10fc4 - languageName: node - linkType: hard - -"ignore@npm:^5.2.0": - version: 5.3.2 - resolution: "ignore@npm:5.3.2" - checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 - languageName: node - linkType: hard - "indent-string@npm:^4.0.0": version: 4.0.0 resolution: "indent-string@npm:4.0.0" @@ -1117,20 +901,6 @@ __metadata: languageName: node linkType: hard -"inherits@npm:^2.0.3, inherits@npm:^2.0.4": - version: 2.0.4 - resolution: "inherits@npm:2.0.4" - checksum: 10/cd45e923bee15186c07fa4c89db0aace24824c482fb887b528304694b2aa6ff8a898da8657046a5dcf3e46cd6db6c61629551f9215f208d7c3f157cf9b290521 - languageName: node - linkType: hard - -"is-callable@npm:^1.1.5": - version: 1.2.7 - resolution: "is-callable@npm:1.2.7" - checksum: 10/48a9297fb92c99e9df48706241a189da362bff3003354aea4048bd5f7b2eb0d823cd16d0a383cece3d76166ba16d85d9659165ac6fcce1ac12e6c649d66dbdb9 - languageName: node - linkType: hard - "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -1184,13 +954,6 @@ __metadata: languageName: node linkType: hard -"is@npm:^3.2.1, is@npm:^3.3.0": - version: 3.3.0 - resolution: "is@npm:3.3.0" - checksum: 10/f77dc5a05a1e8fd1f1de282add9bb01c44dae27af72b883bf0ce342151dec48f125b0b8923efa78c1e93c4fb866095629b2c7de3e5e3853aea4ed17c82c5cd8d - languageName: node - linkType: hard - "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" @@ -1228,19 +991,6 @@ __metadata: languageName: node linkType: hard -"json-file-plus@npm:^3.3.1": - version: 3.3.1 - resolution: "json-file-plus@npm:3.3.1" - dependencies: - is: "npm:^3.2.1" - node.extend: "npm:^2.0.0" - object.assign: "npm:^4.1.0" - promiseback: "npm:^2.0.2" - safer-buffer: "npm:^2.0.2" - checksum: 10/6b71dad39e0fd8d0a23a82ca70b7c94adfcd59986e63165935d2adba5502076b75f3267e357372dd118f9d680ecc142f0f67617de9f27139c3c8b113cdd9c574 - languageName: node - linkType: hard - "keyv@npm:^4.0.0": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -1431,13 +1181,6 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.15": - version: 4.17.23 - resolution: "lodash@npm:4.17.23" - checksum: 10/82504c88250f58da7a5a4289f57a4f759c44946c005dd232821c7688b5fcfbf4a6268f6a6cdde4b792c91edd2f3b5398c1d2a0998274432cff76def48735e233 - languageName: node - linkType: hard - "log-update@npm:^6.1.0": version: 6.1.0 resolution: "log-update@npm:6.1.0" @@ -1491,7 +1234,7 @@ __metadata: languageName: node linkType: hard -"merge2@npm:^1.3.0, merge2@npm:^1.4.1": +"merge2@npm:^1.3.0": version: 1.4.1 resolution: "merge2@npm:1.4.1" checksum: 10/7268db63ed5169466540b6fb947aec313200bcf6d40c5ab722c22e242f651994619bcd85601602972d3c85bd2cc45a358a4c61937e9f11a061919a1da569b0c2 @@ -1543,12 +1286,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.1.1": - version: 10.1.1 - resolution: "minimatch@npm:10.1.1" +"minimatch@npm:^10.2.2": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" dependencies: - "@isaacs/brace-expansion": "npm:^5.0.0" - checksum: 10/110f38921ea527022e90f7a5f43721838ac740d0a0c26881c03b57c261354fb9a0430e40b2c56dfcea2ef3c773768f27210d1106f1f2be19cde3eea93f26f45e + brace-expansion: "npm:^5.0.2" + checksum: 10/aea4874e521c55bb60744685bbffe3d152e5460f84efac3ea936e6bbe2ceba7deb93345fec3f9bb17f7b6946776073a64d40ae32bf5f298ad690308121068a1f languageName: node linkType: hard @@ -1559,21 +1302,45 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^7.1.2": - version: 7.1.2 - resolution: "minipass@npm:7.1.2" - checksum: 10/c25f0ee8196d8e6036661104bacd743785b2599a21de5c516b32b3fa2b83113ac89a2358465bc04956baab37ffb956ae43be679b2262bf7be15fce467ccd7950 +"minipass@npm:^3.0.0": + version: 3.3.6 + resolution: "minipass@npm:3.3.6" + dependencies: + yallist: "npm:^4.0.0" + checksum: 10/a5c6ef069f70d9a524d3428af39f2b117ff8cd84172e19b754e7264a33df460873e6eb3d6e55758531580970de50ae950c496256bb4ad3691a2974cddff189f0 + languageName: node + linkType: hard + +"minipass@npm:^5.0.0": + version: 5.0.0 + resolution: "minipass@npm:5.0.0" + checksum: 10/61682162d29f45d3152b78b08bab7fb32ca10899bc5991ffe98afc18c9e9543bd1e3be94f8b8373ba6262497db63607079dc242ea62e43e7b2270837b7347c93 languageName: node linkType: hard -"mkdirp@npm:^0.5.1": - version: 0.5.6 - resolution: "mkdirp@npm:0.5.6" +"minipass@npm:^7.1.2, minipass@npm:^7.1.3": + version: 7.1.3 + resolution: "minipass@npm:7.1.3" + checksum: 10/175e4d5e20980c3cd316ae82d2c031c42f6c746467d8b1905b51060a0ba4461441a0c25bb67c025fd9617f9a3873e152c7b543c6b5ac83a1846be8ade80dffd6 + languageName: node + linkType: hard + +"minizlib@npm:^2.1.1": + version: 2.1.2 + resolution: "minizlib@npm:2.1.2" dependencies: - minimist: "npm:^1.2.6" + minipass: "npm:^3.0.0" + yallist: "npm:^4.0.0" + checksum: 10/ae0f45436fb51344dcb87938446a32fbebb540d0e191d63b35e1c773d47512e17307bf54aa88326cc6d176594d00e4423563a091f7266c2f9a6872cdc1e234d1 + languageName: node + linkType: hard + +"mkdirp@npm:^1.0.3": + version: 1.0.4 + resolution: "mkdirp@npm:1.0.4" bin: mkdirp: bin/cmd.js - checksum: 10/0c91b721bb12c3f9af4b77ebf73604baf350e64d80df91754dc509491ae93bf238581e59c7188360cec7cb62fc4100959245a42cfe01834efedc5e9d068376c2 + checksum: 10/d71b8dcd4b5af2fe13ecf3bd24070263489404fe216488c5ba7e38ece1f54daf219e72a833a3a2dc404331e870e9f44963a33399589490956bff003a3404d3b2 languageName: node linkType: hard @@ -1584,16 +1351,6 @@ __metadata: languageName: node linkType: hard -"node.extend@npm:^2.0.0": - version: 2.0.3 - resolution: "node.extend@npm:2.0.3" - dependencies: - hasown: "npm:^2.0.0" - is: "npm:^3.3.0" - checksum: 10/f500ace16d0b90e9db3919676de593eb37e7b82d8d9b67d95a40e5856ef5842592df3364b4d01fc2c3f4c0dea6dd9d627444dd85fe18581b7a22caad5ffab249 - languageName: node - linkType: hard - "normalize-url@npm:^6.0.1": version: 6.1.0 resolution: "normalize-url@npm:6.1.0" @@ -1617,25 +1374,6 @@ __metadata: languageName: node linkType: hard -"object-keys@npm:^1.1.1": - version: 1.1.1 - resolution: "object-keys@npm:1.1.1" - checksum: 10/3d81d02674115973df0b7117628ea4110d56042e5326413e4b4313f0bcdf7dd78d4a3acef2c831463fa3796a66762c49daef306f4a0ea1af44877d7086d73bde - languageName: node - linkType: hard - -"object.assign@npm:^4.1.0": - version: 4.1.5 - resolution: "object.assign@npm:4.1.5" - dependencies: - call-bind: "npm:^1.0.5" - define-properties: "npm:^1.2.1" - has-symbols: "npm:^1.0.3" - object-keys: "npm:^1.1.1" - checksum: 10/dbb22da4cda82e1658349ea62b80815f587b47131b3dd7a4ab7f84190ab31d206bbd8fe7e26ae3220c55b65725ac4529825f6142154211220302aa6b1518045d - languageName: node - linkType: hard - "once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" @@ -1645,15 +1383,6 @@ __metadata: languageName: node linkType: hard -"once@npm:~1.3.0": - version: 1.3.3 - resolution: "once@npm:1.3.3" - dependencies: - wrappy: "npm:1" - checksum: 10/8e832de08b1d73b470e01690c211cb4fcefccab1fd1bd19e706d572d74d3e9b7e38a8bfcdabdd364f9f868757d9e8e5812a59817dc473eaf698ff3bfae2219f2 - languageName: node - linkType: hard - "onetime@npm:^6.0.0": version: 6.0.0 resolution: "onetime@npm:6.0.0" @@ -1718,10 +1447,10 @@ __metadata: languageName: node linkType: hard -"packageurl-js@npm:1.2.0": - version: 1.2.0 - resolution: "packageurl-js@npm:1.2.0" - checksum: 10/b780ad6cf9f75055effafe8fbed37617eb1924e3dc5b055fb3ecceaaaa93da73ea1508a3874b04bd13342a77bd852b70a4e52596c171cbc57840c4b8452d2d56 +"packageurl-js@npm:2.0.1": + version: 2.0.1 + resolution: "packageurl-js@npm:2.0.1" + checksum: 10/5fdb2b89e5c39dbb87806ef303bc558da0f8c178b8afb2647979d49212039f2cccc6c0135816819d061c6b12b47e8c6bb8c34a2b9fdd8684b9fb975dcf3bc73b languageName: node linkType: hard @@ -1739,20 +1468,13 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^2.0.0": - version: 2.0.0 - resolution: "path-scurry@npm:2.0.0" +"path-scurry@npm:^2.0.2": + version: 2.0.2 + resolution: "path-scurry@npm:2.0.2" dependencies: lru-cache: "npm:^11.0.0" minipass: "npm:^7.1.2" - checksum: 10/285ae0c2d6c34ae91dc1d5378ede21981c9a2f6de1ea9ca5a88b5a270ce9763b83dbadc7a324d512211d8d36b0c540427d3d0817030849d97a60fa840a2c59ec - languageName: node - linkType: hard - -"path-type@npm:^4.0.0": - version: 4.0.0 - resolution: "path-type@npm:4.0.0" - checksum: 10/5b1e2daa247062061325b8fdbfd1fb56dde0a448fb1455453276ea18c60685bdad23a445dc148cf87bc216be1573357509b7d4060494a6fd768c7efad833ee45 + checksum: 10/2b4257422bcb870a4c2d205b3acdbb213a72f5e2250f61c80f79c9d014d010f82bdf8584441612c8e1fa4eb098678f5704a66fa8377d72646bad4be38e57a2c3 languageName: node linkType: hard @@ -1772,48 +1494,6 @@ __metadata: languageName: node linkType: hard -"pluralize@npm:^7.0.0": - version: 7.0.0 - resolution: "pluralize@npm:7.0.0" - checksum: 10/905274e679d3802650fdfdd977434757d4680082da7a23c0938a608d1d5c8246790b62dc15ff1f737b0d57baa6ad2f6ebb0857b1950435a583e32af76ee58e1f - languageName: node - linkType: hard - -"pretty-bytes@npm:^5.1.0": - version: 5.6.0 - resolution: "pretty-bytes@npm:5.6.0" - checksum: 10/9c082500d1e93434b5b291bd651662936b8bd6204ec9fa17d563116a192d6d86b98f6d328526b4e8d783c07d5499e2614a807520249692da9ec81564b2f439cd - languageName: node - linkType: hard - -"promise-deferred@npm:^2.0.3": - version: 2.0.4 - resolution: "promise-deferred@npm:2.0.4" - dependencies: - promise: "npm:^8.3.0" - checksum: 10/1d0e306d54a7436e288836c0784abdf11798011a6c3309f4ce8e24564ba958c41ca0d21bb7ec95386f04ac8f9691fdd8e3dd0af5176b496a2303d00db96acf5a - languageName: node - linkType: hard - -"promise@npm:^8.3.0": - version: 8.3.0 - resolution: "promise@npm:8.3.0" - dependencies: - asap: "npm:~2.0.6" - checksum: 10/55e9d0d723c66810966bc055c6c77a3658c0af7e4a8cc88ea47aeaf2949ca0bd1de327d9c631df61236f5406ad478384fa19a77afb3f88c0303eba9e5eb0a8d8 - languageName: node - linkType: hard - -"promiseback@npm:^2.0.2": - version: 2.0.3 - resolution: "promiseback@npm:2.0.3" - dependencies: - is-callable: "npm:^1.1.5" - promise-deferred: "npm:^2.0.3" - checksum: 10/39716e64ac75b3a5c58532493f594d4788267ee13e2aeee5c60b448eb17e8f98c8ff4778c5497aed1594e29c428710ae21c83671c87c24b3d2c42f0c359d6e55 - languageName: node - linkType: hard - "pump@npm:^3.0.0": version: 3.0.0 resolution: "pump@npm:3.0.0" @@ -1838,17 +1518,6 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0": - version: 3.6.2 - resolution: "readable-stream@npm:3.6.2" - dependencies: - inherits: "npm:^2.0.3" - string_decoder: "npm:^1.1.1" - util-deprecate: "npm:^1.0.1" - checksum: 10/d9e3e53193adcdb79d8f10f2a1f6989bd4389f5936c6f8b870e77570853561c362bee69feca2bbb7b32368ce96a85504aa4cedf7cf80f36e6a9de30d64244048 - languageName: node - linkType: hard - "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -1903,15 +1572,15 @@ __metadata: languageName: node linkType: hard -"rimraf@npm:^6.1.2": - version: 6.1.2 - resolution: "rimraf@npm:6.1.2" +"rimraf@npm:^6.1.3": + version: 6.1.3 + resolution: "rimraf@npm:6.1.3" dependencies: - glob: "npm:^13.0.0" + glob: "npm:^13.0.3" package-json-from-dist: "npm:^1.0.1" bin: rimraf: dist/esm/bin.mjs - checksum: 10/add8e566fe903f59d7b55c6c2382320c48302778640d1951baf247b3b451af496c2dee7195c204a8c646fd6327feadd1f5b61ce68c1362d4898075a726d83cc6 + checksum: 10/dd98ec2ad7cd2cccae1c7110754d472eac8edb2bab8a8b057dce04edfe1433dab246a889b3fd85a66c78ca81caa1429caa0e736c7647f6832b04fd5d4dfb8ab8 languageName: node linkType: hard @@ -1933,49 +1602,12 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:~5.2.0": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: 10/32872cd0ff68a3ddade7a7617b8f4c2ae8764d8b7d884c651b74457967a9e0e886267d3ecc781220629c44a865167b61c375d2da6c720c840ecd73f45d5d9451 - languageName: node - linkType: hard - -"safer-buffer@npm:^2.0.2": - version: 2.1.2 - resolution: "safer-buffer@npm:2.1.2" - checksum: 10/7eaf7a0cf37cc27b42fb3ef6a9b1df6e93a1c6d98c6c6702b02fe262d5fcbd89db63320793b99b21cb5348097d0a53de81bd5f4e8b86e20cc9412e3f1cfb4e83 - languageName: node - linkType: hard - -"semver@npm:^7.0.0, semver@npm:^7.1.2, semver@npm:^7.3.8, semver@npm:^7.6.0": - version: 7.6.3 - resolution: "semver@npm:7.6.3" +"semver@npm:^7.0.0, semver@npm:^7.1.2, semver@npm:^7.3.8, semver@npm:^7.6.0, semver@npm:^7.7.4": + version: 7.7.4 + resolution: "semver@npm:7.7.4" bin: semver: bin/semver.js - checksum: 10/36b1fbe1a2b6f873559cd57b238f1094a053dbfd997ceeb8757d79d1d2089c56d1321b9f1069ce263dc64cfa922fa1d2ad566b39426fe1ac6c723c1487589e10 - languageName: node - linkType: hard - -"semver@npm:^7.7.3": - version: 7.7.3 - resolution: "semver@npm:7.7.3" - bin: - semver: bin/semver.js - checksum: 10/8dbc3168e057a38fc322af909c7f5617483c50caddba135439ff09a754b20bdd6482a5123ff543dad4affa488ecf46ec5fb56d61312ad20bb140199b88dfaea9 - languageName: node - linkType: hard - -"set-function-length@npm:^1.2.1": - version: 1.2.2 - resolution: "set-function-length@npm:1.2.2" - dependencies: - define-data-property: "npm:^1.1.4" - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - get-intrinsic: "npm:^1.2.4" - gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.2" - checksum: 10/505d62b8e088468917ca4e3f8f39d0e29f9a563b97dbebf92f4bd2c3172ccfb3c5b8e4566d5fcd00784a00433900e7cb8fbc404e2dbd8c3818ba05bb9d4a8a6d + checksum: 10/26bdc6d58b29528f4142d29afb8526bc335f4fc04c4a10f2b98b217f277a031c66736bf82d3d3bb354a2f6a3ae50f18fd62b053c4ac3f294a3d10a61f5075b75 languageName: node linkType: hard @@ -2009,13 +1641,6 @@ __metadata: languageName: node linkType: hard -"slash@npm:^3.0.0": - version: 3.0.0 - resolution: "slash@npm:3.0.0" - checksum: 10/94a93fff615f25a999ad4b83c9d5e257a7280c90a32a7cb8b4a87996e4babf322e469c42b7f649fd5796edd8687652f3fb452a86dc97a816f01113183393f11c - languageName: node - linkType: hard - "slice-ansi@npm:^5.0.0": version: 5.0.0 resolution: "slice-ansi@npm:5.0.0" @@ -2048,14 +1673,14 @@ __metadata: languageName: node linkType: hard -"snyk-nodejs-lockfile-parser@npm:^1.60.1": - version: 1.60.1 - resolution: "snyk-nodejs-lockfile-parser@npm:1.60.1" +"snyk-nodejs-lockfile-parser@npm:^2.6.0": + version: 2.6.0 + resolution: "snyk-nodejs-lockfile-parser@npm:2.6.0" dependencies: - "@snyk/dep-graph": "npm:^2.3.0" + "@snyk/dep-graph": "npm:^2.12.0" "@snyk/error-catalog-nodejs-public": "npm:^5.16.0" "@snyk/graphlib": "npm:2.1.9-patch.3" - "@yarnpkg/core": "npm:^2.4.0" + "@yarnpkg/core": "npm:^4.4.1" "@yarnpkg/lockfile": "npm:^1.1.0" dependency-path: "npm:^9.2.8" event-loop-spinner: "npm:^2.0.0" @@ -2072,7 +1697,7 @@ __metadata: uuid: "npm:^8.3.0" bin: parse-nodejs-lockfile: bin/index.js - checksum: 10/2186abf1a7930ff12c5a3929c514300a0ecb47f576f64b84b265292c106aba9ec13a98cdae0b2f01449e53311d361a67cbb13ed631e718d28faaafa97971adc5 + checksum: 10/26541be907bea65136c3bb8513b82d3569b1a0d0dc0c9b4a0f9c20a7d923467a0a26a638cfa7a07e11ebf910e259876d448a2104802130d661f8a2b38867664c languageName: node linkType: hard @@ -2083,33 +1708,6 @@ __metadata: languageName: node linkType: hard -"stream-buffers@npm:^3.0.2": - version: 3.0.3 - resolution: "stream-buffers@npm:3.0.3" - checksum: 10/8a1d5ea656fc8c3ed8daaf18e0f3755829683912c4a182f47360480f29c4757fe558518a9f5375075c71578fa1a3f18d72a0270f90fbf5288b6f119f347b156f - languageName: node - linkType: hard - -"stream-to-array@npm:~2.3.0": - version: 2.3.0 - resolution: "stream-to-array@npm:2.3.0" - dependencies: - any-promise: "npm:^1.1.0" - checksum: 10/7feaf63b38399b850615e6ffcaa951e96e4c8f46745dbce4b553a94c5dc43966933813747014935a3ff97793e7f30a65270bde19f82b2932871a1879229a77cf - languageName: node - linkType: hard - -"stream-to-promise@npm:^2.2.0": - version: 2.2.0 - resolution: "stream-to-promise@npm:2.2.0" - dependencies: - any-promise: "npm:~1.3.0" - end-of-stream: "npm:~1.1.0" - stream-to-array: "npm:~2.3.0" - checksum: 10/e4d3253c68dae65c51c5aa1bd657a072267869fd61b57068e74cee7a8e45d67fe154b56918cf546b38cb5be1fa042e632b7267abc9676bb75bba55952d2d57d1 - languageName: node - linkType: hard - "string-argv@npm:^0.3.2": version: 0.3.2 resolution: "string-argv@npm:0.3.2" @@ -2139,15 +1737,6 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:^1.1.1": - version: 1.3.0 - resolution: "string_decoder@npm:1.3.0" - dependencies: - safe-buffer: "npm:~5.2.0" - checksum: 10/54d23f4a6acae0e93f999a585e673be9e561b65cd4cca37714af1e893ab8cd8dfa52a9e4f58f48f87b4a44918d3a9254326cb80ed194bf2e4c226e2b21767e56 - languageName: node - linkType: hard - "strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -2191,16 +1780,24 @@ __metadata: languageName: node linkType: hard -"tar-stream@npm:^2.0.1": - version: 2.2.0 - resolution: "tar-stream@npm:2.2.0" +"tar@npm:^6.0.5": + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: - bl: "npm:^4.0.3" - end-of-stream: "npm:^1.4.1" - fs-constants: "npm:^1.0.0" - inherits: "npm:^2.0.3" - readable-stream: "npm:^3.1.1" - checksum: 10/1a52a51d240c118cbcd30f7368ea5e5baef1eac3e6b793fb1a41e6cd7319296c79c0264ccc5859f5294aa80f8f00b9239d519e627b9aade80038de6f966fec6a + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.0.0" + minipass: "npm:^5.0.0" + minizlib: "npm:^2.1.1" + mkdirp: "npm:^1.0.3" + yallist: "npm:^4.0.0" + checksum: 10/bfbfbb2861888077fc1130b84029cdc2721efb93d1d1fb80f22a7ac3a98ec6f8972f29e564103bbebf5e97be67ebc356d37fa48dbc4960600a1eb7230fbd1ea0 + languageName: node + linkType: hard + +"tinylogic@npm:^2.0.0": + version: 2.0.0 + resolution: "tinylogic@npm:2.0.0" + checksum: 10/6467b1ed9b602dae035726ee3faf2682bddffb5389b42fdb4daf13878037420ed9981a572ca7db467bd26c4ab00fb4eefe654f24e35984ec017fb5e83081db97 languageName: node linkType: hard @@ -2229,24 +1826,24 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.13.0, tslib@npm:^1.9.3": +"tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb languageName: node linkType: hard -"tslib@npm:^2, tslib@npm:^2.1.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3": - version: 2.7.0 - resolution: "tslib@npm:2.7.0" - checksum: 10/9a5b47ddac65874fa011c20ff76db69f97cf90c78cff5934799ab8894a5342db2d17b4e7613a087046bc1d133d21547ddff87ac558abeec31ffa929c88b7fce6 +"tslib@npm:^2, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.6.3": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 10/3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 languageName: node linkType: hard -"tunnel@npm:^0.0.6": - version: 0.0.6 - resolution: "tunnel@npm:0.0.6" - checksum: 10/cf1ffed5e67159b901a924dbf94c989f20b2b3b65649cfbbe4b6abb35955ce2cf7433b23498bdb2c5530ab185b82190fce531597b3b4a649f06a907fc8702405 +"typanion@npm:^3.8.0": + version: 3.14.0 + resolution: "typanion@npm:3.14.0" + checksum: 10/5e88d9e6121ff0ec543f572152fdd1b70e9cca35406d79013ec8e08defa8ef96de5fec9e98da3afbd1eb4426b9e8e8fe423163d0b482e34a40103cab1ef29abd languageName: node linkType: hard @@ -2257,13 +1854,6 @@ __metadata: languageName: node linkType: hard -"util-deprecate@npm:^1.0.1": - version: 1.0.2 - resolution: "util-deprecate@npm:1.0.2" - checksum: 10/474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 - languageName: node - linkType: hard - "uuid@npm:^8.3.0": version: 8.3.2 resolution: "uuid@npm:8.3.2" @@ -2329,6 +1919,13 @@ __metadata: languageName: node linkType: hard +"yallist@npm:^4.0.0": + version: 4.0.0 + resolution: "yallist@npm:4.0.0" + checksum: 10/4cb02b42b8a93b5cf50caf5d8e9beb409400a8a4d85e83bb0685c1457e9ac0b7a00819e9f5991ac25ffabb56a78e2f017c1acc010b3a1babfe6de690ba531abd + languageName: node + linkType: hard + "yaml@npm:^2.7.0": version: 2.7.0 resolution: "yaml@npm:2.7.0"