From 43b3f62dd2ab0f279aae3098a50a73a7845645e0 Mon Sep 17 00:00:00 2001 From: Baud Date: Wed, 23 Aug 2023 15:45:20 +0100 Subject: [PATCH 001/291] feat: First draft of Ingest API --- packages/openapi/api/actions.yaml | 17 + packages/openapi/api/definitions/ingest.yaml | 1502 ++++++++++++++++++ 2 files changed, 1519 insertions(+) create mode 100644 packages/openapi/api/definitions/ingest.yaml diff --git a/packages/openapi/api/actions.yaml b/packages/openapi/api/actions.yaml index 70f1788148c..8a32158c72b 100644 --- a/packages/openapi/api/actions.yaml +++ b/packages/openapi/api/actions.yaml @@ -111,3 +111,20 @@ paths: # snapshot operations /snapshots: $ref: 'definitions/snapshots.yaml#/resources/snapshots' + # ingest operations + /ingest/{studioId}/playlists: + $ref: 'definitions/ingest.yaml#/resources/ingestPlaylists' + /ingest/{studioId}/playlists/{playlistId}: + $ref: 'definitions/ingest.yaml#/resources/ingestPlaylist' + /ingest/{studioId}/playlists/{playlistId}/rundowns: + $ref: 'definitions/ingest.yaml#/resources/ingestRundowns' + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}: + $ref: 'definitions/ingest.yaml#/resources/ingestRundown' + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments: + $ref: 'definitions/ingest.yaml#/resources/ingestSegments' + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}: + $ref: 'definitions/ingest.yaml#/resources/ingestSegment' + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts: + $ref: 'definitions/ingest.yaml#/resources/ingestParts' + /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts/{partId}: + $ref: 'definitions/ingest.yaml#/resources/ingestPart' diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml new file mode 100644 index 00000000000..56f6d56462d --- /dev/null +++ b/packages/openapi/api/definitions/ingest.yaml @@ -0,0 +1,1502 @@ +title: ingest +description: Ingest methods +resources: + ingestPlaylists: + get: + operationId: getIngestPlaylists + tags: + - ingest + summary: Gets ingest data for all Playlists in Sofie belonging to a Studio. + parameters: + - name: studioId + in: path + description: Studio the Playlist belongs to. + required: true + schema: + type: string + responses: + 200: + description: Command successfully handled - returns an array of Playlist Ids. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + playlists: + type: array + items: + $ref: '#/components/schemas/ingestPlaylistItem' + example: + - id: '4e8fb4df-4d37-4ce5-adb7-009af7feb755' + externalId: 'playlist1' + - id: 'd0deb87b-e9fd-4283-977b-4cd3c9b559bd' + externalId: 'playlist2' + required: + - status + - playlists + additionalProperties: false + 404: + $ref: '#/components/responses/studioNotFound' + ingestPlaylist: + get: + operationId: getIngestPlaylist + tags: + - ingest + summary: Gets ingest data for a specific Playlist from Sofie. + parameters: + - name: studioId + in: path + description: Studio to ingest Playlist into. + required: true + schema: + type: string + - name: playlistId + in: path + description: Requested Playlist. May use external Id or Sofie internal Id. + required: true + schema: + type: string + responses: + 200: + description: Playlist is returned. + headers: + ETag: + schema: + type: string + description: Version of Playlist, if known. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + playlist: + $ref: '#/components/schemas/ingestPlaylist' + example: + status: 200 + playlist: + name: playlist1 + externalId: playlist1 + required: + - status + - playlist + additionalProperties: false + 404: + description: Invalid studioId or playlistId + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + put: + operationId: putIngestPlaylist + tags: + - ingest + summary: Creates a new or updates an existing Playlist. + parameters: + - name: studioId + in: path + description: Studio to ingest Playlist into. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist to create/update. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: If-None-Match + in: header + schema: + type: array + items: + type: string + description: If specified, the Playlist will only be updated if the specified ETag does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + - name: ETag + in: header + required: true + schema: + type: string + description: ETag to use as version information for Playlist. + requestBody: + description: Contains the Playlist data. + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Name of the Playlist as shown to the user. + externalId: + type: string + description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Rundown. + resyncURL: + type: string + description: The URL to POST a message to in order to request that the entire Playlist be re-sent to Sofie. This message will have no request body. + example: + name: playlist1 + externalId: playlist1 + required: + - name + - externalId + additionalProperties: false + responses: + 200: + description: Playlist has been updated. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false + 201: + description: Playlist has been created. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 201 + example: 201 + required: + - status + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + 409: + $ref: '#/components/responses/externalIdConflict' + delete: + operationId: deleteIngestPlaylist + tags: + - ingest + summary: Deletes a specified ingest Playlist. Resources under the Playlist (e.g. Rundowns) will also be removed. + parameters: + - name: studioId + in: path + description: Studio the ingest Playlist belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist to delete. May use external Id or Sofie internal Id. + required: true + schema: + type: string + responses: + 200: + description: Playlist removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + ingestRundowns: + get: + operationId: getIngestRundowns + tags: + - ingest + summary: Gets ingest data for all Rundowns belonging to a Playlist. + parameters: + - name: studioId + in: path + description: Studio the Playlist belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist to get all Rundowns for. May use external Id or Sofie internal Id. + required: true + schema: + type: string + responses: + 200: + description: Command successfully handled - returns an array of Rundowns. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + rundowns: + type: array + items: + $ref: '#/components/schemas/ingestRundownItem' + example: + - id: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 + externalId: rundown1 + required: + - status + - playlists + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + put: + operationId: putIngestRundowns + tags: + - ingest + summary: Creates/updates the Rundowns in a Playlist. Any existing Rundowns in the Playlist that are not included in this list will be deleted (including their Segments and Parts). Rundowns will be placed in the Playlist in the order specified by their individual ranks. If the creation/deletion/updating of any Rundown fails all changes will be discarded. ETags are not supported for bulk updates, no version information will be stored for any of the created/modified Rundowns. + parameters: + - name: studioId + in: path + description: Studio the Playlist belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist to create/update all Rundowns for. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: If-None-Match + in: header + schema: + type: array + items: + type: string + description: If specified, each Rundown will only be updated if its version does not match one of the specified ETags. If no ETag is found for a Rundown, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + requestBody: + description: Contains the Rundown data. + required: true + content: + application/json: + schema: + type: object + properties: + rundowns: + type: array + items: + $ref: '#/components/schemas/ingestRundown' + example: + - name: rundown1 + source: 'Our Company - Some Product Name' + externalId: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 + rank: 0 + required: + - rundowns + additionalProperties: false + responses: + 200: + description: Rundowns have been updated. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + 409: + $ref: '#/components/responses/externalIdConflict' + ingestRundown: + get: + operationId: getIngestRundown + tags: + - ingest + summary: Gets ingest data for a specific Rundown. + parameters: + - name: studioId + in: path + description: Studio the Rundown belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist the Rundown belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown to return. May use external Id or Sofie internal Id. + required: true + schema: + type: string + responses: + 200: + description: Rundown is returned. + headers: + ETag: + schema: + type: string + description: Version of Rundown, if known. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + rundown: + $ref: '#/components/schemas/ingestRundown' + example: + status: 200 + rundown: + name: rundown1 + source: 'Our Company - Some Product Name' + externalId: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 + rank: 0 + required: + - status + - rundown + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + put: + operationId: putIngestRundown + tags: + - ingest + summary: Creates a new or updates an existing Rundown. + parameters: + - name: studioId + in: path + description: Studio to ingest Rundown into. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist to ingest Rundown into. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown to create/update. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: If-None-Match + in: header + schema: + type: array + items: + type: string + description: If specified, the Rundown will only be updated if the specified ETag does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + - name: ETag + in: header + required: true + schema: + type: string + description: ETag to use as version information for Rundown. + requestBody: + description: Contains the Rundown data. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ingestRundown' + example: + name: rundown1 + source: 'Our Company - Some Product Name' + externalId: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 + rank: 0 + responses: + 200: + description: Rundown has been updated. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false + 201: + description: Rundown has been created. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 201 + example: 201 + required: + - status + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + 409: + $ref: '#/components/responses/externalIdConflict' + delete: + operationId: deleteIngestRundown + tags: + - ingest + summary: Deletes a specified ingest Rundown. Resources under the Rundown (e.g. Segments) will also be removed. + parameters: + - name: studioId + in: path + description: Studio the ingest Rundown belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist the ingest Rundown belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown to delete. May use external Id or Sofie internal Id. + required: true + schema: + type: string + responses: + 200: + description: Rundown removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + ingestSegments: + get: + operationId: getIngestSegments + tags: + - ingest + summary: Gets the ingest data for all Segments belonging to a Rundown. + parameters: + - name: studioId + in: path + description: Studio the Segment belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist the Rundown belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown to get Segments for. May use external Id or Sofie internal Id. + required: true + schema: + type: string + responses: + 200: + description: Command successfully handled - returns an array of Segments. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + segments: + type: array + items: + $ref: '#/components/schemas/ingestSegmentItem' + example: + - id: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 + externalId: segment1 + required: + - status + - segments + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + put: + operationId: putIngestSegments + tags: + - ingest + summary: Creates/updates the Segments in a Rundown. Any existing Segments in the Rundown that are not included in this list will be deleted (including their Parts). Segments will be placed in the Rundown in the order specified by their individual ranks. If the creation/deletion/updating of any Segment fails all changes will be discarded. + parameters: + - name: studioId + in: path + description: Studio the Rundown belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist the Rundown belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown to create/update all Segments for. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: If-None-Match + in: header + schema: + type: array + items: + type: string + description: If specified, each Segment will only be updated if its version does not match one of the specified ETags. If no ETag is found for a Segment, the new data will replace whatever currently exists, regardless of whether the data is actually the same. ETags are not supported for bulk updates, no version information will be stored for any of the created/modified Rundowns. + requestBody: + description: Contains the Segment data. + required: true + content: + application/json: + schema: + type: object + properties: + segments: + type: array + items: + $ref: '#/components/schemas/ingestSegment' + example: + - name: segment1 + externalId: segment1 + rank: 0 + required: + - segments + additionalProperties: false + responses: + 200: + description: Segments have been updated. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + 409: + $ref: '#/components/responses/externalIdConflict' + ingestSegment: + get: + operationId: getIngestSegment + tags: + - ingest + summary: Gets ingest data for a specific Segment. + parameters: + - name: studioId + in: path + description: Studio the Segment belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist the Segment belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown the Segment belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: segmentId + in: path + description: Segment to create/update. May use external Id or Sofie internal Id. + required: true + schema: + type: string + responses: + 200: + description: Segment is returned. + headers: + ETag: + schema: + type: string + description: Version of Segment, if known. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + segment: + $ref: '#/components/schemas/ingestSegment' + example: + status: 200 + segment: + name: segment1 + externalId: segment1 + rank: 0 + required: + - status + - segment + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/segmentNotFound' + put: + operationId: putIngestSegment + tags: + - ingest + summary: Creates a new or updates an existing Segment. + parameters: + - name: studioId + in: path + description: Studio to ingest Segment into. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist to ingest Segment into. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown to ingest Segment into. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: segmentId + in: path + description: Segment to create/update. May use external Id or Sofie internal Id. + schema: + type: string + - name: If-None-Match + in: header + schema: + type: array + items: + type: string + description: If specified, the Segment will only be updated if the specified ETag does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + - name: ETag + in: header + required: true + schema: + type: string + description: ETag to use as version information for Segment. + requestBody: + description: Contains the Segment data. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ingestSegment' + example: + name: segment1 + externalId: segment1 + rank: 0 + responses: + 200: + description: Segment has been updated. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false + 201: + description: Segment has been created. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 201 + example: 201 + required: + - status + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + 409: + $ref: '#/components/responses/externalIdConflict' + delete: + operationId: deleteIngestSegment + tags: + - ingest + summary: Deletes a specified ingest Segment. Resources under the Segment (e.g. Parts) will also be removed. + parameters: + - name: studioId + in: path + description: Studio the ingest Segment belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist the ingest Segment belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown the ingest Segment belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: segmentId + in: path + description: Segment to delete. May use external Id or Sofie internal Id. + required: true + schema: + type: string + responses: + 200: + description: Segment removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + ingestParts: + get: + operationId: getIngestParts + tags: + - ingest + summary: Gets the ingest data for all Parts belonging to a Segment. + parameters: + - name: studioId + in: path + description: Studio the Segment belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist the Segment belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown the Segment belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: segmentId + in: path + description: Segment to get Parts for. May use external Id or Sofie internal Id. + required: true + schema: + type: string + responses: + 200: + description: Command successfully handled - returns an array of Parts. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + parts: + type: array + items: + $ref: '#/components/schemas/ingestPartItem' + example: + - name: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 + externalId: part1 + required: + - status + - parts + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/segmentNotFound' + put: + operationId: putIngestParts + tags: + - ingest + summary: Creates/updates the Parts in a Segment. Any existing Parts in the Segment that are not included in this list will be deleted. Parts will be placed in the Segment in the order specified by their individual ranks. If the creation/deletion/updating of any Parts fails all changes will be discarded. + parameters: + - name: studioId + in: path + description: Studio the Segment belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist the Segment belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown the Segment belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: segmentId + in: path + description: Segment to create/update all Parts for. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: If-None-Match + in: header + schema: + type: array + items: + type: string + description: If specified, each Part will only be updated if its version does not match one of the specified ETags. If no ETag is found for a Part, the new data will replace whatever currently exists, regardless of whether the data is actually the same. ETags are not supported for bulk updates, no version information will be stored for any of the created/modified Rundowns. + requestBody: + description: Contains the Part data. + required: true + content: + application/json: + schema: + type: object + properties: + parts: + type: array + items: + $ref: '#/components/schemas/ingestPartItem' + example: + - name: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 + externalId: part1 + required: + - parts + additionalProperties: false + responses: + 200: + description: Parts have been updated. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/partNotFound' + 409: + $ref: '#/components/responses/externalIdConflict' + ingestPart: + get: + operationId: getIngestPart + tags: + - ingest + summary: Gets ingest data for a specific Part. + parameters: + - name: studioId + in: path + description: Studio the Part belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist the Part belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown the Part belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: segmentId + in: path + description: Segment the Part belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: partId + in: path + description: Part to create/update. May use external Id or Sofie internal Id. + required: true + schema: + type: string + responses: + 200: + description: Part is returned. + headers: + ETag: + schema: + type: string + description: Version of Part, if known. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + part: + $ref: '#/components/schemas/ingestPart' + example: + status: 200 + part: + name: part1 + externalId: part1 + rank: 0 + required: + - status + - part + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/segmentNotFound' + # - $ref: '#/components/responses/partNotFound' + put: + operationId: putIngestPart + tags: + - ingest + summary: Creates a new or updates an existing Part. + parameters: + - name: studioId + in: path + description: Studio to ingest Part into. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist to ingest Part into. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown to ingest Part into. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: segmentId + in: path + description: Segment to ingest Part into. May use external Id or Sofie internal Id. + schema: + type: string + - name: partId + in: path + description: Part to update/create. May use external Id or Sofie internal Id. + schema: + type: string + - name: If-None-Match + in: header + schema: + type: array + items: + type: string + description: If specified, the Part will only be updated if the specified ETag does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + - name: ETag + in: header + required: true + schema: + type: string + description: ETag to use as version information for Part. + requestBody: + description: Contains the Rundown data. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ingestPart' + example: + name: part1 + externalId: part1 + rank: 0 + responses: + 200: + description: Part has been updated. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false + 201: + description: Part has been created. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 201 + example: 201 + required: + - status + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/segmentNotFound' + 409: + $ref: '#/components/responses/externalIdConflict' + delete: + operationId: deleteIngestPart + tags: + - ingest + summary: Deletes a specified ingest Part. + parameters: + - name: studioId + in: path + description: Studio the ingest Part belongs to. + required: true + schema: + type: string + - name: playlistId + in: path + description: Playlist the ingest Part belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown the ingest Part belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: segmentId + in: path + description: Segment the ingest Part belongs to. May use external Id or Sofie internal Id. + required: true + schema: + type: string + - name: partId + in: path + description: Part to delete. May use external Id or Sofie internal Id. + required: true + schema: + type: string + responses: + 200: + description: Part removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false + 404: + $ref: '#/components/responses/idNotFound' + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + # - $ref: '#/components/responses/rundownNotFound' + # - $ref: '#/components/responses/segmentNotFound' + +components: + responses: + idNotFound: + # oneOf responses like below don't render correctly with current tools - use studio as an example for the docs. + # oneOf: + # - $ref: '#/components/responses/studioNotFound' + # - $ref: '#/components/responses/playlistNotFound' + $ref: '#/components/responses/studioNotFound' + studioNotFound: + description: The specified Studio does not exist. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 404 + example: 404 + notFound: + type: string + const: studio + example: studio + message: + type: string + example: The specified Studio was not found. + required: + - status + - notFound + - 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 + notFound: + type: string + const: rundown + example: rundown + 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 + notFound: + type: string + const: segment + example: segment + 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 + notFound: + type: string + const: part + example: part + message: + type: string + example: The specified Part was not found. + required: + - status + - notFound + - message + additionalProperties: false + externalIdConflict: + description: The provided external Id is already in use by a different object with a different Sofie internal Id. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 409 + example: 409 + conflict: + type: string + const: externalId + example: externalId + message: + type: string + example: The externalId "b105625d-e1ab-4ce7-b99f-d720cdcdc519" is already in use by "808267a9-a074-4800-b6c7-bfcf7dc1f144" with externalId "9c8faa5d-07af-4e2b-a860-46c3b0f30b2e". + schemas: + ingestPlaylistItem: + type: object + properties: + id: + type: string + description: The Id used internally by Sofie. + externalId: + type: string + description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Playlist. + required: + - id + - externalId + additionalProperties: false + ingestRundownItem: + type: object + properties: + id: + type: string + description: The Id used internally by Sofie. + externalId: + type: string + description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Rundown. + required: + - id + - externalId + additionalProperties: false + ingestSegmentItem: + type: object + properties: + id: + type: string + description: The Id used internally by Sofie + externalId: + type: string + description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Segment. + required: + - id + - externalId + additionalProperties: false + ingestPartItem: + type: object + properties: + name: + type: string + externalId: + type: string + description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Part. + required: + - name + - externalId + additionalProperties: false + ingestPlaylist: + type: object + properties: + name: + type: string + externalId: + type: string + description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Playlist. + resyncURL: + type: string + description: The URL to POST a message to in order to request that the entire Playlist be re-sent to Sofie. This message will have no request body. + required: + - name + - externalId + additionalProperties: false + ingestRundown: + type: object + properties: + name: + type: string + source: + type: string + description: A source type that can be displayed to the end-user. Should identify what type of system (e.g. vendor/product name) the data has been sent from. + examples: + - 'Some Product Name' + - 'Our Company - Some Product Name' + externalId: + type: string + description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Rundown. + rank: + type: number + description: The position of the Rundown in the parent Playlist. + inclusiveMinimum: 0.0 + payload: + type: object + additionalProperties: true + required: + - name + - source + - externalId + - rank + additionalProperties: false + ingestSegment: + type: object + properties: + name: + type: string + externalId: + type: string + description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Segment. + rank: + type: number + description: The position of the Segment in the parent Rundown. + inclusiveMinimum: 0.0 + payload: + type: object + additionalProperties: true + required: + - name + - externalId + - rank + additionalProperties: false + ingestPart: + type: object + properties: + name: + type: string + externalId: + type: string + description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Part. + rank: + type: number + description: The position of the Part in the parent Segment. + payload: + type: object + additionalProperties: true + required: + - name + - externalId + - rank + additionalProperties: false From 2fee4deff2b28217bcfee636e629fb6b9c77945b Mon Sep 17 00:00:00 2001 From: Baud Date: Thu, 31 Aug 2023 10:45:15 +0100 Subject: [PATCH 002/291] chore: Remove resyncURL --- packages/openapi/api/definitions/ingest.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index 56f6d56462d..8a882fd95f3 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -138,9 +138,6 @@ resources: externalId: type: string description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Rundown. - resyncURL: - type: string - description: The URL to POST a message to in order to request that the entire Playlist be re-sent to Sofie. This message will have no request body. example: name: playlist1 externalId: playlist1 @@ -1427,9 +1424,6 @@ components: externalId: type: string description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Playlist. - resyncURL: - type: string - description: The URL to POST a message to in order to request that the entire Playlist be re-sent to Sofie. This message will have no request body. required: - name - externalId From bd112e94702660ad25b7f4e1d493f4525275afb8 Mon Sep 17 00:00:00 2001 From: Baud Date: Thu, 7 Sep 2023 13:37:56 +0100 Subject: [PATCH 003/291] chore: Remove internal Ids --- packages/openapi/api/definitions/ingest.yaml | 180 ++++++------------- 1 file changed, 50 insertions(+), 130 deletions(-) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index 8a882fd95f3..f8b6d6219f1 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -31,10 +31,8 @@ resources: items: $ref: '#/components/schemas/ingestPlaylistItem' example: - - id: '4e8fb4df-4d37-4ce5-adb7-009af7feb755' - externalId: 'playlist1' - - id: 'd0deb87b-e9fd-4283-977b-4cd3c9b559bd' - externalId: 'playlist2' + - externalId: 'playlist1' + - externalId: 'playlist2' required: - status - playlists @@ -56,7 +54,7 @@ resources: type: string - name: playlistId in: path - description: Requested Playlist. May use external Id or Sofie internal Id. + description: Requested Playlist. required: true schema: type: string @@ -82,7 +80,6 @@ resources: status: 200 playlist: name: playlist1 - externalId: playlist1 required: - status - playlist @@ -107,7 +104,7 @@ resources: type: string - name: playlistId in: path - description: Playlist to create/update. May use external Id or Sofie internal Id. + description: Playlist to create/update. required: true schema: type: string @@ -135,15 +132,10 @@ resources: name: type: string description: Name of the Playlist as shown to the user. - externalId: - type: string - description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Rundown. - example: - name: playlist1 - externalId: playlist1 required: - name - - externalId + example: + name: playlist1 additionalProperties: false responses: 200: @@ -176,8 +168,6 @@ resources: additionalProperties: false 404: $ref: '#/components/responses/idNotFound' - 409: - $ref: '#/components/responses/externalIdConflict' delete: operationId: deleteIngestPlaylist tags: @@ -192,7 +182,7 @@ resources: type: string - name: playlistId in: path - description: Playlist to delete. May use external Id or Sofie internal Id. + description: Playlist to delete. required: true schema: type: string @@ -231,7 +221,7 @@ resources: type: string - name: playlistId in: path - description: Playlist to get all Rundowns for. May use external Id or Sofie internal Id. + description: Playlist to get all Rundowns for. required: true schema: type: string @@ -252,8 +242,7 @@ resources: items: $ref: '#/components/schemas/ingestRundownItem' example: - - id: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 - externalId: rundown1 + - externalId: rundown1 required: - status - playlists @@ -277,7 +266,7 @@ resources: type: string - name: playlistId in: path - description: Playlist to create/update all Rundowns for. May use external Id or Sofie internal Id. + description: Playlist to create/update all Rundowns for. required: true schema: type: string @@ -303,7 +292,6 @@ resources: example: - name: rundown1 source: 'Our Company - Some Product Name' - externalId: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 rank: 0 required: - rundowns @@ -328,8 +316,6 @@ resources: # oneOf: # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' - 409: - $ref: '#/components/responses/externalIdConflict' ingestRundown: get: operationId: getIngestRundown @@ -345,13 +331,13 @@ resources: type: string - name: playlistId in: path - description: Playlist the Rundown belongs to. May use external Id or Sofie internal Id. + description: Playlist the Rundown belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown to return. May use external Id or Sofie internal Id. + description: Rundown to return. required: true schema: type: string @@ -378,7 +364,6 @@ resources: rundown: name: rundown1 source: 'Our Company - Some Product Name' - externalId: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 rank: 0 required: - status @@ -404,13 +389,13 @@ resources: type: string - name: playlistId in: path - description: Playlist to ingest Rundown into. May use external Id or Sofie internal Id. + description: Playlist to ingest Rundown into. required: true schema: type: string - name: rundownId in: path - description: Rundown to create/update. May use external Id or Sofie internal Id. + description: Rundown to create/update. required: true schema: type: string @@ -437,7 +422,6 @@ resources: example: name: rundown1 source: 'Our Company - Some Product Name' - externalId: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 rank: 0 responses: 200: @@ -474,8 +458,6 @@ resources: # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' - 409: - $ref: '#/components/responses/externalIdConflict' delete: operationId: deleteIngestRundown tags: @@ -490,13 +472,13 @@ resources: type: string - name: playlistId in: path - description: Playlist the ingest Rundown belongs to. May use external Id or Sofie internal Id. + description: Playlist the ingest Rundown belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown to delete. May use external Id or Sofie internal Id. + description: Rundown to delete. required: true schema: type: string @@ -536,13 +518,13 @@ resources: type: string - name: playlistId in: path - description: Playlist the Rundown belongs to. May use external Id or Sofie internal Id. + description: Playlist the Rundown belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown to get Segments for. May use external Id or Sofie internal Id. + description: Rundown to get Segments for. required: true schema: type: string @@ -563,8 +545,7 @@ resources: items: $ref: '#/components/schemas/ingestSegmentItem' example: - - id: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 - externalId: segment1 + - externalId: segment1 required: - status - segments @@ -589,13 +570,13 @@ resources: type: string - name: playlistId in: path - description: Playlist the Rundown belongs to. May use external Id or Sofie internal Id. + description: Playlist the Rundown belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown to create/update all Segments for. May use external Id or Sofie internal Id. + description: Rundown to create/update all Segments for. required: true schema: type: string @@ -620,7 +601,6 @@ resources: $ref: '#/components/schemas/ingestSegment' example: - name: segment1 - externalId: segment1 rank: 0 required: - segments @@ -646,8 +626,6 @@ resources: # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' - 409: - $ref: '#/components/responses/externalIdConflict' ingestSegment: get: operationId: getIngestSegment @@ -663,19 +641,19 @@ resources: type: string - name: playlistId in: path - description: Playlist the Segment belongs to. May use external Id or Sofie internal Id. + description: Playlist the Segment belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown the Segment belongs to. May use external Id or Sofie internal Id. + description: Rundown the Segment belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment to create/update. May use external Id or Sofie internal Id. + description: Segment to create/update. required: true schema: type: string @@ -701,7 +679,6 @@ resources: status: 200 segment: name: segment1 - externalId: segment1 rank: 0 required: - status @@ -728,19 +705,19 @@ resources: type: string - name: playlistId in: path - description: Playlist to ingest Segment into. May use external Id or Sofie internal Id. + description: Playlist to ingest Segment into. required: true schema: type: string - name: rundownId in: path - description: Rundown to ingest Segment into. May use external Id or Sofie internal Id. + description: Rundown to ingest Segment into. required: true schema: type: string - name: segmentId in: path - description: Segment to create/update. May use external Id or Sofie internal Id. + description: Segment to create/update. schema: type: string - name: If-None-Match @@ -765,7 +742,6 @@ resources: $ref: '#/components/schemas/ingestSegment' example: name: segment1 - externalId: segment1 rank: 0 responses: 200: @@ -802,8 +778,6 @@ resources: # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' - 409: - $ref: '#/components/responses/externalIdConflict' delete: operationId: deleteIngestSegment tags: @@ -818,19 +792,19 @@ resources: type: string - name: playlistId in: path - description: Playlist the ingest Segment belongs to. May use external Id or Sofie internal Id. + description: Playlist the ingest Segment belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown the ingest Segment belongs to. May use external Id or Sofie internal Id. + description: Rundown the ingest Segment belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment to delete. May use external Id or Sofie internal Id. + description: Segment to delete. required: true schema: type: string @@ -870,19 +844,19 @@ resources: type: string - name: playlistId in: path - description: Playlist the Segment belongs to. May use external Id or Sofie internal Id. + description: Playlist the Segment belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown the Segment belongs to. May use external Id or Sofie internal Id. + description: Rundown the Segment belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment to get Parts for. May use external Id or Sofie internal Id. + description: Segment to get Parts for. required: true schema: type: string @@ -903,8 +877,7 @@ resources: items: $ref: '#/components/schemas/ingestPartItem' example: - - name: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 - externalId: part1 + - externalId: part1 required: - status - parts @@ -930,19 +903,19 @@ resources: type: string - name: playlistId in: path - description: Playlist the Segment belongs to. May use external Id or Sofie internal Id. + description: Playlist the Segment belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown the Segment belongs to. May use external Id or Sofie internal Id. + description: Rundown the Segment belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment to create/update all Parts for. May use external Id or Sofie internal Id. + description: Segment to create/update all Parts for. required: true schema: type: string @@ -966,8 +939,7 @@ resources: items: $ref: '#/components/schemas/ingestPartItem' example: - - name: 4e8fb4df-4d37-4ce5-adb7-009af7feb755 - externalId: part1 + - externalId: part1 required: - parts additionalProperties: false @@ -993,8 +965,6 @@ resources: # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/partNotFound' - 409: - $ref: '#/components/responses/externalIdConflict' ingestPart: get: operationId: getIngestPart @@ -1010,25 +980,25 @@ resources: type: string - name: playlistId in: path - description: Playlist the Part belongs to. May use external Id or Sofie internal Id. + description: Playlist the Part belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown the Part belongs to. May use external Id or Sofie internal Id. + description: Rundown the Part belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment the Part belongs to. May use external Id or Sofie internal Id. + description: Segment the Part belongs to. required: true schema: type: string - name: partId in: path - description: Part to create/update. May use external Id or Sofie internal Id. + description: Part to create/update. required: true schema: type: string @@ -1054,7 +1024,6 @@ resources: status: 200 part: name: part1 - externalId: part1 rank: 0 required: - status @@ -1082,24 +1051,24 @@ resources: type: string - name: playlistId in: path - description: Playlist to ingest Part into. May use external Id or Sofie internal Id. + description: Playlist to ingest Part into. required: true schema: type: string - name: rundownId in: path - description: Rundown to ingest Part into. May use external Id or Sofie internal Id. + description: Rundown to ingest Part into. required: true schema: type: string - name: segmentId in: path - description: Segment to ingest Part into. May use external Id or Sofie internal Id. + description: Segment to ingest Part into. schema: type: string - name: partId in: path - description: Part to update/create. May use external Id or Sofie internal Id. + description: Part to update/create. schema: type: string - name: If-None-Match @@ -1124,7 +1093,6 @@ resources: $ref: '#/components/schemas/ingestPart' example: name: part1 - externalId: part1 rank: 0 responses: 200: @@ -1162,8 +1130,6 @@ resources: # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/segmentNotFound' - 409: - $ref: '#/components/responses/externalIdConflict' delete: operationId: deleteIngestPart tags: @@ -1178,25 +1144,25 @@ resources: type: string - name: playlistId in: path - description: Playlist the ingest Part belongs to. May use external Id or Sofie internal Id. + description: Playlist the ingest Part belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown the ingest Part belongs to. May use external Id or Sofie internal Id. + description: Rundown the ingest Part belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment the ingest Part belongs to. May use external Id or Sofie internal Id. + description: Segment the ingest Part belongs to. required: true schema: type: string - name: partId in: path - description: Part to delete. May use external Id or Sofie internal Id. + description: Part to delete. required: true schema: type: string @@ -1346,62 +1312,32 @@ components: - notFound - message additionalProperties: false - externalIdConflict: - description: The provided external Id is already in use by a different object with a different Sofie internal Id. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 409 - example: 409 - conflict: - type: string - const: externalId - example: externalId - message: - type: string - example: The externalId "b105625d-e1ab-4ce7-b99f-d720cdcdc519" is already in use by "808267a9-a074-4800-b6c7-bfcf7dc1f144" with externalId "9c8faa5d-07af-4e2b-a860-46c3b0f30b2e". schemas: ingestPlaylistItem: type: object properties: - id: - type: string - description: The Id used internally by Sofie. externalId: type: string description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Playlist. required: - - id - externalId additionalProperties: false ingestRundownItem: type: object properties: - id: - type: string - description: The Id used internally by Sofie. externalId: type: string description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Rundown. required: - - id - externalId additionalProperties: false ingestSegmentItem: type: object properties: - id: - type: string - description: The Id used internally by Sofie externalId: type: string description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Segment. required: - - id - externalId additionalProperties: false ingestPartItem: @@ -1421,12 +1357,8 @@ components: properties: name: type: string - externalId: - type: string - description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Playlist. required: - name - - externalId additionalProperties: false ingestRundown: type: object @@ -1439,9 +1371,6 @@ components: examples: - 'Some Product Name' - 'Our Company - Some Product Name' - externalId: - type: string - description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Rundown. rank: type: number description: The position of the Rundown in the parent Playlist. @@ -1452,7 +1381,6 @@ components: required: - name - source - - externalId - rank additionalProperties: false ingestSegment: @@ -1460,9 +1388,6 @@ components: properties: name: type: string - externalId: - type: string - description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Segment. rank: type: number description: The position of the Segment in the parent Rundown. @@ -1472,7 +1397,6 @@ components: additionalProperties: true required: - name - - externalId - rank additionalProperties: false ingestPart: @@ -1480,9 +1404,6 @@ components: properties: name: type: string - externalId: - type: string - description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Part. rank: type: number description: The position of the Part in the parent Segment. @@ -1491,6 +1412,5 @@ components: additionalProperties: true required: - name - - externalId - rank additionalProperties: false From e5706be0ffdb4e53b3ac9efb1f1dacad8e840cfe Mon Sep 17 00:00:00 2001 From: Baud Date: Thu, 7 Sep 2023 13:52:16 +0100 Subject: [PATCH 004/291] fix: Cleanup some errors --- packages/openapi/api/definitions/ingest.yaml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index f8b6d6219f1..c219c1cdcc3 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -126,17 +126,7 @@ resources: required: true content: application/json: - schema: - type: object - properties: - name: - type: string - description: Name of the Playlist as shown to the user. - required: - - name - example: - name: playlist1 - additionalProperties: false + $ref: '#/components/schemas/ingestPlaylist' responses: 200: description: Playlist has been updated. @@ -937,7 +927,7 @@ resources: parts: type: array items: - $ref: '#/components/schemas/ingestPartItem' + $ref: '#/components/schemas/ingestPart' example: - externalId: part1 required: From a1be4289dc61fa8a9e24034b8136470b328bf0f7 Mon Sep 17 00:00:00 2001 From: Baud Date: Thu, 7 Sep 2023 15:25:38 +0100 Subject: [PATCH 005/291] feat: If-Match headers --- packages/openapi/api/definitions/ingest.yaml | 34 ++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index c219c1cdcc3..22eaed55c8e 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -114,7 +114,14 @@ resources: type: array items: type: string - description: If specified, the Playlist will only be updated if the specified ETag does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + description: If specified, the Playlist will only be updated if one of the specified ETags does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + - name: If-Match + in: header + schema: + type: array + items: + type: string + description: If specified, the Playlist will only be updated if one of the specified ETags matches. - name: ETag in: header required: true @@ -395,7 +402,14 @@ resources: type: array items: type: string - description: If specified, the Rundown will only be updated if the specified ETag does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + description: If specified, the Rundown will only be updated if one of the specified ETags does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + - name: If-Match + in: header + schema: + type: array + items: + type: string + description: If specified, the Rundown will only be updated if one of the specified ETags matches. - name: ETag in: header required: true @@ -716,7 +730,14 @@ resources: type: array items: type: string - description: If specified, the Segment will only be updated if the specified ETag does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + description: If specified, the Segment will only be updated if one of the specified ETags does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + - name: If-Match + in: header + schema: + type: array + items: + type: string + description: If specified, the Segment will only be updated if one of the specified ETags matches. - name: ETag in: header required: true @@ -1068,6 +1089,13 @@ resources: items: type: string description: If specified, the Part will only be updated if the specified ETag does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + - name: If-Match + in: header + schema: + type: array + items: + type: string + description: If specified, the Part will only be updated if one of the specified ETags matches. - name: ETag in: header required: true From e274374a9b95199a3745c58e60a31529d456fbfc Mon Sep 17 00:00:00 2001 From: Baud Date: Tue, 31 Oct 2023 16:40:19 +0000 Subject: [PATCH 006/291] chore: Remove studio Ids from ingest API --- packages/openapi/api/actions.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/openapi/api/actions.yaml b/packages/openapi/api/actions.yaml index 8a32158c72b..30e557d6318 100644 --- a/packages/openapi/api/actions.yaml +++ b/packages/openapi/api/actions.yaml @@ -112,19 +112,19 @@ paths: /snapshots: $ref: 'definitions/snapshots.yaml#/resources/snapshots' # ingest operations - /ingest/{studioId}/playlists: + /ingest/playlists: $ref: 'definitions/ingest.yaml#/resources/ingestPlaylists' - /ingest/{studioId}/playlists/{playlistId}: + /ingest/playlists/{playlistId}: $ref: 'definitions/ingest.yaml#/resources/ingestPlaylist' - /ingest/{studioId}/playlists/{playlistId}/rundowns: + /ingest/playlists/{playlistId}/rundowns: $ref: 'definitions/ingest.yaml#/resources/ingestRundowns' - /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}: + /ingest/playlists/{playlistId}/rundowns/{rundownId}: $ref: 'definitions/ingest.yaml#/resources/ingestRundown' - /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments: + /ingest/playlists/{playlistId}/rundowns/{rundownId}/segments: $ref: 'definitions/ingest.yaml#/resources/ingestSegments' - /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}: + /ingest/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}: $ref: 'definitions/ingest.yaml#/resources/ingestSegment' - /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts: + /ingest/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts: $ref: 'definitions/ingest.yaml#/resources/ingestParts' - /ingest/{studioId}/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts/{partId}: + /ingest/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts/{partId}: $ref: 'definitions/ingest.yaml#/resources/ingestPart' From d7de38648296e8077bb9cd129d9884e4fcaf0757 Mon Sep 17 00:00:00 2001 From: Baud Date: Thu, 2 Nov 2023 13:37:29 +0000 Subject: [PATCH 007/291] chore: Remove references to studio --- packages/openapi/api/definitions/ingest.yaml | 166 +------------------ 1 file changed, 4 insertions(+), 162 deletions(-) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index 22eaed55c8e..a794e9ae337 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -6,14 +6,7 @@ resources: operationId: getIngestPlaylists tags: - ingest - summary: Gets ingest data for all Playlists in Sofie belonging to a Studio. - parameters: - - name: studioId - in: path - description: Studio the Playlist belongs to. - required: true - schema: - type: string + summary: Gets ingest data for all Playlists in Sofie. responses: 200: description: Command successfully handled - returns an array of Playlist Ids. @@ -37,8 +30,6 @@ resources: - status - playlists additionalProperties: false - 404: - $ref: '#/components/responses/studioNotFound' ingestPlaylist: get: operationId: getIngestPlaylist @@ -46,12 +37,6 @@ resources: - ingest summary: Gets ingest data for a specific Playlist from Sofie. parameters: - - name: studioId - in: path - description: Studio to ingest Playlist into. - required: true - schema: - type: string - name: playlistId in: path description: Requested Playlist. @@ -85,10 +70,9 @@ resources: - playlist additionalProperties: false 404: - description: Invalid studioId or playlistId + description: Invalid playlistId $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' put: operationId: putIngestPlaylist @@ -96,12 +80,6 @@ resources: - ingest summary: Creates a new or updates an existing Playlist. parameters: - - name: studioId - in: path - description: Studio to ingest Playlist into. - required: true - schema: - type: string - name: playlistId in: path description: Playlist to create/update. @@ -171,12 +149,6 @@ resources: - ingest summary: Deletes a specified ingest Playlist. Resources under the Playlist (e.g. Rundowns) will also be removed. parameters: - - name: studioId - in: path - description: Studio the ingest Playlist belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist to delete. @@ -201,7 +173,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' ingestRundowns: get: @@ -210,12 +181,6 @@ resources: - ingest summary: Gets ingest data for all Rundowns belonging to a Playlist. parameters: - - name: studioId - in: path - description: Studio the Playlist belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist to get all Rundowns for. @@ -247,7 +212,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' put: operationId: putIngestRundowns @@ -255,12 +219,6 @@ resources: - ingest summary: Creates/updates the Rundowns in a Playlist. Any existing Rundowns in the Playlist that are not included in this list will be deleted (including their Segments and Parts). Rundowns will be placed in the Playlist in the order specified by their individual ranks. If the creation/deletion/updating of any Rundown fails all changes will be discarded. ETags are not supported for bulk updates, no version information will be stored for any of the created/modified Rundowns. parameters: - - name: studioId - in: path - description: Studio the Playlist belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist to create/update all Rundowns for. @@ -311,7 +269,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' ingestRundown: get: @@ -320,12 +277,6 @@ resources: - ingest summary: Gets ingest data for a specific Rundown. parameters: - - name: studioId - in: path - description: Studio the Rundown belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist the Rundown belongs to. @@ -369,7 +320,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' put: @@ -378,12 +328,6 @@ resources: - ingest summary: Creates a new or updates an existing Rundown. parameters: - - name: studioId - in: path - description: Studio to ingest Rundown into. - required: true - schema: - type: string - name: playlistId in: path description: Playlist to ingest Rundown into. @@ -459,7 +403,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' delete: @@ -468,12 +411,6 @@ resources: - ingest summary: Deletes a specified ingest Rundown. Resources under the Rundown (e.g. Segments) will also be removed. parameters: - - name: studioId - in: path - description: Studio the ingest Rundown belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist the ingest Rundown belongs to. @@ -504,7 +441,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' ingestSegments: @@ -514,12 +450,6 @@ resources: - ingest summary: Gets the ingest data for all Segments belonging to a Rundown. parameters: - - name: studioId - in: path - description: Studio the Segment belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist the Rundown belongs to. @@ -557,7 +487,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' put: @@ -566,12 +495,6 @@ resources: - ingest summary: Creates/updates the Segments in a Rundown. Any existing Segments in the Rundown that are not included in this list will be deleted (including their Parts). Segments will be placed in the Rundown in the order specified by their individual ranks. If the creation/deletion/updating of any Segment fails all changes will be discarded. parameters: - - name: studioId - in: path - description: Studio the Rundown belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist the Rundown belongs to. @@ -627,7 +550,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' ingestSegment: @@ -637,12 +559,6 @@ resources: - ingest summary: Gets ingest data for a specific Segment. parameters: - - name: studioId - in: path - description: Studio the Segment belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist the Segment belongs to. @@ -691,7 +607,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/segmentNotFound' @@ -701,12 +616,6 @@ resources: - ingest summary: Creates a new or updates an existing Segment. parameters: - - name: studioId - in: path - description: Studio to ingest Segment into. - required: true - schema: - type: string - name: playlistId in: path description: Playlist to ingest Segment into. @@ -786,7 +695,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' delete: @@ -795,12 +703,6 @@ resources: - ingest summary: Deletes a specified ingest Segment. Resources under the Segment (e.g. Parts) will also be removed. parameters: - - name: studioId - in: path - description: Studio the ingest Segment belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist the ingest Segment belongs to. @@ -837,7 +739,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' ingestParts: @@ -847,12 +748,6 @@ resources: - ingest summary: Gets the ingest data for all Parts belonging to a Segment. parameters: - - name: studioId - in: path - description: Studio the Segment belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist the Segment belongs to. @@ -896,7 +791,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/segmentNotFound' @@ -906,12 +800,6 @@ resources: - ingest summary: Creates/updates the Parts in a Segment. Any existing Parts in the Segment that are not included in this list will be deleted. Parts will be placed in the Segment in the order specified by their individual ranks. If the creation/deletion/updating of any Parts fails all changes will be discarded. parameters: - - name: studioId - in: path - description: Studio the Segment belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist the Segment belongs to. @@ -972,7 +860,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/partNotFound' @@ -983,12 +870,6 @@ resources: - ingest summary: Gets ingest data for a specific Part. parameters: - - name: studioId - in: path - description: Studio the Part belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist the Part belongs to. @@ -1043,7 +924,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/segmentNotFound' @@ -1054,12 +934,6 @@ resources: - ingest summary: Creates a new or updates an existing Part. parameters: - - name: studioId - in: path - description: Studio to ingest Part into. - required: true - schema: - type: string - name: playlistId in: path description: Playlist to ingest Part into. @@ -1144,7 +1018,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/segmentNotFound' @@ -1154,12 +1027,6 @@ resources: - ingest summary: Deletes a specified ingest Part. parameters: - - name: studioId - in: path - description: Studio the ingest Part belongs to. - required: true - schema: - type: string - name: playlistId in: path description: Playlist the ingest Part belongs to. @@ -1202,7 +1069,6 @@ resources: 404: $ref: '#/components/responses/idNotFound' # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/segmentNotFound' @@ -1210,34 +1076,10 @@ resources: components: responses: idNotFound: - # oneOf responses like below don't render correctly with current tools - use studio as an example for the docs. + # oneOf responses like below don't render correctly with current tools - use playlist as an example for the docs. # oneOf: - # - $ref: '#/components/responses/studioNotFound' # - $ref: '#/components/responses/playlistNotFound' - $ref: '#/components/responses/studioNotFound' - studioNotFound: - description: The specified Studio does not exist. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 404 - example: 404 - notFound: - type: string - const: studio - example: studio - message: - type: string - example: The specified Studio was not found. - required: - - status - - notFound - - message - additionalProperties: false + $ref: '#/components/responses/playlistNotFound' playlistNotFound: description: The specified Playlist does not exist. content: From afe4933f95feb7ca972a7455bc9ff50cb46cf2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Wed, 15 Nov 2023 16:18:39 +0100 Subject: [PATCH 008/291] fix: missing property prevents build --- packages/openapi/api/definitions/ingest.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index a794e9ae337..f117e966a49 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -111,7 +111,8 @@ resources: required: true content: application/json: - $ref: '#/components/schemas/ingestPlaylist' + schema: + $ref: '#/components/schemas/ingestPlaylist' responses: 200: description: Playlist has been updated. From a9a052e9835af583b9403f3761f48a7613b45c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Wed, 15 Nov 2023 16:29:55 +0100 Subject: [PATCH 009/291] feat: changes ingest/playlists response Lists internal playlistId and also lists all Rundowns with their externalIds --- packages/openapi/api/definitions/ingest.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index f117e966a49..d0d463e71a7 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -9,7 +9,7 @@ resources: summary: Gets ingest data for all Playlists in Sofie. responses: 200: - description: Command successfully handled - returns an array of Playlist Ids. + description: Command successfully handled - returns an array of Playlists with their playlistIds and list of Rundow. content: application/json: schema: @@ -1177,11 +1177,17 @@ components: ingestPlaylistItem: type: object properties: - externalId: + playlistId: type: string - description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Playlist. + description: The Id provided by Sofie. This Id will be used for /playlist commands for controlling playlist activations, playback etc. + rundowns: + type: array + description: All rundowns in a Playlist. + items: + $ref: '#/components/schemas/ingestRundownItem' required: - - externalId + - playlistId + - rundowns additionalProperties: false ingestRundownItem: type: object From 8b888c9744bfabcb0d3023802e7ca906de07f157 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Wed, 15 Nov 2023 16:30:59 +0100 Subject: [PATCH 010/291] feat: ingest/playlists updated example Moves example to the referenced item for better safety when updating in the future --- packages/openapi/api/definitions/ingest.yaml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index d0d463e71a7..c4ec0eabfb0 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -23,9 +23,6 @@ resources: type: array items: $ref: '#/components/schemas/ingestPlaylistItem' - example: - - externalId: 'playlist1' - - externalId: 'playlist2' required: - status - playlists @@ -1189,6 +1186,16 @@ components: - playlistId - rundowns additionalProperties: false + example: + - playlistId: 'playlist1' + rundowns: + - externalId: 'playlist1Rundown1' + - externalId: 'playlist1Rundown2' + - playlistId: 'playlist2' + rundowns: + - externalId: 'playlist2Rundown1' + - externalId: 'playlist2Rundown2' + - externalId: 'playlist2Rundown3' ingestRundownItem: type: object properties: From 0273b09e3be9515df9c3489196ba16f1b90d9a0f Mon Sep 17 00:00:00 2001 From: romain-garcia-rodriguez Date: Mon, 2 Sep 2024 11:52:08 +0200 Subject: [PATCH 011/291] test: create tests for ingest api --- packages/openapi/api/definitions/ingest.yaml | 94 +----- packages/openapi/src/__tests__/ingest.spec.ts | 291 ++++++++++++++++++ 2 files changed, 301 insertions(+), 84 deletions(-) create mode 100644 packages/openapi/src/__tests__/ingest.spec.ts diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index c4ec0eabfb0..f30619627c7 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -23,10 +23,16 @@ resources: type: array items: $ref: '#/components/schemas/ingestPlaylistItem' - required: - - status - - playlists - additionalProperties: false + example: + - playlistId: 'playlist1' + rundowns: + - externalId: 'playlist1Rundown1' + - externalId: 'playlist1Rundown2' + - playlistId: 'playlist2' + rundowns: + - externalId: 'playlist2Rundown1' + - externalId: 'playlist2Rundown2' + - externalId: 'playlist2Rundown3' ingestPlaylist: get: operationId: getIngestPlaylist @@ -71,76 +77,6 @@ resources: $ref: '#/components/responses/idNotFound' # oneOf: # - $ref: '#/components/responses/playlistNotFound' - put: - operationId: putIngestPlaylist - tags: - - ingest - summary: Creates a new or updates an existing Playlist. - parameters: - - name: playlistId - in: path - description: Playlist to create/update. - required: true - schema: - type: string - - name: If-None-Match - in: header - schema: - type: array - items: - type: string - description: If specified, the Playlist will only be updated if one of the specified ETags does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. - - name: If-Match - in: header - schema: - type: array - items: - type: string - description: If specified, the Playlist will only be updated if one of the specified ETags matches. - - name: ETag - in: header - required: true - schema: - type: string - description: ETag to use as version information for Playlist. - requestBody: - description: Contains the Playlist data. - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/ingestPlaylist' - responses: - 200: - description: Playlist has been updated. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false - 201: - description: Playlist has been created. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 201 - example: 201 - required: - - status - additionalProperties: false - 404: - $ref: '#/components/responses/idNotFound' delete: operationId: deleteIngestPlaylist tags: @@ -1186,16 +1122,6 @@ components: - playlistId - rundowns additionalProperties: false - example: - - playlistId: 'playlist1' - rundowns: - - externalId: 'playlist1Rundown1' - - externalId: 'playlist1Rundown2' - - playlistId: 'playlist2' - rundowns: - - externalId: 'playlist2Rundown1' - - externalId: 'playlist2Rundown2' - - externalId: 'playlist2Rundown3' ingestRundownItem: type: object properties: diff --git a/packages/openapi/src/__tests__/ingest.spec.ts b/packages/openapi/src/__tests__/ingest.spec.ts new file mode 100644 index 00000000000..fd7f57051ec --- /dev/null +++ b/packages/openapi/src/__tests__/ingest.spec.ts @@ -0,0 +1,291 @@ +// eslint-disable-next-line node/no-missing-import +import { Configuration, IngestApi, IngestPart, IngestRundown, IngestSegment } from '../../client/ts' +import { checkServer } from '../checkServer' +import Logging from '../httpLogging' + +const httpLogging = false +// let testServer = false +// if (process.env.SERVER_TYPE === 'TEST') { +// testServer = true +// } + +describe('Network client', () => { + const config = new Configuration({ + basePath: process.env.SERVER_URL, + middleware: [new Logging(httpLogging)], + }) + + beforeAll(async () => await checkServer(config)) + + const ingestApi = new IngestApi(config) + + /** + * INGEST PLAYLIST + */ + const playlistIds: string[] = [] + test('Can request all ingest playlists in Sofie', async () => { + const ingestPlaylists = await ingestApi.getIngestPlaylists() + expect(ingestPlaylists.status).toBe(200) + expect(ingestPlaylists).toHaveProperty('playlists') + + expect(ingestPlaylists.playlists.length).toBeGreaterThanOrEqual(1) + ingestPlaylists.playlists.forEach((playlist) => { + expect(typeof playlist).toBe('object') + expect(typeof playlist.playlistId).toBe('string') + playlistIds.push(playlist.playlistId) + }) + }) + + test('Can request a playlist by id in Sofie', async () => { + const ingestPlaylist = await ingestApi.getIngestPlaylist({ + playlistId: playlistIds[0], + }) + expect(ingestPlaylist.status).toBe(200) + expect(ingestPlaylist).toHaveProperty('playlist') + + expect(ingestPlaylist.playlist).toHaveProperty('name') + expect(typeof ingestPlaylist.playlist.name).toBe('string') + }) + + /** + * INGEST RUNDOWS + */ + const rundownIds: string[] = [] + test('Can request all ingest rundowns in Sofie', async () => { + const ingestRundowns = await ingestApi.getIngestRundowns({ + playlistId: playlistIds[0], + }) + expect(ingestRundowns.status).toBe(200) + expect(ingestRundowns).toHaveProperty('rundowns') + + expect(ingestRundowns.rundowns.length).toBeGreaterThanOrEqual(1) + + ingestRundowns.rundowns.forEach((rundown) => { + expect(typeof rundown).toBe('object') + expect(typeof rundown.externalId).toBe('string') + rundownIds.push(rundown.externalId) + }) + }) + + let newIngestRundown: IngestRundown | undefined + test('Can request ingest rundown by id in Sofie', async () => { + const ingestRundown = await ingestApi.getIngestRundown({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + }) + expect(ingestRundown.status).toBe(200) + expect(ingestRundown).toHaveProperty('rundown') + + expect(ingestRundown.rundown).toHaveProperty('name') + expect(ingestRundown.rundown).toHaveProperty('rank') + expect(ingestRundown.rundown).toHaveProperty('source') + expect(typeof ingestRundown.rundown.name).toBe('string') + expect(typeof ingestRundown.rundown.rank).toBe('number') + expect(typeof ingestRundown.rundown.source).toBe('string') + newIngestRundown = JSON.parse(JSON.stringify(ingestRundown.rundown)) + }) + + test('Can add/update multiple rundowns in Sofie', async () => { + newIngestRundown.name = newIngestRundown.name + 'added' + newIngestRundown.rank = 2 + const ingestRundown = await ingestApi.putIngestRundowns({ + playlistId: playlistIds[0], + putIngestRundownsRequest: { + rundowns: [ + { + name: 'rundown1', + source: 'Our Company - Some Product Name', + rank: 0, + }, + { + name: 'rundown2', + source: 'Our Second Company - Some Product Name', + rank: 1, + }, + ], + }, + }) + expect(ingestRundown.status).toBe(200) + }) + + const testIngestRundownId = 'rundown3' + test('Can add/update an ingest rundown in Sofie', async () => { + const newPutIngestRundown = await ingestApi.putIngestRundown({ + playlistId: playlistIds[0], + rundownId: testIngestRundownId, + ingestRundown: { + name: 'rundown3', + source: 'Our Company - Some Product Name', + rank: 3, + }, + eTag: '1725268817', + }) + expect(newPutIngestRundown.status).toBe(200) + }) + + test('Can delete ingest rundown by id in Sofie', async () => { + const ingestRundown = await ingestApi.deleteIngestRundown({ + playlistId: playlistIds[0], + rundownId: testIngestRundownId, + }) + expect(ingestRundown.status).toBe(200) + }) + + /** + * INGEST SEGMENT + */ + const segmentIds: string[] = [] + test('Can request all ingest segments in Sofie', async () => { + const ingestSegments = await ingestApi.getIngestSegments({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + }) + expect(ingestSegments.status).toBe(200) + expect(ingestSegments).toHaveProperty('segments') + + expect(ingestSegments.segments.length).toBeGreaterThanOrEqual(1) + + ingestSegments.segments.forEach((segment) => { + expect(typeof segment).toBe('object') + expect(typeof segment.externalId).toBe('string') + segmentIds.push(segment.externalId) + }) + }) + + let newIngestSegment: IngestSegment | undefined + test('Can request ingest segment by id in Sofie', async () => { + const ingestSegment = await ingestApi.getIngestSegment({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + }) + expect(ingestSegment.status).toBe(200) + expect(ingestSegment).toHaveProperty('segment') + + expect(ingestSegment.segment).toHaveProperty('name') + expect(ingestSegment.segment).toHaveProperty('rank') + expect(typeof ingestSegment.segment.name).toBe('string') + expect(typeof ingestSegment.segment.rank).toBe('number') + newIngestSegment = JSON.parse(JSON.stringify(ingestSegment.segment)) + }) + + test('can add/update multiple ingest segments in Sofie', async () => { + const ingestSegment = await ingestApi.putIngestSegments({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + putIngestSegmentsRequest: { + segments: [ + { + name: 'segment1', + rank: 0, + }, + ], + }, + }) + expect(ingestSegment.status).toBe(200) + }) + + const testIngestSegmentId = 'segment2' + test('Can add/update an ingest segment in Sofie', async () => { + newIngestSegment.name = newIngestSegment.name + 'Added' + const ingestSegment = await ingestApi.putIngestSegment({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: testIngestSegmentId, + eTag: '1725269223', + ingestSegment: newIngestSegment, + }) + expect(ingestSegment.status).toBe(200) + }) + + test('Can delete ingest segment by id in Sofie', async () => { + const ingestRundown = await ingestApi.deleteIngestSegment({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: testIngestSegmentId, + }) + expect(ingestRundown.status).toBe(200) + }) + + /** + * INGEST PARTS + */ + const partIds: string[] = [] + test('Can request all ingest parts in Sofie', async () => { + const ingestParts = await ingestApi.getIngestParts({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + }) + + expect(ingestParts.status).toBe(200) + expect(ingestParts).toHaveProperty('parts') + + expect(ingestParts.parts.length).toBeGreaterThanOrEqual(1) + + ingestParts.parts.forEach((part) => { + expect(typeof part).toBe('object') + expect(typeof part.externalId).toBe('string') + partIds.push(part.externalId) + }) + }) + + let newIngestPart: IngestPart | undefined + test('Can request ingest part by id in Sofie', async () => { + const ingestPart = await ingestApi.getIngestPart({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + partId: partIds[0], + }) + expect(ingestPart.status).toBe(200) + expect(ingestPart).toHaveProperty('part') + + expect(ingestPart.part).toHaveProperty('name') + expect(ingestPart.part).toHaveProperty('rank') + expect(typeof ingestPart.part.name).toBe('string') + expect(typeof ingestPart.part.rank).toBe('number') + newIngestPart = JSON.parse(JSON.stringify(ingestPart.part)) + }) + + test('Can add/update multiple ingest parts in Sofie', async () => { + const ingestPart = await ingestApi.putIngestParts({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + putIngestPartsRequest: { + parts: [ + { + name: 'part1', + rank: 0, + }, + ], + }, + }) + expect(ingestPart.status).toBe(200) + }) + + const testIngestPartId = 'part2' + test('Can add/update an ingest part in Sofie', async () => { + newIngestPart.name = newIngestPart.name + 'Added' + const ingestPart = await ingestApi.putIngestPart({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + partId: testIngestPartId, + eTag: '1725269417', + ingestPart: newIngestPart, + }) + expect(ingestPart.status).toBe(200) + }) + + test('Can delete ingest part by id in Sofie', async () => { + const ingestRundown = await ingestApi.deleteIngestPart({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + partId: testIngestPartId, + }) + expect(ingestRundown.status).toBe(200) + }) +}) From 98fe5c898f74f0581cdf1a2fcfc32bf4f49a774d Mon Sep 17 00:00:00 2001 From: romain-garcia-rodriguez Date: Wed, 4 Sep 2024 10:45:54 +0200 Subject: [PATCH 012/291] test: add missing delete for ingest api --- packages/openapi/api/definitions/ingest.yaml | 119 ++++++++++++++++++ packages/openapi/src/__tests__/ingest.spec.ts | 40 +++++- 2 files changed, 155 insertions(+), 4 deletions(-) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index f30619627c7..611a74ee552 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -33,6 +33,26 @@ resources: - externalId: 'playlist2Rundown1' - externalId: 'playlist2Rundown2' - externalId: 'playlist2Rundown3' + delete: + operationId: deleteIngestPlaylists + tags: + - ingest + summary: Delete multiple playlists. + responses: + 200: + description: Playlists removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false ingestPlaylist: get: operationId: getIngestPlaylist @@ -204,6 +224,33 @@ resources: $ref: '#/components/responses/idNotFound' # oneOf: # - $ref: '#/components/responses/playlistNotFound' + delete: + operationId: deleteIngestRundowns + tags: + - ingest + summary: Delete multiple rundowns. + parameters: + - name: playlistId + in: path + description: Playlist the ingest Part belongs to. + required: true + schema: + type: string + responses: + 200: + description: Rundown removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false ingestRundown: get: operationId: getIngestRundown @@ -486,6 +533,39 @@ resources: # oneOf: # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' + delete: + operationId: deleteIngestSegments + tags: + - ingest + summary: Delete multiple segments. + parameters: + - name: playlistId + in: path + description: Playlist the ingest Part belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown the ingest Part belongs to. + required: true + schema: + type: string + responses: + 200: + description: Segments removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false ingestSegment: get: operationId: getIngestSegment @@ -797,6 +877,45 @@ resources: # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/partNotFound' + delete: + operationId: deleteIngestParts + tags: + - ingest + summary: Delete multiple Parts. + parameters: + - name: playlistId + in: path + description: Playlist the ingest Part belongs to. + required: true + schema: + type: string + - name: rundownId + in: path + description: Rundown the ingest Part belongs to. + required: true + schema: + type: string + - name: segmentId + in: path + description: Segment the ingest Part belongs to. + required: true + schema: + type: string + responses: + 200: + description: Parts removed. + content: + application/json: + schema: + type: object + properties: + status: + type: number + const: 200 + example: 200 + required: + - status + additionalProperties: false ingestPart: get: operationId: getIngestPart diff --git a/packages/openapi/src/__tests__/ingest.spec.ts b/packages/openapi/src/__tests__/ingest.spec.ts index fd7f57051ec..af2492897f7 100644 --- a/packages/openapi/src/__tests__/ingest.spec.ts +++ b/packages/openapi/src/__tests__/ingest.spec.ts @@ -4,10 +4,6 @@ import { checkServer } from '../checkServer' import Logging from '../httpLogging' const httpLogging = false -// let testServer = false -// if (process.env.SERVER_TYPE === 'TEST') { -// testServer = true -// } describe('Network client', () => { const config = new Configuration({ @@ -47,6 +43,18 @@ describe('Network client', () => { expect(typeof ingestPlaylist.playlist.name).toBe('string') }) + test('Can delete multiple ingest playlists in Sofie', async () => { + const ingestRundown = await ingestApi.deleteIngestPlaylists() + expect(ingestRundown.status).toBe(200) + }) + + test('Can delete ingest playlist by id in Sofie', async () => { + const ingestRundown = await ingestApi.deleteIngestPlaylist({ + playlistId: playlistIds[0], + }) + expect(ingestRundown.status).toBe(200) + }) + /** * INGEST RUNDOWS */ @@ -123,6 +131,13 @@ describe('Network client', () => { expect(newPutIngestRundown.status).toBe(200) }) + test('Can delete multiple ingest rundowns in Sofie', async () => { + const ingestRundown = await ingestApi.deleteIngestRundowns({ + playlistId: playlistIds[0], + }) + expect(ingestRundown.status).toBe(200) + }) + test('Can delete ingest rundown by id in Sofie', async () => { const ingestRundown = await ingestApi.deleteIngestRundown({ playlistId: playlistIds[0], @@ -198,6 +213,14 @@ describe('Network client', () => { expect(ingestSegment.status).toBe(200) }) + test('Can delete multiple ingest segments in Sofie', async () => { + const ingestRundown = await ingestApi.deleteIngestSegments({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + }) + expect(ingestRundown.status).toBe(200) + }) + test('Can delete ingest segment by id in Sofie', async () => { const ingestRundown = await ingestApi.deleteIngestSegment({ playlistId: playlistIds[0], @@ -279,6 +302,15 @@ describe('Network client', () => { expect(ingestPart.status).toBe(200) }) + test('Can delete multiple ingest parts in Sofie', async () => { + const ingestRundown = await ingestApi.deleteIngestParts({ + playlistId: playlistIds[0], + rundownId: rundownIds[0], + segmentId: segmentIds[0], + }) + expect(ingestRundown.status).toBe(200) + }) + test('Can delete ingest part by id in Sofie', async () => { const ingestRundown = await ingestApi.deleteIngestPart({ playlistId: playlistIds[0], From a7c1ecb0abc5f9cb8e3fad060ada2365cc4504b1 Mon Sep 17 00:00:00 2001 From: romain-garcia-rodriguez Date: Wed, 4 Sep 2024 14:39:11 +0200 Subject: [PATCH 013/291] test: fix array header in api definition --- packages/openapi/api/definitions/ingest.yaml | 10 ++++++++++ packages/openapi/src/__tests__/ingest.spec.ts | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index 611a74ee552..35b5164bd91 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -181,6 +181,7 @@ resources: type: string - name: If-None-Match in: header + style: simple schema: type: array items: @@ -323,6 +324,7 @@ resources: type: string - name: If-None-Match in: header + style: simple schema: type: array items: @@ -330,6 +332,7 @@ resources: description: If specified, the Rundown will only be updated if one of the specified ETags does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. - name: If-Match in: header + style: simple schema: type: array items: @@ -340,6 +343,7 @@ resources: required: true schema: type: string + example: '123456789' description: ETag to use as version information for Rundown. requestBody: description: Contains the Rundown data. @@ -490,6 +494,7 @@ resources: type: string - name: If-None-Match in: header + style: simple schema: type: array items: @@ -649,6 +654,7 @@ resources: type: string - name: If-None-Match in: header + style: simple schema: type: array items: @@ -656,6 +662,7 @@ resources: description: If specified, the Segment will only be updated if one of the specified ETags does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. - name: If-Match in: header + style: simple schema: type: array items: @@ -834,6 +841,7 @@ resources: type: string - name: If-None-Match in: header + style: simple schema: type: array items: @@ -1011,6 +1019,7 @@ resources: type: string - name: If-None-Match in: header + style: simple schema: type: array items: @@ -1018,6 +1027,7 @@ resources: description: If specified, the Part will only be updated if the specified ETag does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. - name: If-Match in: header + style: simple schema: type: array items: diff --git a/packages/openapi/src/__tests__/ingest.spec.ts b/packages/openapi/src/__tests__/ingest.spec.ts index af2492897f7..b2f7b0a1c8c 100644 --- a/packages/openapi/src/__tests__/ingest.spec.ts +++ b/packages/openapi/src/__tests__/ingest.spec.ts @@ -126,7 +126,8 @@ describe('Network client', () => { source: 'Our Company - Some Product Name', rank: 3, }, - eTag: '1725268817', + eTag: '123456789', + ifNoneMatch: ['123456789', '1725453459'], }) expect(newPutIngestRundown.status).toBe(200) }) From 3ee218328c87572f233d8ed42aff21297f5f039c Mon Sep 17 00:00:00 2001 From: romain-garcia-rodriguez Date: Wed, 4 Sep 2024 14:41:42 +0200 Subject: [PATCH 014/291] test: add comments in api definition --- packages/openapi/api/definitions/ingest.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index 35b5164bd91..62e905f8f79 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -181,6 +181,7 @@ resources: type: string - name: If-None-Match in: header + # Indicates that the array elements are serialized as a comma-separated list in the header. style: simple schema: type: array @@ -324,6 +325,7 @@ resources: type: string - name: If-None-Match in: header + # Indicates that the array elements are serialized as a comma-separated list in the header. style: simple schema: type: array @@ -332,6 +334,7 @@ resources: description: If specified, the Rundown will only be updated if one of the specified ETags does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. - name: If-Match in: header + # Indicates that the array elements are serialized as a comma-separated list in the header. style: simple schema: type: array @@ -494,6 +497,7 @@ resources: type: string - name: If-None-Match in: header + # Indicates that the array elements are serialized as a comma-separated list in the header. style: simple schema: type: array @@ -654,6 +658,7 @@ resources: type: string - name: If-None-Match in: header + # Indicates that the array elements are serialized as a comma-separated list in the header. style: simple schema: type: array @@ -662,6 +667,7 @@ resources: description: If specified, the Segment will only be updated if one of the specified ETags does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. - name: If-Match in: header + # Indicates that the array elements are serialized as a comma-separated list in the header. style: simple schema: type: array @@ -841,6 +847,7 @@ resources: type: string - name: If-None-Match in: header + # Indicates that the array elements are serialized as a comma-separated list in the header. style: simple schema: type: array @@ -1019,6 +1026,7 @@ resources: type: string - name: If-None-Match in: header + # Indicates that the array elements are serialized as a comma-separated list in the header. style: simple schema: type: array @@ -1027,6 +1035,7 @@ resources: description: If specified, the Part will only be updated if the specified ETag does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. - name: If-Match in: header + # Indicates that the array elements are serialized as a comma-separated list in the header. style: simple schema: type: array From 04139bc0eaefe9da68b005f868373d0b9ee6abf8 Mon Sep 17 00:00:00 2001 From: romain-garcia-rodriguez Date: Wed, 4 Sep 2024 14:43:29 +0200 Subject: [PATCH 015/291] test: fix devices tests --- packages/openapi/src/__tests__/devices.spec.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/openapi/src/__tests__/devices.spec.ts b/packages/openapi/src/__tests__/devices.spec.ts index 788bee3fbb7..3560cc2fd07 100644 --- a/packages/openapi/src/__tests__/devices.spec.ts +++ b/packages/openapi/src/__tests__/devices.spec.ts @@ -22,11 +22,15 @@ describe('Network client', () => { const devices = await devicesApi.devices() expect(devices.status).toBe(200) expect(devices).toHaveProperty('result') - devices.result.forEach((device) => { - expect(typeof device).toBe('object') - expect(device).toHaveProperty('id') - expect(typeof device.id).toBe('string') - deviceIds.push(device.id) + expect(devices.result).toHaveProperty('ingest') + expect(devices.result).toHaveProperty('liveStatus') + expect(devices.result).toHaveProperty('mediaManager') + expect(devices.result).toHaveProperty('packageManager') + expect(devices.result).toHaveProperty('playout') + expect(devices.result).toHaveProperty('triggerInput') + devices.result.playout.forEach((device) => { + expect(typeof device).toBe('string') + deviceIds.push(device) }) }) From 410927bde42de27cf9864123395135007e9d1c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Wed, 4 Sep 2024 15:08:18 +0200 Subject: [PATCH 016/291] feat: implement ingest API --- meteor/.meteor/packages | 1 + meteor/__mocks__/_setupMocks.ts | 1 + meteor/server/api/ingest/actions.ts | 24 + meteor/server/api/rest/v1/index.ts | 2 + meteor/server/api/rest/v1/ingest.ts | 1695 +++++++++++++++++ meteor/server/api/rest/v1/playlists.ts | 26 +- meteor/server/api/rest/v1/typeConversion.ts | 17 + meteor/server/lib/rest/v1/ingest.ts | 273 +++ meteor/server/security/check.ts | 19 +- .../blueprints-integration/src/api/studio.ts | 3 + packages/corelib/src/dataModel/Rundown.ts | 12 +- .../ingest/MutableIngestPartImpl.ts | 8 + .../ingest/MutableIngestRundownImpl.ts | 2 + .../ingest/MutableIngestSegmentImpl.ts | 10 + .../MutableIngestRundownImpl.spec.ts | 42 + .../MutableIngestSegmentImpl.spec.ts | 14 + .../job-worker/src/ingest/runOperation.ts | 2 + packages/job-worker/src/playout/lock.ts | 8 +- .../implementation/PlayoutRundownModelImpl.ts | 5 +- .../implementation/PlayoutSegmentModelImpl.ts | 3 +- packages/openapi/api/actions.yaml | 32 +- packages/openapi/api/definitions/ingest.yaml | 1366 +++++++------ .../openapi/src/__tests__/devices.spec.ts | 14 +- packages/openapi/src/__tests__/ingest.spec.ts | 500 +++-- .../shared-lib/src/peripheralDevice/ingest.ts | 31 +- 25 files changed, 3180 insertions(+), 930 deletions(-) create mode 100644 meteor/server/api/rest/v1/ingest.ts create mode 100644 meteor/server/lib/rest/v1/ingest.ts diff --git a/meteor/.meteor/packages b/meteor/.meteor/packages index 5e49caae6e3..5a6339c8587 100644 --- a/meteor/.meteor/packages +++ b/meteor/.meteor/packages @@ -20,3 +20,4 @@ typescript@5.6.6 # Enable TypeScript syntax in .ts and .tsx modules tracker@1.3.4 # Meteor's client-side reactive programming library zodern:types +fetch diff --git a/meteor/__mocks__/_setupMocks.ts b/meteor/__mocks__/_setupMocks.ts index 8cf580f95da..77ca346ddb3 100644 --- a/meteor/__mocks__/_setupMocks.ts +++ b/meteor/__mocks__/_setupMocks.ts @@ -10,6 +10,7 @@ jest.mock('nanoid', (...args) => require('./random').setup(args), { virtual: tru // Add references to all "meteor" mocks below, so that jest resolves the imports properly. +jest.mock('meteor/fetch', () => null, { virtual: true }) jest.mock('meteor/meteor', (...args) => require('./meteor').setup(args), { virtual: true }) jest.mock('meteor/random', (...args) => require('./random').setup(args), { virtual: true }) jest.mock('meteor/check', (...args) => require('./check').setup(args), { virtual: true }) diff --git a/meteor/server/api/ingest/actions.ts b/meteor/server/api/ingest/actions.ts index 6a6cf852bf3..ae3f1a6daaf 100644 --- a/meteor/server/api/ingest/actions.ts +++ b/meteor/server/api/ingest/actions.ts @@ -7,6 +7,8 @@ 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 { fetch } from 'meteor/fetch' +import { logger } from '../../logging' /* This file contains actions that can be performed on an ingest-device @@ -27,6 +29,28 @@ export namespace IngestActions { return TriggerReloadDataResponse.COMPLETED } + case 'httpIngest': { + const resyncUrl = rundown.source.resyncUrl + fetch(resyncUrl, { method: 'POST' }) + .then(() => { + logger.info(`Reload rundown: resync request sent to "${resyncUrl}"`) + }) + .catch((error) => { + if (error.errno === 'ECONNREFUSED') { + logger.error( + `Reload rundown: could not establish connection with "${resyncUrl}" (ECONNREFUSED)` + ) + return + } + logger.error( + `Reload rundown: error occured while sending resync request to "${resyncUrl}", error: "${JSON.stringify( + error + )}"` + ) + }) + + return TriggerReloadDataResponse.WORKING + } case 'testing': { await runIngestOperation(rundown.studioId, IngestJobs.CreateAdlibTestingRundownForShowStyleVariant, { showStyleVariantId: rundown.showStyleVariantId, diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index 87e8bdf1cf7..fb1cb71a552 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -19,6 +19,7 @@ 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' @@ -296,3 +297,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..8d936c96d9f --- /dev/null +++ b/meteor/server/api/rest/v1/ingest.ts @@ -0,0 +1,1695 @@ +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 { Meteor } from 'meteor/meteor' +import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' +import { + HttpIngestRundown, + IngestRestAPI, + PartResponse, + PlaylistResponse, + RundownResponse, + SegmentResponse, +} from '../../../lib/rest/v1/ingest' +import { check } from '../../../lib/check' +import { Parts, RundownPlaylists, Rundowns, Segments, Studios } from '../../../collections' +import { logger } from '../../../logging' +import { runIngestOperation } from '../../ingest/lib' +import { validateAPIPartPayload } from './typeConversion' +import { APIFactory, APIRegisterHook, ServerAPIContext } from './types' + +class IngestServerAPI implements IngestRestAPI { + private async validateAPIPartPayloadForRundown( + blueprintId: BlueprintId | undefined, + ingestRundown: IngestRundown, + indexes?: { + rundown?: number + } + ) { + return Promise.all( + ingestRundown.segments.map(async (segment, index) => { + return this.validateAPIPartPayloadForSegment(blueprintId, segment, { + ...indexes, + segment: index, + }) + }) + ) + } + + private async validateAPIPartPayloadForSegment( + blueprintId: BlueprintId | undefined, + segment: IngestRundown['segments'][number], + indexes?: { + rundown?: number + segment?: number + } + ) { + return Promise.all( + segment.parts.map(async (part, index) => { + return this.validateAPIPartPayloadForPart(blueprintId, part, { ...indexes, part: index }) + }) + ) + } + + private async validateAPIPartPayloadForPart( + blueprintId: BlueprintId | undefined, + part: IngestRundown['segments'][number]['parts'][number], + indexes?: { + rundown?: number + segment?: number + part?: number + } + ) { + const validationResult = await validateAPIPartPayload(blueprintId, part.payload) + if (validationResult && validationResult.length > 0) { + const parts = [] + if (indexes?.rundown !== undefined) parts.push(`rundowns[${indexes.rundown}]`) + if (indexes?.segment !== undefined) parts.push(`segments[${indexes.segment}]`) + if (indexes?.part !== undefined) parts.push(`parts[${indexes.part}]`) + let msg = `Part payload validation failed` + if (parts.length > 0) msg += ` for ${parts.join('.')}` + + logger.error(`${msg} with errors: ${validationResult}`) + throw new Meteor.Error(409, msg, JSON.stringify(validationResult)) + } + } + + private validateRundown(ingestRundown: HttpIngestRundown) { + check(ingestRundown, Object) + check(ingestRundown.externalId, String) + check(ingestRundown.name, String) + check(ingestRundown.type, String) + check(ingestRundown.segments, Array) + check(ingestRundown.resyncUrl, String) + + check(ingestRundown.timing, Object) + check(ingestRundown.timing?.type, String) + + if (ingestRundown.timing?.type === 'forward-time') { + check(ingestRundown.timing.expectedStart, Number) + } else if (ingestRundown.timing?.type === 'back-time') { + check(ingestRundown.timing?.expectedEnd, Number) + } + + 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) + + if (ingestSegment.isHidden !== undefined) check(ingestSegment.isHidden, Boolean) + if (ingestSegment.timing !== undefined) { + check(ingestSegment.timing.expectedStart, Number) + check(ingestSegment.timing.expectedEnd, Number) + } + + 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) + + if (ingestPart.float !== undefined) check(ingestPart.float, Boolean) + if (ingestPart.autoNext !== undefined) check(ingestPart.autoNext, Boolean) + } + + 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 !== 'httpIngest') { + throw new Meteor.Error( + 403, + `Cannot replace existing rundown from source '${getRundownNrcsName( + rundown + )}' with new data from 'httpIngest' 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: HttpIngestRundown + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(ingestRundown, Object) + + const studio = await this.findStudio(studioId) + + this.validateRundown(ingestRundown) + await this.validateAPIPartPayloadForRundown(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: 'httpIngest', + resyncUrl: ingestRundown.resyncUrl, + }, + }) + + return ClientAPI.responseSuccess(undefined) + } + + async putRundowns( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + ingestRundowns: HttpIngestRundown[] + ): 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.validateAPIPartPayloadForRundown(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: 'httpIngest', + resyncUrl: ingestRundown.resyncUrl, + }, + }) + }) + ) + + return ClientAPI.responseSuccess(undefined) + } + + async putRundown( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + ingestRundown: HttpIngestRundown + ): Promise> { + check(studioId, String) + check(playlistId, String) + check(rundownId, String) + check(ingestRundown, Object) + + const studio = await this.findStudio(studioId) + + this.validateRundown(ingestRundown) + await this.validateAPIPartPayloadForRundown(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: 'httpIngest', + 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.validateAPIPartPayloadForSegment(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.validateAPIPartPayloadForSegment(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.validateAPIPartPayloadForSegment(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.validateAPIPartPayloadForPart(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.validateAPIPartPayloadForPart(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.validateAPIPartPayloadForPart(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 HttpIngestRundown + 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 HttpIngestRundown[] + 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 HttpIngestRundown + 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..520b8b096ae 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -126,9 +126,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( @@ -169,9 +172,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( @@ -459,7 +465,9 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { fromPartInstanceId: PartInstanceId | undefined ): Promise> { triggerWriteAccess() - const rundownPlaylist = await RundownPlaylists.findOneAsync(rundownPlaylistId) + const rundownPlaylist = await RundownPlaylists.findOneAsync({ + $or: [{ _id: rundownPlaylistId }, { externalId: rundownPlaylistId }], + }) if (!rundownPlaylist) throw new Error(`Rundown playlist ${rundownPlaylistId} does not exist`) return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( @@ -484,7 +492,9 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { rundownPlaylistId: RundownPlaylistId, sourceLayerIds: string[] ): Promise> { - const rundownPlaylist = await RundownPlaylists.findOneAsync(rundownPlaylistId) + const rundownPlaylist = await RundownPlaylists.findOneAsync({ + $or: [{ _id: rundownPlaylistId }, { externalId: rundownPlaylistId }], + }) if (!rundownPlaylist) return ClientAPI.responseError( UserError.from( diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index be9332e14ad..0e919fcbfc3 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -726,3 +726,20 @@ export function playlistSnapshotOptionsFrom(options: APIPlaylistSnapshotOptions) withTimeline: !!options.withTimeline, } } + +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/lib/rest/v1/ingest.ts b/meteor/server/lib/rest/v1/ingest.ts new file mode 100644 index 00000000000..b604303c388 --- /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: HttpIngestRundown + ): Promise> + + putRundowns( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + ingestRundowns: HttpIngestRundown[] + ): Promise> + + putRundown( + _connection: Meteor.Connection, + _event: string, + studioId: StudioId, + playlistId: string, + rundownId: string, + ingestRundown: HttpIngestRundown + ): 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 HttpIngestRundown = 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/security/check.ts b/meteor/server/security/check.ts index da6d38ad1de..7c43da165ac 100644 --- a/meteor/server/security/check.ts +++ b/meteor/server/security/check.ts @@ -20,14 +20,17 @@ export async function checkAccessToPlaylist( ): Promise { assertConnectionHasOneOfPermissions(cred, 'studio') - const playlist = (await RundownPlaylists.findOneAsync(playlistId, { - projection: { - _id: 1, - studioId: 1, - organizationId: 1, - name: 1, - }, - })) as Pick | undefined + const playlist = (await RundownPlaylists.findOneAsync( + { $or: [{ _id: playlistId }, { externalId: playlistId }] }, + { + projection: { + _id: 1, + studioId: 1, + organizationId: 1, + name: 1, + }, + } + )) as Pick | undefined if (!playlist) throw new Meteor.Error(404, `RundownPlaylist "${playlistId}" not found`) return playlist diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index a2e4c268c65..ebac1053cd0 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -103,6 +103,9 @@ export interface StudioBlueprintManifest 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 diff --git a/packages/corelib/src/dataModel/Rundown.ts b/packages/corelib/src/dataModel/Rundown.ts index 61b1159eb9e..efbf64d71bb 100644 --- a/packages/corelib/src/dataModel/Rundown.ts +++ b/packages/corelib/src/dataModel/Rundown.ts @@ -93,7 +93,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 + | RundownSourceHttpIngest /** A description of the external NRCS source of a Rundown */ export interface RundownSourceNrcs { @@ -119,6 +124,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 RundownSourceHttpIngest { + type: 'httpIngest' + resyncUrl: string +} export function getRundownNrcsName(rundown: ReadonlyDeep> | undefined): string { if (rundown?.source?.type === 'nrcs' && rundown.source.nrcsName) { diff --git a/packages/job-worker/src/blueprints/ingest/MutableIngestPartImpl.ts b/packages/job-worker/src/blueprints/ingest/MutableIngestPartImpl.ts index 068eefddd67..7948a2c4adb 100644 --- a/packages/job-worker/src/blueprints/ingest/MutableIngestPartImpl.ts +++ b/packages/job-worker/src/blueprints/ingest/MutableIngestPartImpl.ts @@ -20,6 +20,14 @@ export class MutableIngestPartImpl implements MutableIng return this.#ingestPart.name } + get float(): boolean { + return this.#ingestPart.float ?? false + } + + get autoNext(): boolean { + return this.#ingestPart.autoNext ?? false + } + get payload(): ReadonlyDeep | undefined { return this.#ingestPart.payload as ReadonlyDeep } diff --git a/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts b/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts index 4a7b286d43e..5c217d4a8a4 100644 --- a/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts +++ b/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts @@ -314,6 +314,8 @@ export class MutableIngestRundownImpl | undefined { return this.#ingestSegment.payload as ReadonlyDeep } @@ -225,6 +233,8 @@ export class MutableIngestSegmentImpl = { externalId: part.externalId, rank, + float: part.float, + autoNext: part.autoNext, name: part.name, payload: part.payload, userEditStates: part.userEditStates, diff --git a/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestRundownImpl.spec.ts b/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestRundownImpl.spec.ts index f24ff4b9cb3..542ce44981b 100644 --- a/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestRundownImpl.spec.ts +++ b/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestRundownImpl.spec.ts @@ -28,6 +28,11 @@ describe('MutableIngestRundownImpl', () => { externalId: 'seg0', name: 'name', rank: 0, + isHidden: false, + timing: { + expectedStart: 0, + expectedEnd: 0, + }, payload: { val: 'first-val', second: 5, @@ -38,6 +43,8 @@ describe('MutableIngestRundownImpl', () => { externalId: 'part0', name: 'my first part', rank: 0, + float: false, + autoNext: false, payload: { val: 'some-val', }, @@ -49,6 +56,11 @@ describe('MutableIngestRundownImpl', () => { externalId: 'seg1', name: 'name 2', rank: 1, + isHidden: false, + timing: { + expectedStart: 0, + expectedEnd: 0, + }, payload: { val: 'next-val', }, @@ -58,6 +70,8 @@ describe('MutableIngestRundownImpl', () => { externalId: 'part1', name: 'my second part', rank: 0, + float: false, + autoNext: false, payload: { val: 'some-val', }, @@ -69,6 +83,11 @@ describe('MutableIngestRundownImpl', () => { externalId: 'seg2', name: 'name 3', rank: 2, + isHidden: false, + timing: { + expectedStart: 0, + expectedEnd: 0, + }, payload: { val: 'last-val', }, @@ -78,6 +97,8 @@ describe('MutableIngestRundownImpl', () => { externalId: 'part2', name: 'my third part', rank: 0, + float: false, + autoNext: false, payload: { val: 'some-val', }, @@ -445,6 +466,11 @@ describe('MutableIngestRundownImpl', () => { const newSegment: Omit = { externalId: 'seg1', name: 'new name', + isHidden: false, + timing: { + expectedStart: 0, + expectedEnd: 0, + }, payload: { val: 'new-val', }, @@ -454,6 +480,8 @@ describe('MutableIngestRundownImpl', () => { externalId: 'part1', name: 'new part name', rank: 0, + float: false, + autoNext: false, payload: { val: 'new-part-val', }, @@ -498,6 +526,11 @@ describe('MutableIngestRundownImpl', () => { const newSegment: Omit = { externalId: 'segX', name: 'new name', + isHidden: false, + timing: { + expectedStart: 0, + expectedEnd: 0, + }, payload: { val: 'new-val', }, @@ -507,6 +540,8 @@ describe('MutableIngestRundownImpl', () => { externalId: 'partX', name: 'new part name', rank: 0, + float: false, + autoNext: false, payload: { val: 'new-part-val', }, @@ -543,6 +578,11 @@ describe('MutableIngestRundownImpl', () => { const newSegment: Omit = { externalId: 'segX', name: 'new name', + isHidden: false, + timing: { + expectedStart: 0, + expectedEnd: 0, + }, payload: { val: 'new-val', }, @@ -551,6 +591,8 @@ describe('MutableIngestRundownImpl', () => { externalId: 'partX', name: 'new part name', rank: 0, + float: false, + autoNext: false, payload: { val: 'new-part-val', }, diff --git a/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestSegmentImpl.spec.ts b/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestSegmentImpl.spec.ts index 5ab56ecb463..a35aa9c6692 100644 --- a/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestSegmentImpl.spec.ts +++ b/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestSegmentImpl.spec.ts @@ -25,6 +25,8 @@ describe('MutableIngestSegmentImpl', () => { externalId: 'part0', name: 'my first part', rank: 0, + float: false, + autoNext: false, payload: { val: 'some-val', }, @@ -34,6 +36,8 @@ describe('MutableIngestSegmentImpl', () => { externalId: 'part1', name: 'another part', rank: 1, + float: false, + autoNext: false, payload: { val: 'second-val', }, @@ -43,6 +47,8 @@ describe('MutableIngestSegmentImpl', () => { externalId: 'part2', name: 'third part', rank: 2, + float: false, + autoNext: false, payload: { val: 'third-val', }, @@ -52,6 +58,8 @@ describe('MutableIngestSegmentImpl', () => { externalId: 'part3', name: 'last part', rank: 3, + float: false, + autoNext: false, payload: { val: 'last-val', }, @@ -329,6 +337,8 @@ describe('MutableIngestSegmentImpl', () => { const newPart: Omit = { externalId: 'part1', name: 'new name', + float: false, + autoNext: false, payload: { val: 'new-val', }, @@ -369,6 +379,8 @@ describe('MutableIngestSegmentImpl', () => { const newPart: Omit = { externalId: 'partX', name: 'new name', + float: false, + autoNext: false, payload: { val: 'new-val', }, @@ -408,6 +420,8 @@ describe('MutableIngestSegmentImpl', () => { const newPart: Omit = { externalId: 'partX', name: 'new name', + float: false, + autoNext: false, payload: { val: 'new-val', }, diff --git a/packages/job-worker/src/ingest/runOperation.ts b/packages/job-worker/src/ingest/runOperation.ts index 86716a7bab2..4f90eb559e2 100644 --- a/packages/job-worker/src/ingest/runOperation.ts +++ b/packages/job-worker/src/ingest/runOperation.ts @@ -275,6 +275,8 @@ async function updateSofieIngestRundown( name: nrcsIngestRundown.name, type: nrcsIngestRundown.type, segments: [], + timing: nrcsIngestRundown.timing, + playlistExternalId: nrcsIngestRundown.playlistExternalId, payload: undefined, userEditStates: {}, rundownSource: nrcsIngestRundown.rundownSource, diff --git a/packages/job-worker/src/playout/lock.ts b/packages/job-worker/src/playout/lock.ts index 0dad5256174..99e9eeba94a 100644 --- a/packages/job-worker/src/playout/lock.ts +++ b/packages/job-worker/src/playout/lock.ts @@ -24,7 +24,9 @@ export async function runJobWithPlayoutModel( // We can lock before checking ownership, as the locks are scoped to the studio return runWithPlaylistLock(context, data.playlistId, async (playlistLock) => { - const playlist = await context.directCollections.RundownPlaylists.findOne(data.playlistId) + const playlist = await context.directCollections.RundownPlaylists.findOne({ + $or: [{ _id: data.playlistId }, { externalId: data.playlistId }], + }) if (!playlist || playlist.studioId !== context.studioId) { throw new Error(`Job playlist "${data.playlistId}" not found or for another studio`) } @@ -48,7 +50,9 @@ export async function runJobWithPlaylistLock( // We can lock before checking ownership, as the locks are scoped to the studio return runWithPlaylistLock(context, data.playlistId, async (lock) => { - const playlist = await context.directCollections.RundownPlaylists.findOne(data.playlistId) + const playlist = await context.directCollections.RundownPlaylists.findOne({ + $or: [{ _id: data.playlistId }, { externalId: data.playlistId }], + }) if (playlist && playlist.studioId !== context.studioId) { throw new Error(`Job playlist "${data.playlistId}" not found or for another studio`) } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts index 4f6b54aef69..1362d4b59ab 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts @@ -9,6 +9,7 @@ import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/erro import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { getRandomId } from '@sofie-automation/corelib/dist/lib' import { PlayoutSegmentModelImpl } from './PlayoutSegmentModelImpl.js' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' export class PlayoutRundownModelImpl implements PlayoutRundownModel { readonly rundown: ReadonlyDeep @@ -47,7 +48,9 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { } getSegment(id: SegmentId): PlayoutSegmentModel | undefined { - return this.segments.find((segment) => segment.segment._id === id) + return this.segments.find( + (segment) => segment.segment._id === id || segment.segment.externalId === unprotectString(id) + ) } getSegmentIds(): SegmentId[] { diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts index 1356244003a..303febcfe11 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts @@ -3,6 +3,7 @@ import { ReadonlyDeep } from 'type-fest' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PlayoutSegmentModel } from '../PlayoutSegmentModel.js' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' export class PlayoutSegmentModelImpl implements PlayoutSegmentModel { readonly #segment: DBSegment @@ -20,7 +21,7 @@ export class PlayoutSegmentModelImpl implements PlayoutSegmentModel { } getPart(id: PartId): ReadonlyDeep | undefined { - return this.parts.find((part) => part._id === id) + return this.parts.find((part) => part._id === id || part.externalId === unprotectString(id)) } getPartIds(): PartId[] { diff --git a/packages/openapi/api/actions.yaml b/packages/openapi/api/actions.yaml index 30e557d6318..ce598e0daa9 100644 --- a/packages/openapi/api/actions.yaml +++ b/packages/openapi/api/actions.yaml @@ -112,19 +112,19 @@ paths: /snapshots: $ref: 'definitions/snapshots.yaml#/resources/snapshots' # ingest operations - /ingest/playlists: - $ref: 'definitions/ingest.yaml#/resources/ingestPlaylists' - /ingest/playlists/{playlistId}: - $ref: 'definitions/ingest.yaml#/resources/ingestPlaylist' - /ingest/playlists/{playlistId}/rundowns: - $ref: 'definitions/ingest.yaml#/resources/ingestRundowns' - /ingest/playlists/{playlistId}/rundowns/{rundownId}: - $ref: 'definitions/ingest.yaml#/resources/ingestRundown' - /ingest/playlists/{playlistId}/rundowns/{rundownId}/segments: - $ref: 'definitions/ingest.yaml#/resources/ingestSegments' - /ingest/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}: - $ref: 'definitions/ingest.yaml#/resources/ingestSegment' - /ingest/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts: - $ref: 'definitions/ingest.yaml#/resources/ingestParts' - /ingest/playlists/{playlistId}/rundowns/{rundownId}/segments/{segmentId}/parts/{partId}: - $ref: 'definitions/ingest.yaml#/resources/ingestPart' + /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 index 62e905f8f79..4edec79274a 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -1,143 +1,111 @@ title: ingest description: Ingest methods resources: - ingestPlaylists: + playlists: get: - operationId: getIngestPlaylists + operationId: getPlaylists + summary: Gets all Playlists. tags: - ingest - summary: Gets ingest data for all Playlists in Sofie. + 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 with their playlistIds and list of Rundow. + description: Command successfully handled - returns an array of Playlists. content: application/json: schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - playlists: - type: array - items: - $ref: '#/components/schemas/ingestPlaylistItem' - example: - - playlistId: 'playlist1' - rundowns: - - externalId: 'playlist1Rundown1' - - externalId: 'playlist1Rundown2' - - playlistId: 'playlist2' - rundowns: - - externalId: 'playlist2Rundown1' - - externalId: 'playlist2Rundown2' - - externalId: 'playlist2Rundown3' + type: array + items: + $ref: '#/components/schemas/playlistResponse' delete: - operationId: deleteIngestPlaylists + operationId: deletePlaylists tags: - ingest - summary: Delete multiple playlists. + 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: - 200: - description: Playlists removed. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false - ingestPlaylist: + 202: + description: Request for deleting accepted. + playlist: get: - operationId: getIngestPlaylist + operationId: getPlaylist + summary: Gets the specified Playlist. tags: - ingest - summary: Gets ingest data for a specific Playlist from Sofie. 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: Requested Playlist. + description: Internal or external ID of the Playlist to return. required: true schema: type: string responses: 200: description: Playlist is returned. - headers: - ETag: - schema: - type: string - description: Version of Playlist, if known. content: application/json: schema: - type: object - properties: - status: - type: number - const: 200 - playlist: - $ref: '#/components/schemas/ingestPlaylist' - example: - status: 200 - playlist: - name: playlist1 - required: - - status - - playlist - additionalProperties: false + $ref: '#/components/schemas/playlistResponse' 404: description: Invalid playlistId - $ref: '#/components/responses/idNotFound' - # oneOf: - # - $ref: '#/components/responses/playlistNotFound' + $ref: '#/components/responses/playlistNotFound' delete: - operationId: deleteIngestPlaylist + operationId: deletePlaylist + summary: Deletes a specified Playlist. Resources under the Playlist (e.g. Rundowns) will also be removed. tags: - ingest - summary: Deletes a specified ingest Playlist. Resources under the Playlist (e.g. Rundowns) 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: Playlist to delete. + description: Internal or external ID of the Playlist to delete. required: true schema: type: string responses: - 200: - description: Playlist removed. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false + 202: + description: Request for deleting accepted. 404: - $ref: '#/components/responses/idNotFound' - # oneOf: - # - $ref: '#/components/responses/playlistNotFound' - ingestRundowns: + $ref: '#/components/responses/playlistNotFound' + rundowns: get: - operationId: getIngestRundowns + operationId: getRundowns + summary: Gets all Rundowns belonging to a specified Playlist. tags: - ingest - summary: Gets ingest data for all Rundowns belonging to a Playlist. 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: Playlist to get all Rundowns for. + description: Internal or external ID of the Playlist the Rundowns belong to. required: true schema: type: string @@ -147,306 +115,235 @@ resources: content: application/json: schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - rundowns: - type: array - items: - $ref: '#/components/schemas/ingestRundownItem' - example: - - externalId: rundown1 - required: - - status - - playlists - additionalProperties: false + type: array + items: + $ref: '#/components/schemas/rundownResponse' 404: $ref: '#/components/responses/idNotFound' # oneOf: # - $ref: '#/components/responses/playlistNotFound' - put: - operationId: putIngestRundowns + # - $ref: '#/components/responses/rundownNotFound' + post: + operationId: postRundown + summary: Creates a Rundown in a specified Playlist. tags: - ingest - summary: Creates/updates the Rundowns in a Playlist. Any existing Rundowns in the Playlist that are not included in this list will be deleted (including their Segments and Parts). Rundowns will be placed in the Playlist in the order specified by their individual ranks. If the creation/deletion/updating of any Rundown fails all changes will be discarded. ETags are not supported for bulk updates, no version information will be stored for any of the created/modified Rundowns. 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: Playlist to create/update all Rundowns for. + description: Internal or external ID of the Playlist the new Rundown belongs to. required: true schema: type: string - - name: If-None-Match - in: header - # Indicates that the array elements are serialized as a comma-separated list in the header. - style: simple + 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: array - items: - type: string - description: If specified, each Rundown will only be updated if its version does not match one of the specified ETags. If no ETag is found for a Rundown, the new data will replace whatever currently exists, regardless of whether the data is actually the same. + 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: object - properties: - rundowns: - type: array - items: - $ref: '#/components/schemas/ingestRundown' - example: - - name: rundown1 - source: 'Our Company - Some Product Name' - rank: 0 - required: - - rundowns - additionalProperties: false + type: array + items: + $ref: '#/components/schemas/rundown' responses: - 200: - description: Rundowns have been updated. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false + 202: + description: Request has been accepted. + 400: + description: Bad request. 404: $ref: '#/components/responses/idNotFound' # oneOf: # - $ref: '#/components/responses/playlistNotFound' delete: - operationId: deleteIngestRundowns + operationId: deleteRundowns tags: - ingest - summary: Delete multiple rundowns. + 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: Playlist the ingest Part belongs to. + description: Internal or external ID of the Playlist the Rundowns to delete belong to. required: true schema: type: string responses: - 200: - description: Rundown removed. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false - ingestRundown: + 202: + description: Request accepted. + 404: + $ref: '#/components/responses/idNotFound' + rundown: get: - operationId: getIngestRundown + operationId: getRundown + summary: Gets the specified Rundown. tags: - ingest - summary: Gets ingest data for a specific 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: Playlist the Rundown belongs to. + description: Internal or external ID of the Playlist the Rundown belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown to return. + description: Internal or external ID of the Rundown to return. required: true schema: type: string responses: 200: description: Rundown is returned. - headers: - ETag: - schema: - type: string - description: Version of Rundown, if known. content: application/json: schema: - type: object - properties: - status: - type: number - const: 200 - rundown: - $ref: '#/components/schemas/ingestRundown' - example: - status: 200 - rundown: - name: rundown1 - source: 'Our Company - Some Product Name' - rank: 0 - required: - - status - - rundown - additionalProperties: false + $ref: '#/components/schemas/rundownResponse' 404: $ref: '#/components/responses/idNotFound' # oneOf: # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' put: - operationId: putIngestRundown + operationId: putRundown + summary: Updates an existing specified Rundown. tags: - ingest - summary: Creates a new or updates an existing Rundown. parameters: - - name: playlistId + - name: studioId in: path - description: Playlist to ingest Rundown into. + description: ID of the studio that is performing ingest operation. required: true schema: type: string - - name: rundownId + - name: playlistId in: path - description: Rundown to create/update. + description: Internal or external ID of the Playlist the Rundown to update belongs to. required: true schema: type: string - - name: If-None-Match - in: header - # Indicates that the array elements are serialized as a comma-separated list in the header. - style: simple - schema: - type: array - items: - type: string - description: If specified, the Rundown will only be updated if one of the specified ETags does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. - - name: If-Match - in: header - # Indicates that the array elements are serialized as a comma-separated list in the header. - style: simple - schema: - type: array - items: - type: string - description: If specified, the Rundown will only be updated if one of the specified ETags matches. - - name: ETag - in: header + - name: rundownId + in: path + description: Internal or external ID of the Rundown to update. required: true schema: type: string - example: '123456789' - description: ETag to use as version information for Rundown. requestBody: description: Contains the Rundown data. required: true content: application/json: schema: - $ref: '#/components/schemas/ingestRundown' - example: - name: rundown1 - source: 'Our Company - Some Product Name' - rank: 0 + $ref: '#/components/schemas/rundown' responses: - 200: - description: Rundown has been updated. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false - 201: - description: Rundown has been created. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 201 - example: 201 - required: - - status - additionalProperties: false + 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: deleteIngestRundown + operationId: deleteRundown + summary: Deletes a specified Rundown. Resources under the Rundown (e.g. Segments) will also be removed. tags: - ingest - summary: Deletes a specified ingest Rundown. Resources under the Rundown (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: Playlist the ingest Rundown belongs to. + description: Internal or external ID of the Playlist the Rundown belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown to delete. + description: Internal or external ID of the Rundown to delete. required: true schema: type: string responses: - 200: - description: Rundown removed. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false + 202: + description: Request for deleting accepted. 404: $ref: '#/components/responses/idNotFound' # oneOf: # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' - ingestSegments: + segments: get: - operationId: getIngestSegments + operationId: getSegments tags: - ingest - summary: Gets the ingest data for all Segments belonging to a Rundown. + 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: Playlist the Rundown belongs to. + description: Internal or external ID of the Playlist the Segments belong to. required: true schema: type: string - name: rundownId in: path - description: Rundown to get Segments for. + description: Internal or external ID of the Rundown the Segments belong to. required: true schema: type: string @@ -456,177 +353,165 @@ resources: content: application/json: schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - segments: - type: array - items: - $ref: '#/components/schemas/ingestSegmentItem' - example: - - externalId: segment1 - required: - - status - - segments - additionalProperties: false + type: array + items: + $ref: '#/components/schemas/segmentResponse' 404: $ref: '#/components/responses/idNotFound' # oneOf: # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' - put: - operationId: putIngestSegments + post: + operationId: postSegment tags: - ingest - summary: Creates/updates the Segments in a Rundown. Any existing Segments in the Rundown that are not included in this list will be deleted (including their Parts). Segments will be placed in the Rundown in the order specified by their individual ranks. If the creation/deletion/updating of any Segment fails all changes will be discarded. + 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: Playlist the Rundown belongs to. + description: Internal or external ID of the Playlist the new Segment belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown to create/update all Segments for. + 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: If-None-Match - in: header - # Indicates that the array elements are serialized as a comma-separated list in the header. - style: simple + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Segments to update belongs to. + required: true schema: - type: array - items: - type: string - description: If specified, each Segment will only be updated if its version does not match one of the specified ETags. If no ETag is found for a Segment, the new data will replace whatever currently exists, regardless of whether the data is actually the same. ETags are not supported for bulk updates, no version information will be stored for any of the created/modified Rundowns. + 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: object - properties: - segments: - type: array - items: - $ref: '#/components/schemas/ingestSegment' - example: - - name: segment1 - rank: 0 - required: - - segments - additionalProperties: false + type: array + items: + $ref: '#/components/schemas/segment' responses: - 200: - description: Segments have been updated. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false + 202: + description: Request accepted. + 400: + description: Bad request. 404: $ref: '#/components/responses/idNotFound' # oneOf: # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' delete: - operationId: deleteIngestSegments + operationId: deleteSegments tags: - ingest - summary: Delete multiple segments. + 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: Playlist the ingest Part belongs to. + description: Internal or external ID of the Playlist the Segments belong to. required: true schema: type: string - name: rundownId in: path - description: Rundown the ingest Part belongs to. + description: Internal or external ID of the Rundown the Segments to delete belong to. required: true schema: type: string responses: - 200: - description: Segments removed. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false - ingestSegment: + 202: + description: Request accepted. + 404: + $ref: '#/components/responses/idNotFound' + segment: get: - operationId: getIngestSegment + operationId: getSegment tags: - ingest - summary: Gets ingest data for a specific Segment. + 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: Playlist the Segment belongs to. + description: Internal or external ID of the Playlist the Segment belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown the Segment belongs to. + description: Internal or external ID of the Rundown the Segment belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment to create/update. + description: Internal or external ID of the Segment to return. required: true schema: type: string responses: 200: description: Segment is returned. - headers: - ETag: - schema: - type: string - description: Version of Segment, if known. content: application/json: schema: - type: object - properties: - status: - type: number - const: 200 - segment: - $ref: '#/components/schemas/ingestSegment' - example: - status: 200 - segment: - name: segment1 - rank: 0 - required: - - status - - segment - additionalProperties: false + $ref: '#/components/schemas/segmentResponse' 404: $ref: '#/components/responses/idNotFound' # oneOf: @@ -634,162 +519,118 @@ resources: # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/segmentNotFound' put: - operationId: putIngestSegment + operationId: putSegment tags: - ingest - summary: Creates a new or updates an existing Segment. + 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: Playlist to ingest Segment into. + description: Internal or external ID of the Playlist the Segment to update belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown to ingest Segment into. + description: Internal or external ID of the Rundown the Segment to update belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment to create/update. - schema: - type: string - - name: If-None-Match - in: header - # Indicates that the array elements are serialized as a comma-separated list in the header. - style: simple - schema: - type: array - items: - type: string - description: If specified, the Segment will only be updated if one of the specified ETags does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. - - name: If-Match - in: header - # Indicates that the array elements are serialized as a comma-separated list in the header. - style: simple - schema: - type: array - items: - type: string - description: If specified, the Segment will only be updated if one of the specified ETags matches. - - name: ETag - in: header + description: Internal or external ID of the Segment to update. required: true schema: type: string - description: ETag to use as version information for Segment. requestBody: description: Contains the Segment data. required: true content: application/json: schema: - $ref: '#/components/schemas/ingestSegment' - example: - name: segment1 - rank: 0 + $ref: '#/components/schemas/segment' responses: - 200: - description: Segment has been updated. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false - 201: - description: Segment has been created. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 201 - example: 201 - required: - - status - additionalProperties: false + 202: + description: Request accepted. + 400: + description: Bad request. 404: $ref: '#/components/responses/idNotFound' # oneOf: # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' delete: - operationId: deleteIngestSegment + operationId: deleteSegment tags: - ingest - summary: Deletes a specified ingest Segment. Resources under the Segment (e.g. Parts) will also be removed. + 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: Playlist the ingest Segment belongs to. + description: Internal or external ID of the Playlist the Segment belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown the ingest Segment belongs to. + description: Internal or external ID of the Rundown the Segment belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment to delete. + description: Internal or external ID of the Segment to delete. required: true schema: type: string responses: - 200: - description: Segment removed. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false + 202: + description: Request accepted. 404: $ref: '#/components/responses/idNotFound' # oneOf: # - $ref: '#/components/responses/playlistNotFound' # - $ref: '#/components/responses/rundownNotFound' - ingestParts: + parts: get: - operationId: getIngestParts + operationId: getParts tags: - ingest - summary: Gets the ingest data for all Parts belonging to a Segment. + 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: Playlist the Segment belongs to. + description: Internal or external ID of the Playlist the Parts belong to. required: true schema: type: string - name: rundownId in: path - description: Rundown the Segment belongs to. + description: Internal or external ID of the Rundown the Parts belong to. required: true schema: type: string - name: segmentId in: path - description: Segment to get Parts for. + description: Internal or external ID of the Segment the Parts belong to. required: true schema: type: string @@ -799,93 +640,107 @@ resources: content: application/json: schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - parts: - type: array - items: - $ref: '#/components/schemas/ingestPartItem' - example: - - externalId: part1 - required: - - status - - parts - additionalProperties: false + 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' - put: - operationId: putIngestParts + post: + operationId: postPart tags: - ingest - summary: Creates/updates the Parts in a Segment. Any existing Parts in the Segment that are not included in this list will be deleted. Parts will be placed in the Segment in the order specified by their individual ranks. If the creation/deletion/updating of any Parts fails all changes will be discarded. + 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: Playlist the Segment belongs to. + description: Internal or external ID of the Playlist the new Part belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown the Segment belongs to. + description: Internal or external ID of the Rundown the new Part belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment to create/update all Parts for. + 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: If-None-Match - in: header - # Indicates that the array elements are serialized as a comma-separated list in the header. - style: simple + - name: playlistId + in: path + description: Internal or external ID of the Playlist the Parts to update belong to. + required: true schema: - type: array - items: - type: string - description: If specified, each Part will only be updated if its version does not match one of the specified ETags. If no ETag is found for a Part, the new data will replace whatever currently exists, regardless of whether the data is actually the same. ETags are not supported for bulk updates, no version information will be stored for any of the created/modified Rundowns. + 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 Part data. + description: Contains the Parts data. required: true content: application/json: schema: - type: object - properties: - parts: - type: array - items: - $ref: '#/components/schemas/ingestPart' - example: - - externalId: part1 - required: - - parts - additionalProperties: false + type: array + items: + $ref: '#/components/schemas/part' responses: - 200: - description: Parts have been updated. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false + 202: + description: Request accepted. + 400: + description: Bad request. 404: $ref: '#/components/responses/idNotFound' # oneOf: @@ -893,102 +748,84 @@ resources: # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/partNotFound' delete: - operationId: deleteIngestParts + operationId: deleteParts tags: - ingest - summary: Delete multiple Parts. + 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: Playlist the ingest Part belongs to. + description: Internal or external ID of the Playlist the Parts belong to. required: true schema: type: string - name: rundownId in: path - description: Rundown the ingest Part belongs to. + description: Internal or external ID of the Rundown the Parts belong to. required: true schema: type: string - name: segmentId in: path - description: Segment the ingest Part belongs to. + description: Internal or external ID of the Segment the Parts to delete belong to. required: true schema: type: string responses: - 200: - description: Parts removed. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false - ingestPart: + 202: + description: Request for deleting accepted. + 404: + $ref: '#/components/responses/idNotFound' + part: get: - operationId: getIngestPart + operationId: getPart tags: - ingest - summary: Gets ingest data for a specific Part. + 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: Playlist the Part belongs to. + description: Internal or external ID of the Playlist the Part belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown the Part belongs to. + description: Internal or external ID of the Rundown the Part belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment the Part belongs to. + description: Internal or external ID of the Segment the Part belongs to. required: true schema: type: string - name: partId in: path - description: Part to create/update. + description: Internal or external ID of the Part to return. required: true schema: type: string responses: 200: description: Part is returned. - headers: - ETag: - schema: - type: string - description: Version of Part, if known. content: application/json: schema: - type: object - properties: - status: - type: number - const: 200 - part: - $ref: '#/components/schemas/ingestPart' - example: - status: 200 - part: - name: part1 - rank: 0 - required: - - status - - part - additionalProperties: false + $ref: '#/components/schemas/partResponse' 404: $ref: '#/components/responses/idNotFound' # oneOf: @@ -997,96 +834,51 @@ resources: # - $ref: '#/components/responses/segmentNotFound' # - $ref: '#/components/responses/partNotFound' put: - operationId: putIngestPart + operationId: putPart tags: - ingest - summary: Creates a new or updates an existing Part. + 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: Playlist to ingest Part into. + description: Internal or external ID of the Playlist the Part to update belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown to ingest Part into. + description: Internal or external ID of the Rundown the Part to update belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment to ingest Part into. + description: Internal or external ID of the Segment the Part to update belongs to. schema: type: string - name: partId in: path - description: Part to update/create. - schema: - type: string - - name: If-None-Match - in: header - # Indicates that the array elements are serialized as a comma-separated list in the header. - style: simple - schema: - type: array - items: - type: string - description: If specified, the Part will only be updated if the specified ETag does not match. If unspecified, the new data will replace whatever currently exists, regardless of whether the data is actually the same. - - name: If-Match - in: header - # Indicates that the array elements are serialized as a comma-separated list in the header. - style: simple - schema: - type: array - items: - type: string - description: If specified, the Part will only be updated if one of the specified ETags matches. - - name: ETag - in: header - required: true + description: Internal or external ID of the Part to update. schema: type: string - description: ETag to use as version information for Part. requestBody: description: Contains the Rundown data. required: true content: application/json: schema: - $ref: '#/components/schemas/ingestPart' - example: - name: part1 - rank: 0 + $ref: '#/components/schemas/part' responses: - 200: - description: Part has been updated. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false - 201: - description: Part has been created. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 201 - example: 201 - required: - - status - additionalProperties: false + 202: + description: Request has been accepted. + 400: + description: Bad request. 404: $ref: '#/components/responses/idNotFound' # oneOf: @@ -1094,50 +886,44 @@ resources: # - $ref: '#/components/responses/rundownNotFound' # - $ref: '#/components/responses/segmentNotFound' delete: - operationId: deleteIngestPart + operationId: deletePart tags: - ingest - summary: Deletes a specified ingest Part. + 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: Playlist the ingest Part belongs to. + description: Internal or external ID of the Playlist the Part belongs to. required: true schema: type: string - name: rundownId in: path - description: Rundown the ingest Part belongs to. + description: Internal or external ID of the Rundown the Part belongs to. required: true schema: type: string - name: segmentId in: path - description: Segment the ingest Part belongs to. + description: Internal or external ID of the Segment the Part belongs to. required: true schema: type: string - name: partId in: path - description: Part to delete. + description: Internal or external ID of the Part to delete. required: true schema: type: string responses: - 200: - description: Part removed. - content: - application/json: - schema: - type: object - properties: - status: - type: number - const: 200 - example: 200 - required: - - status - additionalProperties: false + 202: + description: Request has been accepted. 404: $ref: '#/components/responses/idNotFound' # oneOf: @@ -1148,10 +934,24 @@ resources: components: responses: idNotFound: - # oneOf responses like below don't render correctly with current tools - use playlist as an example for the docs. - # oneOf: - # - $ref: '#/components/responses/playlistNotFound' - $ref: '#/components/responses/playlistNotFound' + # 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: @@ -1186,10 +986,6 @@ components: type: number const: 404 example: 404 - notFound: - type: string - const: rundown - example: rundown message: type: string example: The specified Rundown was not found. @@ -1209,10 +1005,6 @@ components: type: number const: 404 example: 404 - notFound: - type: string - const: segment - example: segment message: type: string example: The specified Segment was not found. @@ -1232,10 +1024,6 @@ components: type: number const: 404 example: 404 - notFound: - type: string - const: part - example: part message: type: string example: The specified Part was not found. @@ -1244,111 +1032,293 @@ components: - notFound - message additionalProperties: false + badRequest: + description: Bad request. schemas: - ingestPlaylistItem: + playlist: type: object properties: - playlistId: + name: + type: string + example: Playlist name + externalId: type: string - description: The Id provided by Sofie. This Id will be used for /playlist commands for controlling playlist activations, playback etc. - rundowns: + example: playlist1 + rundownIds: type: array - description: All rundowns in a Playlist. items: - $ref: '#/components/schemas/ingestRundownItem' + type: string + example: + - rundown1 + - rundown2 + - rundown3 required: - - playlistId - - rundowns + - name additionalProperties: false - ingestRundownItem: + playlistResponse: type: object properties: + id: + type: string externalId: type: string - description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Rundown. + example: playlist1 + rundownIds: + type: array + items: + type: string + example: + - rundown1 + - rundown2 + - rundown3 + studioId: + type: string + example: studio0 required: + - id - externalId + - rundownIds + - studioId additionalProperties: false - ingestSegmentItem: + rundown: type: object properties: externalId: type: string - description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Segment. + 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. + timing: + type: object + description: If type is "none", only expectedDuration can be optionally provided. If type is "forward-time", expectedStart must be provided while either duration or expectedEnd can be optionally provided. If type is "back-time", expectedEnd must be provided while either duration or expectedStart can be optionally provided. + properties: + type: + type: string + enum: + - none + - forward-time + - back-time + expectedStart: + type: number + description: Epoch timestamp in milliseconds. + example: 1705924800000 + expectedEnd: + type: number + description: Epoch timestamp in milliseconds. + example: 1705927500000 + expectedDuration: + type: number + description: Interval in milliseconds. + example: 3600000 + required: + - type + additionalProperties: false + segments: + type: array + items: + $ref: '#/components/schemas/segment' required: - externalId + - name + - type + - resyncUrl + - timing additionalProperties: false - ingestPartItem: + rundownResponse: type: object properties: - name: + id: type: string externalId: type: string - description: The Id provided by a system external to Sofie that can be used by the same external system to identify this Part. + 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: - - name + - id - externalId - additionalProperties: false - ingestPlaylist: + - 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 + isHidden: + type: boolean + example: false + description: If the Segment is hidden or not. + timing: + type: object + description: Segment timing. + properties: + expectedStart: + type: number + description: Epoch timestamp in milliseconds. + example: 1705924800000 + expectedEnd: + type: number + description: Epoch timestamp in milliseconds. + example: 1705927500000 + additionalProperties: false + parts: + type: array + items: + $ref: '#/components/schemas/part' required: + - externalId - name + - rank additionalProperties: false - ingestRundown: + segmentResponse: type: object properties: - name: + id: type: string - source: + example: segment1 + externalId: + type: string + example: segmentExternal1 + rundownId: + type: string + example: rundown11 + name: type: string - description: A source type that can be displayed to the end-user. Should identify what type of system (e.g. vendor/product name) the data has been sent from. - examples: - - 'Some Product Name' - - 'Our Company - Some Product Name' + example: Segment 1 rank: type: number - description: The position of the Rundown in the parent Playlist. - inclusiveMinimum: 0.0 - payload: + example: 1 + isHidden: + type: boolean + timing: type: object - additionalProperties: true + properties: + expectedStart: + type: number + description: Epoch timestamp in milliseconds. + expectedEnd: + type: number + description: Epoch timestamp in milliseconds. + additionalProperties: false required: + - id + - externalId + - rundownId - name - - source - rank additionalProperties: false - ingestSegment: + part: type: object properties: + externalId: + type: string + example: part1 name: type: string + example: Part 1 + float: + type: boolean + example: false + autoNext: + type: boolean + example: false rank: type: number - description: The position of the Segment in the parent Rundown. - inclusiveMinimum: 0.0 + description: Position of the Part in the Segment. + example: 0 payload: type: object additionalProperties: true required: + - externalId - name - rank additionalProperties: false - ingestPart: + 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 - description: The position of the Part in the parent Segment. - payload: - type: object - additionalProperties: true + 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/src/__tests__/devices.spec.ts b/packages/openapi/src/__tests__/devices.spec.ts index 3560cc2fd07..788bee3fbb7 100644 --- a/packages/openapi/src/__tests__/devices.spec.ts +++ b/packages/openapi/src/__tests__/devices.spec.ts @@ -22,15 +22,11 @@ describe('Network client', () => { const devices = await devicesApi.devices() expect(devices.status).toBe(200) expect(devices).toHaveProperty('result') - expect(devices.result).toHaveProperty('ingest') - expect(devices.result).toHaveProperty('liveStatus') - expect(devices.result).toHaveProperty('mediaManager') - expect(devices.result).toHaveProperty('packageManager') - expect(devices.result).toHaveProperty('playout') - expect(devices.result).toHaveProperty('triggerInput') - devices.result.playout.forEach((device) => { - expect(typeof device).toBe('string') - deviceIds.push(device) + devices.result.forEach((device) => { + expect(typeof device).toBe('object') + expect(device).toHaveProperty('id') + expect(typeof device.id).toBe('string') + deviceIds.push(device.id) }) }) diff --git a/packages/openapi/src/__tests__/ingest.spec.ts b/packages/openapi/src/__tests__/ingest.spec.ts index b2f7b0a1c8c..0b7ed133e1b 100644 --- a/packages/openapi/src/__tests__/ingest.spec.ts +++ b/packages/openapi/src/__tests__/ingest.spec.ts @@ -1,11 +1,11 @@ -// eslint-disable-next-line node/no-missing-import -import { Configuration, IngestApi, IngestPart, IngestRundown, IngestSegment } from '../../client/ts' -import { checkServer } from '../checkServer' -import Logging from '../httpLogging' +import { Configuration, IngestApi, Part, RundownTimingTypeEnum } from '../../client/ts/index.js' +import { checkServer } from '../checkServer.js' +import Logging from '../httpLogging.js' const httpLogging = false +const studioId = 'studio0' -describe('Network client', () => { +describe('Ingest API', () => { const config = new Configuration({ basePath: process.env.SERVER_URL, middleware: [new Logging(httpLogging)], @@ -16,309 +16,455 @@ describe('Network client', () => { const ingestApi = new IngestApi(config) /** - * INGEST PLAYLIST + * PLAYLISTS */ const playlistIds: string[] = [] - test('Can request all ingest playlists in Sofie', async () => { - const ingestPlaylists = await ingestApi.getIngestPlaylists() - expect(ingestPlaylists.status).toBe(200) - expect(ingestPlaylists).toHaveProperty('playlists') + test('Can request all playlists', async () => { + const playlists = await ingestApi.getPlaylists({ studioId }) - expect(ingestPlaylists.playlists.length).toBeGreaterThanOrEqual(1) - ingestPlaylists.playlists.forEach((playlist) => { + expect(playlists.length).toBeGreaterThanOrEqual(1) + playlists.forEach((playlist) => { expect(typeof playlist).toBe('object') - expect(typeof playlist.playlistId).toBe('string') - playlistIds.push(playlist.playlistId) + 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 in Sofie', async () => { - const ingestPlaylist = await ingestApi.getIngestPlaylist({ + test('Can request a playlist by id', async () => { + const playlist = await ingestApi.getPlaylist({ + studioId, playlistId: playlistIds[0], }) - expect(ingestPlaylist.status).toBe(200) - expect(ingestPlaylist).toHaveProperty('playlist') - expect(ingestPlaylist.playlist).toHaveProperty('name') - expect(typeof ingestPlaylist.playlist.name).toBe('string') + 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 ingest playlists in Sofie', async () => { - const ingestRundown = await ingestApi.deleteIngestPlaylists() - expect(ingestRundown.status).toBe(200) + test('Can delete multiple playlists', async () => { + const result = await ingestApi.deletePlaylists({ studioId }) + expect(result).toBe(undefined) }) - test('Can delete ingest playlist by id in Sofie', async () => { - const ingestRundown = await ingestApi.deleteIngestPlaylist({ + test('Can delete playlist by id', async () => { + const result = await ingestApi.deletePlaylist({ + studioId, playlistId: playlistIds[0], }) - expect(ingestRundown.status).toBe(200) + expect(result).toBe(undefined) }) /** - * INGEST RUNDOWS + * RUNDOWNS */ const rundownIds: string[] = [] - test('Can request all ingest rundowns in Sofie', async () => { - const ingestRundowns = await ingestApi.getIngestRundowns({ + test('Can request all rundowns', async () => { + const rundowns = await ingestApi.getRundowns({ + studioId, playlistId: playlistIds[0], }) - expect(ingestRundowns.status).toBe(200) - expect(ingestRundowns).toHaveProperty('rundowns') - expect(ingestRundowns.rundowns.length).toBeGreaterThanOrEqual(1) + expect(rundowns.length).toBeGreaterThanOrEqual(1) - ingestRundowns.rundowns.forEach((rundown) => { + 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) }) }) - let newIngestRundown: IngestRundown | undefined - test('Can request ingest rundown by id in Sofie', async () => { - const ingestRundown = await ingestApi.getIngestRundown({ + test('Can request rundown by id', async () => { + const rundown = await ingestApi.getRundown({ + studioId, playlistId: playlistIds[0], rundownId: rundownIds[0], }) - expect(ingestRundown.status).toBe(200) - expect(ingestRundown).toHaveProperty('rundown') - - expect(ingestRundown.rundown).toHaveProperty('name') - expect(ingestRundown.rundown).toHaveProperty('rank') - expect(ingestRundown.rundown).toHaveProperty('source') - expect(typeof ingestRundown.rundown.name).toBe('string') - expect(typeof ingestRundown.rundown.rank).toBe('number') - expect(typeof ingestRundown.rundown.source).toBe('string') - newIngestRundown = JSON.parse(JSON.stringify(ingestRundown.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') }) - test('Can add/update multiple rundowns in Sofie', async () => { - newIngestRundown.name = newIngestRundown.name + 'added' - newIngestRundown.rank = 2 - const ingestRundown = await ingestApi.putIngestRundowns({ - playlistId: playlistIds[0], - putIngestRundownsRequest: { - rundowns: [ - { - name: 'rundown1', - source: 'Our Company - Some Product Name', - rank: 0, - }, - { - name: 'rundown2', - source: 'Our Second Company - Some Product Name', - rank: 1, - }, - ], - }, - }) - expect(ingestRundown.status).toBe(200) + const rundown = { + externalId: 'newRundown', + name: 'New rundown', + type: 'external', + resyncUrl: 'resyncUrl', + timing: { + type: RundownTimingTypeEnum.None, + expectedStart: 0, + expectedEnd: 0, + expectedDuration: 0, + }, + } + + test('Can create rundown', async () => { + const result = await ingestApi.postRundown({ studioId, playlistId: playlistIds[0], rundown }) + + expect(result).toBe(undefined) }) - const testIngestRundownId = 'rundown3' - test('Can add/update an ingest rundown in Sofie', async () => { - const newPutIngestRundown = await ingestApi.putIngestRundown({ - playlistId: playlistIds[0], - rundownId: testIngestRundownId, - ingestRundown: { - name: 'rundown3', - source: 'Our Company - Some Product Name', - rank: 3, - }, - eTag: '123456789', - ifNoneMatch: ['123456789', '1725453459'], - }) - expect(newPutIngestRundown.status).toBe(200) + test('Can update multiple rundowns', async () => { + const result = await ingestApi.putRundowns({ studioId, playlistId: playlistIds[0], rundown: [rundown] }) + expect(result).toBe(undefined) }) - test('Can delete multiple ingest rundowns in Sofie', async () => { - const ingestRundown = await ingestApi.deleteIngestRundowns({ + const updatedRundownId = 'rundown3' + test('Can update single rundown', async () => { + const result = await ingestApi.putRundown({ + studioId, playlistId: playlistIds[0], + rundownId: updatedRundownId, + rundown, }) - expect(ingestRundown.status).toBe(200) + 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 ingest rundown by id in Sofie', async () => { - const ingestRundown = await ingestApi.deleteIngestRundown({ + test('Can delete rundown by id', async () => { + const result = await ingestApi.deleteRundown({ + studioId, playlistId: playlistIds[0], - rundownId: testIngestRundownId, + rundownId: updatedRundownId, }) - expect(ingestRundown.status).toBe(200) + expect(result).toBe(undefined) }) /** * INGEST SEGMENT */ const segmentIds: string[] = [] - test('Can request all ingest segments in Sofie', async () => { - const ingestSegments = await ingestApi.getIngestSegments({ - playlistId: playlistIds[0], - rundownId: rundownIds[0], - }) - expect(ingestSegments.status).toBe(200) - expect(ingestSegments).toHaveProperty('segments') + test('Can request all segments', async () => { + const segments = await ingestApi.getSegments({ studioId, playlistId: playlistIds[0], rundownId: rundownIds[0] }) - expect(ingestSegments.segments.length).toBeGreaterThanOrEqual(1) + expect(segments.length).toBeGreaterThanOrEqual(1) - ingestSegments.segments.forEach((segment) => { + 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) }) }) - let newIngestSegment: IngestSegment | undefined - test('Can request ingest segment by id in Sofie', async () => { - const ingestSegment = await ingestApi.getIngestSegment({ + test('Can request segment by id', async () => { + const segment = await ingestApi.getSegment({ + studioId, playlistId: playlistIds[0], rundownId: rundownIds[0], segmentId: segmentIds[0], }) - expect(ingestSegment.status).toBe(200) - expect(ingestSegment).toHaveProperty('segment') - - expect(ingestSegment.segment).toHaveProperty('name') - expect(ingestSegment.segment).toHaveProperty('rank') - expect(typeof ingestSegment.segment.name).toBe('string') - expect(typeof ingestSegment.segment.rank).toBe('number') - newIngestSegment = JSON.parse(JSON.stringify(ingestSegment.segment)) + + 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') }) - test('can add/update multiple ingest segments in Sofie', async () => { - const ingestSegment = await ingestApi.putIngestSegments({ + const segment = { + externalId: 'segment1', + name: 'Segment 1', + rank: 0, + _float: true, + timing: { + expectedStart: 0, + expectedEnd: 0, + }, + } + + test('Can create segment', async () => { + const result = await ingestApi.postSegment({ + studioId, playlistId: playlistIds[0], rundownId: rundownIds[0], - putIngestSegmentsRequest: { - segments: [ - { - name: 'segment1', - rank: 0, - }, - ], - }, + segment, }) - expect(ingestSegment.status).toBe(200) + + expect(result).toBe(undefined) }) - const testIngestSegmentId = 'segment2' - test('Can add/update an ingest segment in Sofie', async () => { - newIngestSegment.name = newIngestSegment.name + 'Added' - const ingestSegment = await ingestApi.putIngestSegment({ + test('Can update multiple segments', async () => { + const result = await ingestApi.putSegments({ + studioId, playlistId: playlistIds[0], rundownId: rundownIds[0], - segmentId: testIngestSegmentId, - eTag: '1725269223', - ingestSegment: newIngestSegment, + segment: [segment], }) - expect(ingestSegment.status).toBe(200) + expect(result).toBe(undefined) }) - test('Can delete multiple ingest segments in Sofie', async () => { - const ingestRundown = await ingestApi.deleteIngestSegments({ + 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(ingestRundown.status).toBe(200) + expect(result).toBe(undefined) }) - test('Can delete ingest segment by id in Sofie', async () => { - const ingestRundown = await ingestApi.deleteIngestSegment({ + test('Can delete multiple segments', async () => { + const result = await ingestApi.deleteSegments({ + studioId, playlistId: playlistIds[0], rundownId: rundownIds[0], - segmentId: testIngestSegmentId, }) - expect(ingestRundown.status).toBe(200) + 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 ingest parts in Sofie', async () => { - const ingestParts = await ingestApi.getIngestParts({ + test('Can request all parts', async () => { + const parts = await ingestApi.getParts({ + studioId, playlistId: playlistIds[0], rundownId: rundownIds[0], segmentId: segmentIds[0], }) - expect(ingestParts.status).toBe(200) - expect(ingestParts).toHaveProperty('parts') - - expect(ingestParts.parts.length).toBeGreaterThanOrEqual(1) + expect(parts.length).toBeGreaterThanOrEqual(1) - ingestParts.parts.forEach((part) => { + parts.forEach((part) => { expect(typeof part).toBe('object') expect(typeof part.externalId).toBe('string') partIds.push(part.externalId) }) }) - let newIngestPart: IngestPart | undefined - test('Can request ingest part by id in Sofie', async () => { - const ingestPart = await ingestApi.getIngestPart({ + 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(ingestPart.status).toBe(200) - expect(ingestPart).toHaveProperty('part') - - expect(ingestPart.part).toHaveProperty('name') - expect(ingestPart.part).toHaveProperty('rank') - expect(typeof ingestPart.part.name).toBe('string') - expect(typeof ingestPart.part.rank).toBe('number') - newIngestPart = JSON.parse(JSON.stringify(ingestPart.part)) + + 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 add/update multiple ingest parts in Sofie', async () => { - const ingestPart = await ingestApi.putIngestParts({ + test('Can create part', async () => { + const result = await ingestApi.postPart({ + studioId, playlistId: playlistIds[0], rundownId: rundownIds[0], segmentId: segmentIds[0], - putIngestPartsRequest: { - parts: [ - { - name: 'part1', - rank: 0, - }, - ], + part: { + externalId: 'part1', + name: 'Part 1', + rank: 0, + _float: true, + autoNext: true, + 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(ingestPart.status).toBe(200) + expect(result).toBe(undefined) }) - const testIngestPartId = 'part2' - test('Can add/update an ingest part in Sofie', async () => { - newIngestPart.name = newIngestPart.name + 'Added' - const ingestPart = await ingestApi.putIngestPart({ + test('Can update multiple parts', async () => { + const result = await ingestApi.putParts({ + studioId, playlistId: playlistIds[0], rundownId: rundownIds[0], segmentId: segmentIds[0], - partId: testIngestPartId, - eTag: '1725269417', - ingestPart: newIngestPart, + part: [ + { + externalId: 'part1', + name: 'Part 1', + rank: 0, + _float: true, + autoNext: true, + 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, + _float: true, + autoNext: true, + payload: { + type: 'CAMERA', + guest: true, + script: '', + pieces: [ + { + id: 'piece1', + label: 'Piece 1', + attributes: {}, + objectType: 'CAMERA', + resourceName: 'camera1', + }, + ], + }, + }, }) - expect(ingestPart.status).toBe(200) + expect(result).toBe(undefined) }) - test('Can delete multiple ingest parts in Sofie', async () => { - const ingestRundown = await ingestApi.deleteIngestParts({ + test('Can delete multiple parts', async () => { + const result = await ingestApi.deleteParts({ + studioId, playlistId: playlistIds[0], rundownId: rundownIds[0], segmentId: segmentIds[0], }) - expect(ingestRundown.status).toBe(200) + expect(result).toBe(undefined) }) - test('Can delete ingest part by id in Sofie', async () => { - const ingestRundown = await ingestApi.deleteIngestPart({ + test('Can delete part by id', async () => { + const result = await ingestApi.deletePart({ + studioId, playlistId: playlistIds[0], rundownId: rundownIds[0], segmentId: segmentIds[0], - partId: testIngestPartId, + partId: updatedPartId, }) - expect(ingestRundown.status).toBe(200) + expect(result).toBe(undefined) }) }) diff --git a/packages/shared-lib/src/peripheralDevice/ingest.ts b/packages/shared-lib/src/peripheralDevice/ingest.ts index c53739f87e6..02a7bfa7163 100644 --- a/packages/shared-lib/src/peripheralDevice/ingest.ts +++ b/packages/shared-lib/src/peripheralDevice/ingest.ts @@ -9,29 +9,40 @@ export interface IngestRundown[] + /** Rundown timing definition */ + timing?: { + type?: 'none' | 'forward-time' | 'back-time' + expectedStart?: number + expectedDuration?: number + expectedEnd?: number + } + /** 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 */ externalId: string /** Name of the segment */ name: string - /** Rank of the segment within the rundown */ - rank: number - /** Raw payload of segment metadata. Only used by the blueprints */ payload: TSegmentPayload - /** Array of parts in this segment */ parts: IngestPart[] + /** Rank of the segment in the rundown */ + rank: number + /** If segment is hidden */ + isHidden?: boolean + /** Timing definition */ + timing?: { + expectedStart?: number + expectedEnd?: number + } } export interface IngestPart { /** Id of the part as reported by the ingest gateway. Must be unique for each part in the rundown */ @@ -40,7 +51,10 @@ export interface IngestPart { name: string /** Rank of the part within the segment */ rank: number - + /** If part is floated or not */ + float?: boolean + /** If part should automatically take to the next one when finished */ + autoNext?: boolean /** Raw payload of the part. Only used by the blueprints */ payload: TPartPayload } @@ -50,7 +64,6 @@ export interface IngestAdlib { externalId: string /** Name of the adlib */ name: string - /** Type of the raw payload. Only used by the blueprints */ payloadType: string /** Raw payload of the adlib. Only used by the blueprints */ From 33c61603a31742cf5ab29c70827aae6bca906df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Mon, 16 Jun 2025 12:42:47 +0200 Subject: [PATCH 017/291] fix: rename rundown source httpIngest to restApi --- meteor/server/api/ingest/actions.ts | 2 +- meteor/server/api/rest/v1/ingest.ts | 26 +++++++++++------------ meteor/server/lib/rest/v1/ingest.ts | 8 +++---- packages/corelib/src/dataModel/Rundown.ts | 6 +++--- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/meteor/server/api/ingest/actions.ts b/meteor/server/api/ingest/actions.ts index ae3f1a6daaf..3952ea2a1dd 100644 --- a/meteor/server/api/ingest/actions.ts +++ b/meteor/server/api/ingest/actions.ts @@ -29,7 +29,7 @@ export namespace IngestActions { return TriggerReloadDataResponse.COMPLETED } - case 'httpIngest': { + case 'restApi': { const resyncUrl = rundown.source.resyncUrl fetch(resyncUrl, { method: 'POST' }) .then(() => { diff --git a/meteor/server/api/rest/v1/ingest.ts b/meteor/server/api/rest/v1/ingest.ts index 8d936c96d9f..a648d762b19 100644 --- a/meteor/server/api/rest/v1/ingest.ts +++ b/meteor/server/api/rest/v1/ingest.ts @@ -16,7 +16,7 @@ import { IngestJobs } from '@sofie-automation/corelib/dist/worker/ingest' import { Meteor } from 'meteor/meteor' import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' import { - HttpIngestRundown, + RestApiIngestRundown, IngestRestAPI, PartResponse, PlaylistResponse, @@ -86,7 +86,7 @@ class IngestServerAPI implements IngestRestAPI { } } - private validateRundown(ingestRundown: HttpIngestRundown) { + private validateRundown(ingestRundown: RestApiIngestRundown) { check(ingestRundown, Object) check(ingestRundown.externalId, String) check(ingestRundown.name, String) @@ -296,12 +296,12 @@ class IngestServerAPI implements IngestRestAPI { } private checkRundownSource(rundown: Rundown | undefined) { - if (rundown && rundown.source.type !== 'httpIngest') { + if (rundown && rundown.source.type !== 'restApi') { throw new Meteor.Error( 403, `Cannot replace existing rundown from source '${getRundownNrcsName( rundown - )}' with new data from 'httpIngest' source` + )}' with new data from 'restApi' source` ) } } @@ -429,7 +429,7 @@ class IngestServerAPI implements IngestRestAPI { _event: string, studioId: StudioId, playlistId: string, - ingestRundown: HttpIngestRundown + ingestRundown: RestApiIngestRundown ): Promise> { check(studioId, String) check(playlistId, String) @@ -463,7 +463,7 @@ class IngestServerAPI implements IngestRestAPI { ingestRundown: { ...ingestRundown, playlistExternalId: playlistId }, isCreateAction: true, rundownSource: { - type: 'httpIngest', + type: 'restApi', resyncUrl: ingestRundown.resyncUrl, }, }) @@ -476,7 +476,7 @@ class IngestServerAPI implements IngestRestAPI { _event: string, studioId: StudioId, playlistId: string, - ingestRundowns: HttpIngestRundown[] + ingestRundowns: RestApiIngestRundown[] ): Promise> { check(studioId, String) check(playlistId, String) @@ -508,7 +508,7 @@ class IngestServerAPI implements IngestRestAPI { ingestRundown: { ...ingestRundown, playlistExternalId: playlist.externalId }, isCreateAction: true, rundownSource: { - type: 'httpIngest', + type: 'restApi', resyncUrl: ingestRundown.resyncUrl, }, }) @@ -524,7 +524,7 @@ class IngestServerAPI implements IngestRestAPI { studioId: StudioId, playlistId: string, rundownId: string, - ingestRundown: HttpIngestRundown + ingestRundown: RestApiIngestRundown ): Promise> { check(studioId, String) check(playlistId, String) @@ -548,7 +548,7 @@ class IngestServerAPI implements IngestRestAPI { ingestRundown: { ...ingestRundown, playlistExternalId: playlist.externalId }, isCreateAction: true, rundownSource: { - type: 'httpIngest', + type: 'restApi', resyncUrl: ingestRundown.resyncUrl, }, }) @@ -1234,7 +1234,7 @@ export function registerRoutes(registerRoute: APIRegisterHook): v const playlistId = params.playlistId check(playlistId, String) - const ingestRundown = body as HttpIngestRundown + 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') @@ -1256,7 +1256,7 @@ export function registerRoutes(registerRoute: APIRegisterHook): v const playlistId = params.playlistId check(playlistId, String) - const ingestRundowns = body as HttpIngestRundown[] + 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') @@ -1280,7 +1280,7 @@ export function registerRoutes(registerRoute: APIRegisterHook): v const rundownId = params.rundownId check(rundownId, String) - const ingestRundown = body as HttpIngestRundown + 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') diff --git a/meteor/server/lib/rest/v1/ingest.ts b/meteor/server/lib/rest/v1/ingest.ts index b604303c388..2469541a948 100644 --- a/meteor/server/lib/rest/v1/ingest.ts +++ b/meteor/server/lib/rest/v1/ingest.ts @@ -61,7 +61,7 @@ export interface IngestRestAPI { _event: string, studioId: StudioId, playlistId: string, - ingestRundown: HttpIngestRundown + ingestRundown: RestApiIngestRundown ): Promise> putRundowns( @@ -69,7 +69,7 @@ export interface IngestRestAPI { _event: string, studioId: StudioId, playlistId: string, - ingestRundowns: HttpIngestRundown[] + ingestRundowns: RestApiIngestRundown[] ): Promise> putRundown( @@ -78,7 +78,7 @@ export interface IngestRestAPI { studioId: StudioId, playlistId: string, rundownId: string, - ingestRundown: HttpIngestRundown + ingestRundown: RestApiIngestRundown ): Promise> deleteRundowns( @@ -232,7 +232,7 @@ export interface IngestRestAPI { ): Promise> } -export type HttpIngestRundown = Omit & { +export type RestApiIngestRundown = Omit & { resyncUrl: string } diff --git a/packages/corelib/src/dataModel/Rundown.ts b/packages/corelib/src/dataModel/Rundown.ts index efbf64d71bb..005f54d9c38 100644 --- a/packages/corelib/src/dataModel/Rundown.ts +++ b/packages/corelib/src/dataModel/Rundown.ts @@ -98,7 +98,7 @@ export type RundownSource = | RundownSourceSnapshot | RundownSourceHttp | RundownSourceTesting - | RundownSourceHttpIngest + | RundownSourceRestApi /** A description of the external NRCS source of a Rundown */ export interface RundownSourceNrcs { @@ -125,8 +125,8 @@ export interface RundownSourceTesting { showStyleVariantId: ShowStyleVariantId } /** A description of the source of a Rundown which was through the new HTTP ingest API */ -export interface RundownSourceHttpIngest { - type: 'httpIngest' +export interface RundownSourceRestApi { + type: 'restApi' resyncUrl: string } From 9a09353b63f86b00a5fc3a6c936451f0c83aa377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Mon, 16 Jun 2025 13:47:32 +0200 Subject: [PATCH 018/291] fix: remove fetch from meteor packages --- meteor/.meteor/packages | 1 - 1 file changed, 1 deletion(-) diff --git a/meteor/.meteor/packages b/meteor/.meteor/packages index 5a6339c8587..5e49caae6e3 100644 --- a/meteor/.meteor/packages +++ b/meteor/.meteor/packages @@ -20,4 +20,3 @@ typescript@5.6.6 # Enable TypeScript syntax in .ts and .tsx modules tracker@1.3.4 # Meteor's client-side reactive programming library zodern:types -fetch From a8ba3bec3092077105665c4659f52bc8640bb0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Mon, 16 Jun 2025 15:23:55 +0200 Subject: [PATCH 019/291] fix: handle ENOTFOUND rest api reload error --- meteor/server/api/ingest/actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meteor/server/api/ingest/actions.ts b/meteor/server/api/ingest/actions.ts index 3952ea2a1dd..79810b6710c 100644 --- a/meteor/server/api/ingest/actions.ts +++ b/meteor/server/api/ingest/actions.ts @@ -36,9 +36,9 @@ export namespace IngestActions { logger.info(`Reload rundown: resync request sent to "${resyncUrl}"`) }) .catch((error) => { - if (error.errno === 'ECONNREFUSED') { + if (error.errno === 'ECONNREFUSED' || error.errno === 'ENOTFOUND') { logger.error( - `Reload rundown: could not establish connection with "${resyncUrl}" (ECONNREFUSED)` + `Reload rundown: could not establish connection with "${resyncUrl}" (${error.errno})` ) return } From 5473c66733e7304566422a7ea8b0f25febbc00d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Tue, 17 Jun 2025 13:33:49 +0200 Subject: [PATCH 020/291] feat: enable rundown and segment payload validation --- meteor/server/api/rest/v1/ingest.ts | 87 +++++++++++++------ meteor/server/api/rest/v1/typeConversion.ts | 34 ++++++++ .../blueprints-integration/src/api/studio.ts | 6 ++ 3 files changed, 100 insertions(+), 27 deletions(-) diff --git a/meteor/server/api/rest/v1/ingest.ts b/meteor/server/api/rest/v1/ingest.ts index a648d762b19..ee27c1edc50 100644 --- a/meteor/server/api/rest/v1/ingest.ts +++ b/meteor/server/api/rest/v1/ingest.ts @@ -27,20 +27,28 @@ import { check } from '../../../lib/check' import { Parts, RundownPlaylists, Rundowns, Segments, Studios } from '../../../collections' import { logger } from '../../../logging' import { runIngestOperation } from '../../ingest/lib' -import { validateAPIPartPayload } from './typeConversion' +import { validateAPIPartPayload, validateAPIRundownPayload, validateAPISegmentPayload } from './typeConversion' import { APIFactory, APIRegisterHook, ServerAPIContext } from './types' class IngestServerAPI implements IngestRestAPI { - private async validateAPIPartPayloadForRundown( + private async validateAPIPayloadsForRundown( blueprintId: BlueprintId | undefined, - ingestRundown: IngestRundown, + 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( - ingestRundown.segments.map(async (segment, index) => { - return this.validateAPIPartPayloadForSegment(blueprintId, segment, { + rundown.segments.map(async (segment, index) => { + return this.validateAPIPayloadsForSegment(blueprintId, segment, { ...indexes, segment: index, }) @@ -48,7 +56,7 @@ class IngestServerAPI implements IngestRestAPI { ) } - private async validateAPIPartPayloadForSegment( + private async validateAPIPayloadsForSegment( blueprintId: BlueprintId | undefined, segment: IngestRundown['segments'][number], indexes?: { @@ -56,14 +64,22 @@ class IngestServerAPI implements IngestRestAPI { 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.validateAPIPartPayloadForPart(blueprintId, part, { ...indexes, part: index }) + return this.validateAPIPayloadsForPart(blueprintId, part, { ...indexes, part: index }) }) ) } - private async validateAPIPartPayloadForPart( + private async validateAPIPayloadsForPart( blueprintId: BlueprintId | undefined, part: IngestRundown['segments'][number]['parts'][number], indexes?: { @@ -73,19 +89,36 @@ class IngestServerAPI implements IngestRestAPI { } ) { const validationResult = await validateAPIPartPayload(blueprintId, part.payload) - if (validationResult && validationResult.length > 0) { - const parts = [] - if (indexes?.rundown !== undefined) parts.push(`rundowns[${indexes.rundown}]`) - if (indexes?.segment !== undefined) parts.push(`segments[${indexes.segment}]`) - if (indexes?.part !== undefined) parts.push(`parts[${indexes.part}]`) - let msg = `Part payload validation failed` - if (parts.length > 0) msg += ` for ${parts.join('.')}` - - logger.error(`${msg} with errors: ${validationResult}`) - throw new Meteor.Error(409, msg, JSON.stringify(validationResult)) + 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) @@ -438,7 +471,7 @@ class IngestServerAPI implements IngestRestAPI { const studio = await this.findStudio(studioId) this.validateRundown(ingestRundown) - await this.validateAPIPartPayloadForRundown(studio.blueprintId, ingestRundown) + await this.validateAPIPayloadsForRundown(studio.blueprintId, ingestRundown) const existingRundown = await Rundowns.findOneAsync({ $or: [ @@ -487,7 +520,7 @@ class IngestServerAPI implements IngestRestAPI { await Promise.all( ingestRundowns.map(async (ingestRundown, index) => { this.validateRundown(ingestRundown) - return this.validateAPIPartPayloadForRundown(studio.blueprintId, ingestRundown, { rundown: index }) + return this.validateAPIPayloadsForRundown(studio.blueprintId, ingestRundown, { rundown: index }) }) ) @@ -534,7 +567,7 @@ class IngestServerAPI implements IngestRestAPI { const studio = await this.findStudio(studioId) this.validateRundown(ingestRundown) - await this.validateAPIPartPayloadForRundown(studio.blueprintId, 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) @@ -666,7 +699,7 @@ class IngestServerAPI implements IngestRestAPI { const studio = await this.findStudio(studioId) this.validateSegment(ingestSegment) - await this.validateAPIPartPayloadForSegment(studio.blueprintId, 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) @@ -703,7 +736,7 @@ class IngestServerAPI implements IngestRestAPI { await Promise.all( ingestSegments.map(async (ingestSegment, index) => { this.validateSegment(ingestSegment) - return await this.validateAPIPartPayloadForSegment(studio.blueprintId, ingestSegment, { + return await this.validateAPIPayloadsForSegment(studio.blueprintId, ingestSegment, { segment: index, }) }) @@ -769,7 +802,7 @@ class IngestServerAPI implements IngestRestAPI { const studio = await this.findStudio(studioId) this.validateSegment(ingestSegment) - await this.validateAPIPartPayloadForSegment(studio.blueprintId, 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) @@ -926,7 +959,7 @@ class IngestServerAPI implements IngestRestAPI { const studio = await this.findStudio(studioId) this.validatePart(ingestPart) - await this.validateAPIPartPayloadForPart(studio.blueprintId, 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) @@ -967,7 +1000,7 @@ class IngestServerAPI implements IngestRestAPI { await Promise.all( ingestParts.map(async (ingestPart, index) => { this.validatePart(ingestPart) - return this.validateAPIPartPayloadForPart(studio.blueprintId, ingestPart, { part: index }) + return this.validateAPIPayloadsForPart(studio.blueprintId, ingestPart, { part: index }) }) ) @@ -1015,7 +1048,7 @@ class IngestServerAPI implements IngestRestAPI { const studio = await this.findStudio(studioId) this.validatePart(ingestPart) - await this.validateAPIPartPayloadForPart(studio.blueprintId, 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) diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index 0e919fcbfc3..6b011d99e96 100644 --- a/meteor/server/api/rest/v1/typeConversion.ts +++ b/meteor/server/api/rest/v1/typeConversion.ts @@ -727,6 +727,40 @@ export function playlistSnapshotOptionsFrom(options: APIPlaylistSnapshotOptions) } } +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 diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index ebac1053cd0..f9f6d624214 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -103,6 +103,12 @@ 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 From 22ceb586c1f622b83106adf89d0f39523e43cca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Tue, 17 Jun 2025 16:06:25 +0200 Subject: [PATCH 021/291] fix: convert user actions external to internal ids on api layer --- meteor/server/api/rest/v1/playlists.ts | 195 ++++++++++++------ meteor/server/security/check.ts | 19 +- packages/job-worker/src/playout/lock.ts | 8 +- .../implementation/PlayoutRundownModelImpl.ts | 5 +- .../implementation/PlayoutSegmentModelImpl.ts | 3 +- 5 files changed, 144 insertions(+), 86 deletions(-) diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 520b8b096ae..837e3dd39aa 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -23,9 +23,11 @@ import { BucketAdLibActions, BucketAdLibs, Buckets, + Parts, RundownBaselineAdLibActions, RundownBaselineAdLibPieces, RundownPlaylists, + Segments, } from '../../../collections' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { ServerClientAPI } from '../../client' @@ -38,6 +40,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,41 +104,47 @@ 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( connection: Meteor.Connection, 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.DeactivateRundownPlaylist, { - playlistId: rundownPlaylistId, + playlistId: playlist._id, } ) } + async executeAdLib( connection: Meteor.Connection, event: string, @@ -155,14 +205,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, @@ -211,14 +261,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, @@ -232,6 +282,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { ) } } + async executeBucketAdLib( connection: Meteor.Connection, event: string, @@ -240,6 +291,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( @@ -278,21 +331,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, @@ -300,42 +354,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, } @@ -347,16 +406,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) => { @@ -364,7 +425,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 ) } @@ -376,17 +437,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, } ) } @@ -396,19 +459,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, } ) } @@ -418,19 +484,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, } ) } @@ -441,19 +510,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, } ) } @@ -465,23 +537,20 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { fromPartInstanceId: PartInstanceId | undefined ): Promise> { triggerWriteAccess() - const rundownPlaylist = await RundownPlaylists.findOneAsync({ - $or: [{ _id: rundownPlaylistId }, { externalId: 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, } ) } @@ -492,10 +561,8 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { rundownPlaylistId: RundownPlaylistId, sourceLayerIds: string[] ): Promise> { - const rundownPlaylist = await RundownPlaylists.findOneAsync({ - $or: [{ _id: rundownPlaylistId }, { externalId: rundownPlaylistId }], - }) - if (!rundownPlaylist) + const playlist = await this.findPlaylist(rundownPlaylistId) + if (!playlist) return ClientAPI.responseError( UserError.from( Error(`Rundown playlist ${rundownPlaylistId} does not exist`), @@ -504,7 +571,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`), @@ -518,15 +585,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, } ) @@ -538,18 +605,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, } ) diff --git a/meteor/server/security/check.ts b/meteor/server/security/check.ts index 7c43da165ac..da6d38ad1de 100644 --- a/meteor/server/security/check.ts +++ b/meteor/server/security/check.ts @@ -20,17 +20,14 @@ export async function checkAccessToPlaylist( ): Promise { assertConnectionHasOneOfPermissions(cred, 'studio') - const playlist = (await RundownPlaylists.findOneAsync( - { $or: [{ _id: playlistId }, { externalId: playlistId }] }, - { - projection: { - _id: 1, - studioId: 1, - organizationId: 1, - name: 1, - }, - } - )) as Pick | undefined + const playlist = (await RundownPlaylists.findOneAsync(playlistId, { + projection: { + _id: 1, + studioId: 1, + organizationId: 1, + name: 1, + }, + })) as Pick | undefined if (!playlist) throw new Meteor.Error(404, `RundownPlaylist "${playlistId}" not found`) return playlist diff --git a/packages/job-worker/src/playout/lock.ts b/packages/job-worker/src/playout/lock.ts index 99e9eeba94a..0dad5256174 100644 --- a/packages/job-worker/src/playout/lock.ts +++ b/packages/job-worker/src/playout/lock.ts @@ -24,9 +24,7 @@ export async function runJobWithPlayoutModel( // We can lock before checking ownership, as the locks are scoped to the studio return runWithPlaylistLock(context, data.playlistId, async (playlistLock) => { - const playlist = await context.directCollections.RundownPlaylists.findOne({ - $or: [{ _id: data.playlistId }, { externalId: data.playlistId }], - }) + const playlist = await context.directCollections.RundownPlaylists.findOne(data.playlistId) if (!playlist || playlist.studioId !== context.studioId) { throw new Error(`Job playlist "${data.playlistId}" not found or for another studio`) } @@ -50,9 +48,7 @@ export async function runJobWithPlaylistLock( // We can lock before checking ownership, as the locks are scoped to the studio return runWithPlaylistLock(context, data.playlistId, async (lock) => { - const playlist = await context.directCollections.RundownPlaylists.findOne({ - $or: [{ _id: data.playlistId }, { externalId: data.playlistId }], - }) + const playlist = await context.directCollections.RundownPlaylists.findOne(data.playlistId) if (playlist && playlist.studioId !== context.studioId) { throw new Error(`Job playlist "${data.playlistId}" not found or for another studio`) } diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts index 1362d4b59ab..4f6b54aef69 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutRundownModelImpl.ts @@ -9,7 +9,6 @@ import { UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/erro import { SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { getRandomId } from '@sofie-automation/corelib/dist/lib' import { PlayoutSegmentModelImpl } from './PlayoutSegmentModelImpl.js' -import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' export class PlayoutRundownModelImpl implements PlayoutRundownModel { readonly rundown: ReadonlyDeep @@ -48,9 +47,7 @@ export class PlayoutRundownModelImpl implements PlayoutRundownModel { } getSegment(id: SegmentId): PlayoutSegmentModel | undefined { - return this.segments.find( - (segment) => segment.segment._id === id || segment.segment.externalId === unprotectString(id) - ) + return this.segments.find((segment) => segment.segment._id === id) } getSegmentIds(): SegmentId[] { diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts index 303febcfe11..1356244003a 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutSegmentModelImpl.ts @@ -3,7 +3,6 @@ import { ReadonlyDeep } from 'type-fest' import { DBSegment, SegmentOrphanedReason } from '@sofie-automation/corelib/dist/dataModel/Segment' import { DBPart } from '@sofie-automation/corelib/dist/dataModel/Part' import { PlayoutSegmentModel } from '../PlayoutSegmentModel.js' -import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' export class PlayoutSegmentModelImpl implements PlayoutSegmentModel { readonly #segment: DBSegment @@ -21,7 +20,7 @@ export class PlayoutSegmentModelImpl implements PlayoutSegmentModel { } getPart(id: PartId): ReadonlyDeep | undefined { - return this.parts.find((part) => part._id === id || part.externalId === unprotectString(id)) + return this.parts.find((part) => part._id === id) } getPartIds(): PartId[] { From 08890d2426ec2f7f4ce3d956a4ab5933c0e6cd6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Wed, 18 Jun 2025 12:34:58 +0200 Subject: [PATCH 022/291] fix: replace meteor fetch function with global fetch --- meteor/__mocks__/_setupMocks.ts | 1 - meteor/server/api/ingest/actions.ts | 9 +++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/meteor/__mocks__/_setupMocks.ts b/meteor/__mocks__/_setupMocks.ts index 77ca346ddb3..8cf580f95da 100644 --- a/meteor/__mocks__/_setupMocks.ts +++ b/meteor/__mocks__/_setupMocks.ts @@ -10,7 +10,6 @@ jest.mock('nanoid', (...args) => require('./random').setup(args), { virtual: tru // Add references to all "meteor" mocks below, so that jest resolves the imports properly. -jest.mock('meteor/fetch', () => null, { virtual: true }) jest.mock('meteor/meteor', (...args) => require('./meteor').setup(args), { virtual: true }) jest.mock('meteor/random', (...args) => require('./random').setup(args), { virtual: true }) jest.mock('meteor/check', (...args) => require('./check').setup(args), { virtual: true }) diff --git a/meteor/server/api/ingest/actions.ts b/meteor/server/api/ingest/actions.ts index 79810b6710c..29fc700312a 100644 --- a/meteor/server/api/ingest/actions.ts +++ b/meteor/server/api/ingest/actions.ts @@ -7,7 +7,6 @@ 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 { fetch } from 'meteor/fetch' import { logger } from '../../logging' /* @@ -36,16 +35,14 @@ export namespace IngestActions { logger.info(`Reload rundown: resync request sent to "${resyncUrl}"`) }) .catch((error) => { - if (error.errno === 'ECONNREFUSED' || error.errno === 'ENOTFOUND') { + if (error.cause.code === 'ECONNREFUSED' || error.cause.code === 'ENOTFOUND') { logger.error( - `Reload rundown: could not establish connection with "${resyncUrl}" (${error.errno})` + `Reload rundown: could not establish connection with "${resyncUrl}" (${error.cause.code})` ) return } logger.error( - `Reload rundown: error occured while sending resync request to "${resyncUrl}", error: "${JSON.stringify( - error - )}"` + `Reload rundown: error occured while sending resync request to "${resyncUrl}", message: ${error.message}, cause: ${JSON.stringify(error.cause)}` ) }) From 54daf41f76f0d66dd092d216d2614368cdc4baa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Tue, 7 Oct 2025 16:43:55 +0200 Subject: [PATCH 023/291] fix: ingest api cleanup --- meteor/server/api/rest/v1/ingest.ts | 32 +++---------- .../ingest/MutableIngestPartImpl.ts | 8 ---- .../ingest/MutableIngestRundownImpl.ts | 2 - .../ingest/MutableIngestSegmentImpl.ts | 10 ---- .../MutableIngestRundownImpl.spec.ts | 42 ---------------- .../MutableIngestSegmentImpl.spec.ts | 14 ------ .../job-worker/src/ingest/runOperation.ts | 2 - packages/openapi/api/definitions/ingest.yaml | 48 ------------------- .../shared-lib/src/peripheralDevice/ingest.ts | 30 ++++-------- 9 files changed, 17 insertions(+), 171 deletions(-) diff --git a/meteor/server/api/rest/v1/ingest.ts b/meteor/server/api/rest/v1/ingest.ts index ee27c1edc50..5f980b86562 100644 --- a/meteor/server/api/rest/v1/ingest.ts +++ b/meteor/server/api/rest/v1/ingest.ts @@ -13,18 +13,18 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund 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 { Meteor } from 'meteor/meteor' 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 { - RestApiIngestRundown, IngestRestAPI, PartResponse, PlaylistResponse, + RestApiIngestRundown, RundownResponse, SegmentResponse, } from '../../../lib/rest/v1/ingest' -import { check } from '../../../lib/check' -import { Parts, RundownPlaylists, Rundowns, Segments, Studios } from '../../../collections' import { logger } from '../../../logging' import { runIngestOperation } from '../../ingest/lib' import { validateAPIPartPayload, validateAPIRundownPayload, validateAPISegmentPayload } from './typeConversion' @@ -127,15 +127,6 @@ class IngestServerAPI implements IngestRestAPI { check(ingestRundown.segments, Array) check(ingestRundown.resyncUrl, String) - check(ingestRundown.timing, Object) - check(ingestRundown.timing?.type, String) - - if (ingestRundown.timing?.type === 'forward-time') { - check(ingestRundown.timing.expectedStart, Number) - } else if (ingestRundown.timing?.type === 'back-time') { - check(ingestRundown.timing?.expectedEnd, Number) - } - ingestRundown.segments.forEach((ingestSegment) => this.validateSegment(ingestSegment)) } @@ -146,12 +137,6 @@ class IngestServerAPI implements IngestRestAPI { check(ingestSegment.rank, Number) check(ingestSegment.parts, Array) - if (ingestSegment.isHidden !== undefined) check(ingestSegment.isHidden, Boolean) - if (ingestSegment.timing !== undefined) { - check(ingestSegment.timing.expectedStart, Number) - check(ingestSegment.timing.expectedEnd, Number) - } - ingestSegment.parts.forEach((ingestPart) => this.validatePart(ingestPart)) } @@ -160,9 +145,6 @@ class IngestServerAPI implements IngestRestAPI { check(ingestPart.externalId, String) check(ingestPart.name, String) check(ingestPart.rank, Number) - - if (ingestPart.float !== undefined) check(ingestPart.float, Boolean) - if (ingestPart.autoNext !== undefined) check(ingestPart.autoNext, Boolean) } private adaptPlaylist(rawPlaylist: DBRundownPlaylist): PlaylistResponse { @@ -493,7 +475,7 @@ class IngestServerAPI implements IngestRestAPI { await runIngestOperation(studio._id, IngestJobs.UpdateRundown, { rundownExternalId: ingestRundown.externalId, - ingestRundown: { ...ingestRundown, playlistExternalId: playlistId }, + ingestRundown: ingestRundown, isCreateAction: true, rundownSource: { type: 'restApi', @@ -538,7 +520,7 @@ class IngestServerAPI implements IngestRestAPI { return runIngestOperation(studio._id, IngestJobs.UpdateRundown, { rundownExternalId: ingestRundown.externalId, - ingestRundown: { ...ingestRundown, playlistExternalId: playlist.externalId }, + ingestRundown: ingestRundown, isCreateAction: true, rundownSource: { type: 'restApi', @@ -578,7 +560,7 @@ class IngestServerAPI implements IngestRestAPI { await runIngestOperation(studio._id, IngestJobs.UpdateRundown, { rundownExternalId: existingRundown.externalId, - ingestRundown: { ...ingestRundown, playlistExternalId: playlist.externalId }, + ingestRundown: ingestRundown, isCreateAction: true, rundownSource: { type: 'restApi', diff --git a/packages/job-worker/src/blueprints/ingest/MutableIngestPartImpl.ts b/packages/job-worker/src/blueprints/ingest/MutableIngestPartImpl.ts index 7948a2c4adb..068eefddd67 100644 --- a/packages/job-worker/src/blueprints/ingest/MutableIngestPartImpl.ts +++ b/packages/job-worker/src/blueprints/ingest/MutableIngestPartImpl.ts @@ -20,14 +20,6 @@ export class MutableIngestPartImpl implements MutableIng return this.#ingestPart.name } - get float(): boolean { - return this.#ingestPart.float ?? false - } - - get autoNext(): boolean { - return this.#ingestPart.autoNext ?? false - } - get payload(): ReadonlyDeep | undefined { return this.#ingestPart.payload as ReadonlyDeep } diff --git a/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts b/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts index 5c217d4a8a4..4a7b286d43e 100644 --- a/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts +++ b/packages/job-worker/src/blueprints/ingest/MutableIngestRundownImpl.ts @@ -314,8 +314,6 @@ export class MutableIngestRundownImpl | undefined { return this.#ingestSegment.payload as ReadonlyDeep } @@ -233,8 +225,6 @@ export class MutableIngestSegmentImpl = { externalId: part.externalId, rank, - float: part.float, - autoNext: part.autoNext, name: part.name, payload: part.payload, userEditStates: part.userEditStates, diff --git a/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestRundownImpl.spec.ts b/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestRundownImpl.spec.ts index 542ce44981b..f24ff4b9cb3 100644 --- a/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestRundownImpl.spec.ts +++ b/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestRundownImpl.spec.ts @@ -28,11 +28,6 @@ describe('MutableIngestRundownImpl', () => { externalId: 'seg0', name: 'name', rank: 0, - isHidden: false, - timing: { - expectedStart: 0, - expectedEnd: 0, - }, payload: { val: 'first-val', second: 5, @@ -43,8 +38,6 @@ describe('MutableIngestRundownImpl', () => { externalId: 'part0', name: 'my first part', rank: 0, - float: false, - autoNext: false, payload: { val: 'some-val', }, @@ -56,11 +49,6 @@ describe('MutableIngestRundownImpl', () => { externalId: 'seg1', name: 'name 2', rank: 1, - isHidden: false, - timing: { - expectedStart: 0, - expectedEnd: 0, - }, payload: { val: 'next-val', }, @@ -70,8 +58,6 @@ describe('MutableIngestRundownImpl', () => { externalId: 'part1', name: 'my second part', rank: 0, - float: false, - autoNext: false, payload: { val: 'some-val', }, @@ -83,11 +69,6 @@ describe('MutableIngestRundownImpl', () => { externalId: 'seg2', name: 'name 3', rank: 2, - isHidden: false, - timing: { - expectedStart: 0, - expectedEnd: 0, - }, payload: { val: 'last-val', }, @@ -97,8 +78,6 @@ describe('MutableIngestRundownImpl', () => { externalId: 'part2', name: 'my third part', rank: 0, - float: false, - autoNext: false, payload: { val: 'some-val', }, @@ -466,11 +445,6 @@ describe('MutableIngestRundownImpl', () => { const newSegment: Omit = { externalId: 'seg1', name: 'new name', - isHidden: false, - timing: { - expectedStart: 0, - expectedEnd: 0, - }, payload: { val: 'new-val', }, @@ -480,8 +454,6 @@ describe('MutableIngestRundownImpl', () => { externalId: 'part1', name: 'new part name', rank: 0, - float: false, - autoNext: false, payload: { val: 'new-part-val', }, @@ -526,11 +498,6 @@ describe('MutableIngestRundownImpl', () => { const newSegment: Omit = { externalId: 'segX', name: 'new name', - isHidden: false, - timing: { - expectedStart: 0, - expectedEnd: 0, - }, payload: { val: 'new-val', }, @@ -540,8 +507,6 @@ describe('MutableIngestRundownImpl', () => { externalId: 'partX', name: 'new part name', rank: 0, - float: false, - autoNext: false, payload: { val: 'new-part-val', }, @@ -578,11 +543,6 @@ describe('MutableIngestRundownImpl', () => { const newSegment: Omit = { externalId: 'segX', name: 'new name', - isHidden: false, - timing: { - expectedStart: 0, - expectedEnd: 0, - }, payload: { val: 'new-val', }, @@ -591,8 +551,6 @@ describe('MutableIngestRundownImpl', () => { externalId: 'partX', name: 'new part name', rank: 0, - float: false, - autoNext: false, payload: { val: 'new-part-val', }, diff --git a/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestSegmentImpl.spec.ts b/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestSegmentImpl.spec.ts index a35aa9c6692..5ab56ecb463 100644 --- a/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestSegmentImpl.spec.ts +++ b/packages/job-worker/src/blueprints/ingest/__tests__/MutableIngestSegmentImpl.spec.ts @@ -25,8 +25,6 @@ describe('MutableIngestSegmentImpl', () => { externalId: 'part0', name: 'my first part', rank: 0, - float: false, - autoNext: false, payload: { val: 'some-val', }, @@ -36,8 +34,6 @@ describe('MutableIngestSegmentImpl', () => { externalId: 'part1', name: 'another part', rank: 1, - float: false, - autoNext: false, payload: { val: 'second-val', }, @@ -47,8 +43,6 @@ describe('MutableIngestSegmentImpl', () => { externalId: 'part2', name: 'third part', rank: 2, - float: false, - autoNext: false, payload: { val: 'third-val', }, @@ -58,8 +52,6 @@ describe('MutableIngestSegmentImpl', () => { externalId: 'part3', name: 'last part', rank: 3, - float: false, - autoNext: false, payload: { val: 'last-val', }, @@ -337,8 +329,6 @@ describe('MutableIngestSegmentImpl', () => { const newPart: Omit = { externalId: 'part1', name: 'new name', - float: false, - autoNext: false, payload: { val: 'new-val', }, @@ -379,8 +369,6 @@ describe('MutableIngestSegmentImpl', () => { const newPart: Omit = { externalId: 'partX', name: 'new name', - float: false, - autoNext: false, payload: { val: 'new-val', }, @@ -420,8 +408,6 @@ describe('MutableIngestSegmentImpl', () => { const newPart: Omit = { externalId: 'partX', name: 'new name', - float: false, - autoNext: false, payload: { val: 'new-val', }, diff --git a/packages/job-worker/src/ingest/runOperation.ts b/packages/job-worker/src/ingest/runOperation.ts index 4f90eb559e2..86716a7bab2 100644 --- a/packages/job-worker/src/ingest/runOperation.ts +++ b/packages/job-worker/src/ingest/runOperation.ts @@ -275,8 +275,6 @@ async function updateSofieIngestRundown( name: nrcsIngestRundown.name, type: nrcsIngestRundown.type, segments: [], - timing: nrcsIngestRundown.timing, - playlistExternalId: nrcsIngestRundown.playlistExternalId, payload: undefined, userEditStates: {}, rundownSource: nrcsIngestRundown.rundownSource, diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index 4edec79274a..36772408fdd 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -1097,31 +1097,6 @@ components: 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. - timing: - type: object - description: If type is "none", only expectedDuration can be optionally provided. If type is "forward-time", expectedStart must be provided while either duration or expectedEnd can be optionally provided. If type is "back-time", expectedEnd must be provided while either duration or expectedStart can be optionally provided. - properties: - type: - type: string - enum: - - none - - forward-time - - back-time - expectedStart: - type: number - description: Epoch timestamp in milliseconds. - example: 1705924800000 - expectedEnd: - type: number - description: Epoch timestamp in milliseconds. - example: 1705927500000 - expectedDuration: - type: number - description: Interval in milliseconds. - example: 3600000 - required: - - type - additionalProperties: false segments: type: array items: @@ -1196,23 +1171,6 @@ components: description: The position of the Segment in the parent Rundown. inclusiveMinimum: 0.0 example: 1 - isHidden: - type: boolean - example: false - description: If the Segment is hidden or not. - timing: - type: object - description: Segment timing. - properties: - expectedStart: - type: number - description: Epoch timestamp in milliseconds. - example: 1705924800000 - expectedEnd: - type: number - description: Epoch timestamp in milliseconds. - example: 1705927500000 - additionalProperties: false parts: type: array items: @@ -1268,12 +1226,6 @@ components: name: type: string example: Part 1 - float: - type: boolean - example: false - autoNext: - type: boolean - example: false rank: type: number description: Position of the Part in the Segment. diff --git a/packages/shared-lib/src/peripheralDevice/ingest.ts b/packages/shared-lib/src/peripheralDevice/ingest.ts index 02a7bfa7163..2ee1c810c1f 100644 --- a/packages/shared-lib/src/peripheralDevice/ingest.ts +++ b/packages/shared-lib/src/peripheralDevice/ingest.ts @@ -9,19 +9,16 @@ export interface IngestRundown[] - /** Rundown timing definition */ - timing?: { - type?: 'none' | 'forward-time' | 'back-time' - expectedStart?: number - expectedDuration?: number - expectedEnd?: number - } + /** Id of the playlist this rundown belongs to */ playlistExternalId?: string } @@ -30,19 +27,14 @@ export interface IngestSegment[] - /** Rank of the segment in the rundown */ - rank: number - /** If segment is hidden */ - isHidden?: boolean - /** Timing definition */ - timing?: { - expectedStart?: number - expectedEnd?: number - } } export interface IngestPart { /** Id of the part as reported by the ingest gateway. Must be unique for each part in the rundown */ @@ -51,10 +43,7 @@ export interface IngestPart { name: string /** Rank of the part within the segment */ rank: number - /** If part is floated or not */ - float?: boolean - /** If part should automatically take to the next one when finished */ - autoNext?: boolean + /** Raw payload of the part. Only used by the blueprints */ payload: TPartPayload } @@ -64,6 +53,7 @@ export interface IngestAdlib { externalId: string /** Name of the adlib */ name: string + /** Type of the raw payload. Only used by the blueprints */ payloadType: string /** Raw payload of the adlib. Only used by the blueprints */ From 9c7844e402355696f2434a4cea47f7f3c7f6d12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Wed, 8 Oct 2025 16:23:46 +0200 Subject: [PATCH 024/291] fix: add playlistExternalId to runOperation --- packages/job-worker/src/ingest/runOperation.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/job-worker/src/ingest/runOperation.ts b/packages/job-worker/src/ingest/runOperation.ts index 86716a7bab2..5104e37b3fc 100644 --- a/packages/job-worker/src/ingest/runOperation.ts +++ b/packages/job-worker/src/ingest/runOperation.ts @@ -278,6 +278,7 @@ async function updateSofieIngestRundown( payload: undefined, userEditStates: {}, rundownSource: nrcsIngestRundown.rundownSource, + playlistExternalId: undefined, } satisfies Complete, false ) From 205e1c9c80da9ab1b9264b719e2426f8c502f7d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Thu, 16 Oct 2025 15:59:00 +0200 Subject: [PATCH 025/291] fix: add payload to rundown and segment --- packages/openapi/api/definitions/ingest.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/openapi/api/definitions/ingest.yaml b/packages/openapi/api/definitions/ingest.yaml index 36772408fdd..3826b8c209f 100644 --- a/packages/openapi/api/definitions/ingest.yaml +++ b/packages/openapi/api/definitions/ingest.yaml @@ -1101,12 +1101,15 @@ components: type: array items: $ref: '#/components/schemas/segment' + payload: + type: object + additionalProperties: true required: - externalId - name - type - resyncUrl - - timing + - segments additionalProperties: false rundownResponse: type: object @@ -1175,10 +1178,14 @@ components: type: array items: $ref: '#/components/schemas/part' + payload: + type: object + additionalProperties: true required: - externalId - name - rank + - parts additionalProperties: false segmentResponse: type: object From dd9eb5b8638494cc5fa44e09097b6e77be19f2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Thu, 16 Oct 2025 18:44:24 +0200 Subject: [PATCH 026/291] fix: add playlistExternalId to ingestRundown --- meteor/server/api/rest/v1/ingest.ts | 6 +++--- packages/job-worker/src/ingest/runOperation.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meteor/server/api/rest/v1/ingest.ts b/meteor/server/api/rest/v1/ingest.ts index 5f980b86562..33a6e2213a9 100644 --- a/meteor/server/api/rest/v1/ingest.ts +++ b/meteor/server/api/rest/v1/ingest.ts @@ -475,7 +475,7 @@ class IngestServerAPI implements IngestRestAPI { await runIngestOperation(studio._id, IngestJobs.UpdateRundown, { rundownExternalId: ingestRundown.externalId, - ingestRundown: ingestRundown, + ingestRundown: { ...ingestRundown, playlistExternalId: playlistId }, isCreateAction: true, rundownSource: { type: 'restApi', @@ -520,7 +520,7 @@ class IngestServerAPI implements IngestRestAPI { return runIngestOperation(studio._id, IngestJobs.UpdateRundown, { rundownExternalId: ingestRundown.externalId, - ingestRundown: ingestRundown, + ingestRundown: { ...ingestRundown, playlistExternalId: playlist.externalId }, isCreateAction: true, rundownSource: { type: 'restApi', @@ -560,7 +560,7 @@ class IngestServerAPI implements IngestRestAPI { await runIngestOperation(studio._id, IngestJobs.UpdateRundown, { rundownExternalId: existingRundown.externalId, - ingestRundown: ingestRundown, + ingestRundown: { ...ingestRundown, playlistExternalId: playlist.externalId }, isCreateAction: true, rundownSource: { type: 'restApi', diff --git a/packages/job-worker/src/ingest/runOperation.ts b/packages/job-worker/src/ingest/runOperation.ts index 5104e37b3fc..ed652571dae 100644 --- a/packages/job-worker/src/ingest/runOperation.ts +++ b/packages/job-worker/src/ingest/runOperation.ts @@ -278,7 +278,7 @@ async function updateSofieIngestRundown( payload: undefined, userEditStates: {}, rundownSource: nrcsIngestRundown.rundownSource, - playlistExternalId: undefined, + playlistExternalId: nrcsIngestRundown.playlistExternalId, } satisfies Complete, false ) From fd9663c025a5cebae7e81547eaba18158aa675a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Mon, 20 Oct 2025 13:23:20 +0200 Subject: [PATCH 027/291] fix: ingest api tests --- packages/openapi/src/__tests__/ingest.spec.ts | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/packages/openapi/src/__tests__/ingest.spec.ts b/packages/openapi/src/__tests__/ingest.spec.ts index 0b7ed133e1b..d0ad0c78446 100644 --- a/packages/openapi/src/__tests__/ingest.spec.ts +++ b/packages/openapi/src/__tests__/ingest.spec.ts @@ -1,4 +1,4 @@ -import { Configuration, IngestApi, Part, RundownTimingTypeEnum } from '../../client/ts/index.js' +import { Configuration, IngestApi, Part } from '../../client/ts/index.js' import { checkServer } from '../checkServer.js' import Logging from '../httpLogging.js' @@ -135,17 +135,11 @@ describe('Ingest API', () => { name: 'New rundown', type: 'external', resyncUrl: 'resyncUrl', - timing: { - type: RundownTimingTypeEnum.None, - expectedStart: 0, - expectedEnd: 0, - expectedDuration: 0, - }, + segments: [], } test('Can create rundown', async () => { const result = await ingestApi.postRundown({ studioId, playlistId: playlistIds[0], rundown }) - expect(result).toBe(undefined) }) @@ -232,11 +226,7 @@ describe('Ingest API', () => { externalId: 'segment1', name: 'Segment 1', rank: 0, - _float: true, - timing: { - expectedStart: 0, - expectedEnd: 0, - }, + parts: [], } test('Can create segment', async () => { @@ -351,8 +341,6 @@ describe('Ingest API', () => { externalId: 'part1', name: 'Part 1', rank: 0, - _float: true, - autoNext: true, payload: { type: 'CAMERA', guest: true, @@ -391,8 +379,6 @@ describe('Ingest API', () => { externalId: 'part1', name: 'Part 1', rank: 0, - _float: true, - autoNext: true, payload: { type: 'CAMERA', guest: true, @@ -426,8 +412,6 @@ describe('Ingest API', () => { externalId: 'part1', name: 'Part 1', rank: 0, - _float: true, - autoNext: true, payload: { type: 'CAMERA', guest: true, From bab6c921a01edaf50a7ccf06ee00b5fe9d0d4b51 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 9 Dec 2025 17:43:18 +0000 Subject: [PATCH 028/291] chore: add missing permissions to github workflows (#1564) --- .github/workflows/audit.yaml | 3 + .github/workflows/deploy-docs.yml | 84 ++++++++++++++++++++ .github/workflows/node.yaml | 25 +++--- .github/workflows/prune-container-images.yml | 4 + .github/workflows/prune-tags.yml | 3 + .github/workflows/sonar.yaml | 3 + .github/workflows/trivy.yml | 4 + 7 files changed, 116 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/deploy-docs.yml diff --git a/.github/workflows/audit.yaml b/.github/workflows/audit.yaml index 8334ab71489..1219cf4c8e8 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 diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 00000000000..b759dd915e4 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,84 @@ +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" + - name: restore node_modules + uses: actions/cache@v4 + with: + path: | + packages/node_modules + key: ${{ runner.os }}-${{ hashFiles('packages/yarn.lock') }} + - name: Prepare Environment + run: | + corepack enable + + cd packages + yarn config set cacheFolder /home/runner/publish-docs-cache + yarn install + 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 \ No newline at end of file diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 44032079af7..4734c78bcf2 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 @@ -103,6 +106,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: @@ -283,6 +291,10 @@ jobs: matrix: gateway-name: [playout-gateway, mos-gateway, "live-status-gateway"] + permissions: + contents: read + packages: write + steps: - uses: actions/checkout@v6 with: @@ -613,12 +625,12 @@ jobs: env: CI: true - publish-docs: - name: Publish Docs + build-docs: + name: Build Docs runs-on: ubuntu-latest - continue-on-error: true timeout-minutes: 15 + # This is just to ensure the docs build, another job performs the build & publish steps: - uses: actions/checkout@v6 with: @@ -656,13 +668,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 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/sonar.yaml b/.github/workflows/sonar.yaml index a7b8cb1d8c7..c85535898fa 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: diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 3f5379e7d22..d12492f71cc 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' }} From 8c199ef79c4dde39a46a61ba0876ac5be66adf73 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 9 Dec 2025 17:43:43 +0000 Subject: [PATCH 029/291] fix: disable some null subscriptions (#15) (#1571) --- .../publications/partsUI/publication.ts | 2 +- .../ui/ClockView/CameraScreen/index.tsx | 14 +++++------ .../client/ui/ClockView/PresenterScreen.tsx | 24 ++++++++++++------- .../src/client/ui/Prompter/PrompterView.tsx | 15 ++++++++---- .../client/ui/Status/package-status/index.tsx | 17 ++++++++----- 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/meteor/server/publications/partsUI/publication.ts b/meteor/server/publications/partsUI/publication.ts index 0c6039b19f5..bd25cc1f3e1 100644 --- a/meteor/server/publications/partsUI/publication.ts +++ b/meteor/server/publications/partsUI/publication.ts @@ -189,7 +189,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/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx b/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx index b633278d84e..ffcd7e6b6d6 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' import { Rundowns } from '../../../collections' -import { useSubscription, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' +import { useSubscription, useSubscriptionIfEnabled, useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' import { UIPartInstances, UIStudios } from '../../Collections' import { Rundown as RundownComponent } from './Rundown' import { useLocation } from 'react-router-dom' @@ -102,17 +102,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(() => { diff --git a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx index 8f1e218493b..69c1aba412a 100644 --- a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx @@ -4,7 +4,13 @@ import { PartUi } from '../SegmentTimeline/SegmentTimelineContainer' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { withTiming, WithTiming } from '../RundownView/RundownTiming/withTiming' -import { useSubscription, useSubscriptions, useTracker, withTracker } from '../../lib/ReactMeteorData/ReactMeteorData' +import { + useSubscription, + useSubscriptionIfEnabled, + useSubscriptions, + useTracker, + withTracker, +} from '../../lib/ReactMeteorData/ReactMeteorData' import { protectString, unprotectString } from '@sofie-automation/shared-lib/dist/lib/protectedString' import { getCurrentTime } from '../../lib/systemTime' import { PartInstance } from '@sofie-automation/meteor-lib/dist/collections/PartInstances' @@ -335,21 +341,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( () => { diff --git a/packages/webui/src/client/ui/Prompter/PrompterView.tsx b/packages/webui/src/client/ui/Prompter/PrompterView.tsx index 1a8f9e4b627..4e1f1668649 100644 --- a/packages/webui/src/client/ui/Prompter/PrompterView.tsx +++ b/packages/webui/src/client/ui/Prompter/PrompterView.tsx @@ -10,6 +10,7 @@ import { Translated, useGlobalDelayedTrackerUpdateState, useSubscription, + useSubscriptionIfEnabled, useSubscriptions, useTracker, } from '../../lib/ReactMeteorData/ReactMeteorData' @@ -653,11 +654,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( () => @@ -1011,7 +1012,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/Status/package-status/index.tsx b/packages/webui/src/client/ui/Status/package-status/index.tsx index ef92268e340..09e71cbcb24 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' +import { useSubscriptionIfEnabled, useTracker } from '../../../lib/ReactMeteorData/react-meteor-data' import { ExpectedPackageWorkStatus } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackageWorkStatuses' import { normalizeArrayToMap, unprotectString } from '../../../lib/tempLib' import { ExpectedPackageDB } from '@sofie-automation/corelib/dist/dataModel/ExpectedPackages' @@ -29,15 +29,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(), [], []) @@ -50,7 +51,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') From fbab7219ac88ac3b157e52f2a4378cdecc1b19c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:34:25 +0000 Subject: [PATCH 030/291] chore(deps): bump actions/cache from 4 to 5 (#1581) Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/cache dependency-version: '5' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/audit.yaml | 4 ++-- .github/workflows/deploy-docs.yml | 2 +- .github/workflows/node.yaml | 16 ++++++++-------- .github/workflows/sonar.yaml | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/audit.yaml b/.github/workflows/audit.yaml index 1219cf4c8e8..9dd265ae6f0 100644 --- a/.github/workflows/audit.yaml +++ b/.github/workflows/audit.yaml @@ -23,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 @@ -53,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 index b759dd915e4..056422a92ec 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -29,7 +29,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 diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 4734c78bcf2..d032834912c 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -28,7 +28,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 @@ -69,7 +69,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 @@ -175,7 +175,7 @@ jobs: - uses: ./.github/actions/setup-meteor if: steps.check-build-and-push.outputs.enable == 'true' - name: restore node_modules - uses: actions/cache@v4 + uses: actions/cache@v5 if: steps.check-build-and-push.outputs.enable == 'true' with: path: | @@ -358,7 +358,7 @@ jobs: node-version-file: ".node-version" - name: restore node_modules if: steps.check-build-and-push.outputs.enable == 'true' - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | packages/node_modules @@ -468,7 +468,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 @@ -536,7 +536,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 @@ -640,7 +640,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 @@ -684,7 +684,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/sonar.yaml b/.github/workflows/sonar.yaml index c85535898fa..1808b411e5e 100644 --- a/.github/workflows/sonar.yaml +++ b/.github/workflows/sonar.yaml @@ -33,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 From 15b18218a1e194f1c51899ef3269ce164cd18102 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:34:41 +0000 Subject: [PATCH 031/291] chore(deps): bump actions/upload-artifact from 5 to 6 (#1580) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish-libs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index 62c82b597d9..376545bde7a 100644 --- a/.github/workflows/publish-libs.yml +++ b/.github/workflows/publish-libs.yml @@ -187,7 +187,7 @@ jobs: yarn install --no-immutable - name: Upload release artifact - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: publish-dist path: | From 6e7cc51f1a8beec8a484e0167b26a7d0e377d2fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:34:48 +0000 Subject: [PATCH 032/291] chore(deps): bump actions/download-artifact from 6 to 7 (#1579) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish-libs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index 376545bde7a..7ad362018e6 100644 --- a/.github/workflows/publish-libs.yml +++ b/.github/workflows/publish-libs.yml @@ -222,7 +222,7 @@ jobs: node-version-file: '.node-version' - name: Download release artifact - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: publish-dist From 0860fd2ad033b7d122322fc9aa5a870e18de37ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 13:35:06 +0000 Subject: [PATCH 033/291] chore(deps): bump SonarSource/sonarqube-scan-action from 6 to 7 (#1578) Bumps [SonarSource/sonarqube-scan-action](https://github.com/sonarsource/sonarqube-scan-action) from 6 to 7. - [Release notes](https://github.com/sonarsource/sonarqube-scan-action/releases) - [Commits](https://github.com/sonarsource/sonarqube-scan-action/compare/v6...v7) --- updated-dependencies: - dependency-name: SonarSource/sonarqube-scan-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/sonar.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sonar.yaml b/.github/workflows/sonar.yaml index 1808b411e5e..f59312544dd 100644 --- a/.github/workflows/sonar.yaml +++ b/.github/workflows/sonar.yaml @@ -50,6 +50,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 }} From 016288e27471eb4812ee7cfc04cd981e6a9db00b Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 14 Jan 2026 15:35:59 +0100 Subject: [PATCH 034/291] chore: improve documentation around First Start (#1576) --- .../device-integrations/intro.md | 2 +- .../for-blueprint-developers/intro.md | 21 ++- .../user-guide/concepts-and-architecture.md | 2 +- .../user-guide/configuration/settings-view.md | 2 +- .../docs/user-guide/features/intro.md | 18 ++ .../installation/initial-sofie-core-setup.md | 4 +- .../installing-a-gateway/_category_.json | 2 +- .../input-gateway.md} | 10 +- .../installing-a-gateway/intro.md | 36 +++- .../installing-a-gateway/playout-gateway.md | 2 +- .../_category_.json | 2 +- ...sheet-support.md => google-spreadsheet.md} | 0 .../inews-gateway.md | 8 +- .../intro.md | 8 +- .../mos-gateway.md | 14 +- .../installation/installing-blueprints.md | 14 +- .../README.md | 2 +- .../_category_.json | 2 +- .../casparcg-server-installation.md | 2 +- .../installing-package-manager.md | 11 +- .../installing-sofie-server-core.md | 171 ++--------------- .../docs/user-guide/installation/intro.md | 26 +-- .../user-guide/installation/media-manager.md | 20 -- .../user-guide/installation/quick-install.md | 172 ++++++++++++++++++ .../user-guide/installation/rundown-editor.md | 2 +- .../documentation/docs/user-guide/intro.md | 2 +- 26 files changed, 305 insertions(+), 250 deletions(-) create mode 100644 packages/documentation/docs/user-guide/features/intro.md rename packages/documentation/docs/user-guide/installation/{installing-input-gateway.md => installing-a-gateway/input-gateway.md} (73%) rename packages/documentation/docs/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/{installing-sofie-with-google-spreadsheet-support.md => google-spreadsheet.md} (100%) delete mode 100644 packages/documentation/docs/user-guide/installation/media-manager.md create mode 100644 packages/documentation/docs/user-guide/installation/quick-install.md diff --git a/packages/documentation/docs/for-developers/device-integrations/intro.md b/packages/documentation/docs/for-developers/device-integrations/intro.md index dbf53b3a49a..9f46c4cd24e 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 responsibilites 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/for-blueprint-developers/intro.md b/packages/documentation/docs/for-developers/for-blueprint-developers/intro.md index a4b1ef62e6b..b9b5b66ab4c 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](../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/user-guide/concepts-and-architecture.md b/packages/documentation/docs/user-guide/concepts-and-architecture.md index 917222182b6..d001e6f8ad8 100644 --- a/packages/documentation/docs/user-guide/concepts-and-architecture.md +++ b/packages/documentation/docs/user-guide/concepts-and-architecture.md @@ -157,7 +157,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 b6ced3ae8cb..5ebfe136f8c 100644 --- a/packages/documentation/docs/user-guide/configuration/settings-view.md +++ b/packages/documentation/docs/user-guide/configuration/settings-view.md @@ -167,7 +167,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/installation/initial-sofie-core-setup.md b/packages/documentation/docs/user-guide/installation/initial-sofie-core-setup.md index c0672b3e55d..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 7fa55d484d6..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 + "position": 50 } \ No newline at end of file diff --git a/packages/documentation/docs/user-guide/installation/installing-input-gateway.md b/packages/documentation/docs/user-guide/installation/installing-a-gateway/input-gateway.md similarity index 73% rename from packages/documentation/docs/user-guide/installation/installing-input-gateway.md rename to packages/documentation/docs/user-guide/installation/installing-a-gateway/input-gateway.md index d17f66d247e..0aa8dcef30e 100644 --- a/packages/documentation/docs/user-guide/installation/installing-input-gateway.md +++ b/packages/documentation/docs/user-guide/installation/installing-a-gateway/input-gateway.md @@ -1,3 +1,7 @@ +--- +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. @@ -31,7 +35,7 @@ Currently, input gateway supports: ### 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. +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. @@ -39,6 +43,10 @@ If you would like to have the same trigger cause the same action in various Shif 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) 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 03bc8a53396..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,25 +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 & Media Manager Gateways +* [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 Gateway](playout-gateway.md) -* [Media Manager](../media-manager.md) +* [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/docs/user-guide/installation/installing-a-gateway/playout-gateway.md b/packages/documentation/docs/user-guide/installation/installing-a-gateway/playout-gateway.md index 0fd5f476267..25e077d2e94 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 b4c4ffc34d5..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 48659251a65..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,12 +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 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. +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/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 7c9c6fd5c44..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,9 +3,13 @@ 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. +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 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 8a2a60145c8..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,8 +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). -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. +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/docs/user-guide/installation/installing-blueprints.md b/packages/documentation/docs/user-guide/installation/installing-blueprints.md index 34796bbb1da..a56fdce59a9 100644 --- a/packages/documentation/docs/user-guide/installation/installing-blueprints.md +++ b/packages/documentation/docs/user-guide/installation/installing-blueprints.md @@ -1,17 +1,17 @@ --- -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 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). +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. @@ -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,8 +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 -- [Blueprints Supporting the Spreadsheet Gateway](https://github.com/SuperFlyTV/sofie-demo-blueprints) +- [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 4d35fb277dc..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,7 +2,7 @@ #### Prerequisites -* [Installed and running Sofie Core](../installing-sofie-server-core.md) +* [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) 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 d3e1e8979e3..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 + "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..16fce5f7626 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. 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 a38c3cc2285..07156ee779b 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) @@ -177,7 +172,7 @@ Note that each appContainer needs to use a different resourceId and will need it 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). +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 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 8d930108a4e..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,172 +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. -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. +:::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 c3a14c218bc..bcf3dd99481 100644 --- a/packages/documentation/docs/user-guide/installation/intro.md +++ b/packages/documentation/docs/user-guide/installation/intro.md @@ -1,27 +1,15 @@ --- -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 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. +:::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)**. +::: - - -## 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) +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 @@ -29,9 +17,9 @@ The _Sofie Core_ is the primary application for managing the broadcast but, ### 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/media-manager.md b/packages/documentation/docs/user-guide/installation/media-manager.md deleted file mode 100644 index 5c966aec573..00000000000 --- a/packages/documentation/docs/user-guide/installation/media-manager.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -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/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 4bf6b039a9f..e2e7ed4787b 100644 --- a/packages/documentation/docs/user-guide/intro.md +++ b/packages/documentation/docs/user-guide/intro.md @@ -33,7 +33,7 @@ This allows the producer to skip ahead or move backwards in a show, without the ### 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. +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 From 66ed96aa0ad0f66e3fab2a50db630f4517807869 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Tue, 20 Jan 2026 10:41:38 +0100 Subject: [PATCH 035/291] SOFIE-290 | fix line breaking on adlibs --- packages/webui/src/client/styles/shelf/dashboard.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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; From 0b69fc2264baf455533078d1d9704b2433ec5c7e Mon Sep 17 00:00:00 2001 From: ianshade Date: Fri, 27 Jun 2025 11:28:51 +0200 Subject: [PATCH 036/291] fix(EAV-640): make current segment timing consider partInstances --- .../src/topics/helpers/segmentTiming.ts | 11 ++++++++++- .../live-status-gateway/src/topics/segmentsTopic.ts | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) 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, } From b9628ad8cd52c936a37b75806a8f37d81cdd8958 Mon Sep 17 00:00:00 2001 From: ianshade Date: Thu, 22 Jan 2026 17:02:35 +0100 Subject: [PATCH 037/291] chore(EAV-640): add test cases for `calculateSegmentTiming` --- .../helpers/__tests__/segmentTiming.test.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 packages/live-status-gateway/src/topics/helpers/__tests__/segmentTiming.test.ts 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) + }) +}) From c57f0438436d75b6f3da75e98f67b0419ecf07a8 Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Fri, 23 Jan 2026 08:43:55 +0000 Subject: [PATCH 038/291] docs: Update documentation to reflect changed org (#1590) --- packages/documentation/docs/about-sofie.md | 10 ++--- .../for-developers/contribution-guidelines.md | 44 ++++++++++++------- .../sync-ingest-changes.md | 4 +- .../docs/for-developers/libraries.md | 6 +-- .../docs/user-guide/further-reading.md | 8 ++-- .../casparcg-server-installation.md | 4 +- .../docs/user-guide/supported-devices.md | 1 - packages/documentation/docusaurus.config.js | 2 +- 8 files changed, 43 insertions(+), 36 deletions(-) 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 11071791583..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,26 +33,28 @@ 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: -* 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 +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. 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 @@ -65,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. @@ -72,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. @@ -82,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​ @@ -92,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/for-blueprint-developers/sync-ingest-changes.md b/packages/documentation/docs/for-developers/for-blueprint-developers/sync-ingest-changes.md index 0d34a7c9359..76d861aedbf 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 943938848c3..8b5e0c3b04e 100644 --- a/packages/documentation/docs/for-developers/libraries.md +++ b/packages/documentation/docs/for-developers/libraries.md @@ -46,11 +46,11 @@ 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. - [**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/)_ +- [**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/further-reading.md b/packages/documentation/docs/user-guide/further-reading.md index d78295d87b5..a8f797172a2 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/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 16fce5f7626..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 @@ -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/supported-devices.md b/packages/documentation/docs/user-guide/supported-devices.md index 5b2016babe8..66f0f02f52f 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 4662c5fe408..60eb60af687 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: 'warn', From 3e5d7f2d4a1f84f525c801abc96d308a73fef7a6 Mon Sep 17 00:00:00 2001 From: ianshade Date: Fri, 5 Sep 2025 10:02:09 +0200 Subject: [PATCH 039/291] fix(EAV-693): make `getPlayheadTrackingInfinitesForPart` respect pieces dynamically converted to infinites --- packages/corelib/src/dataModel/PieceInstance.ts | 3 +++ packages/corelib/src/playout/infinites.ts | 7 +++++-- .../implementation/PlayoutPieceInstanceModelImpl.ts | 12 +++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) 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/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/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() + } } } From 3581f8af7ccabdc762d7e096c124b57f773d976b Mon Sep 17 00:00:00 2001 From: ianshade Date: Tue, 9 Sep 2025 18:12:42 +0200 Subject: [PATCH 040/291] fix(EAV-693): allow current pieces to be modified from `onSetAsNext` It was an artificial limitation --- .../blueprints-integration/src/context/onSetAsNextContext.ts | 2 +- .../job-worker/src/blueprints/context/OnSetAsNextContext.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/blueprints-integration/src/context/onSetAsNextContext.ts b/packages/blueprints-integration/src/context/onSetAsNextContext.ts index 9e729ce4029..8a0e45cf21a 100644 --- a/packages/blueprints-integration/src/context/onSetAsNextContext.ts +++ b/packages/blueprints-integration/src/context/onSetAsNextContext.ts @@ -69,7 +69,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/job-worker/src/blueprints/context/OnSetAsNextContext.ts b/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts index a476c1c593e..13bf42c7374 100644 --- a/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts +++ b/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts @@ -22,7 +22,6 @@ 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' @@ -116,9 +115,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) } From 64d9dcffe924cad2f70b0e3db9f6fc8f528f3b88 Mon Sep 17 00:00:00 2001 From: ianshade Date: Tue, 9 Sep 2025 18:22:25 +0200 Subject: [PATCH 041/291] fix(EAV-693): make it possible to update infinite piece continuations in the current part it was an artificial limitation those pieces are not recreated by `syncInfinitesForNextPartInstance` as previously believed --- .../context/services/PartAndPieceInstanceActionService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts b/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts index f18e2e3a0c1..16829c355a6 100644 --- a/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts +++ b/packages/job-worker/src/blueprints/context/services/PartAndPieceInstanceActionService.ts @@ -313,7 +313,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') } From baf033b9c633db5e15bd4186977d5048aa8eb812 Mon Sep 17 00:00:00 2001 From: ianshade Date: Sat, 24 Jan 2026 08:12:39 +0100 Subject: [PATCH 042/291] chore(EAV-693): add infintes conversion tests --- .../src/playout/__tests__/infinites.test.ts | 41 ++++++++++ .../PartAndPieceInstanceActionService.test.ts | 75 +++++++++++++++++++ 2 files changed, 116 insertions(+) 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/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', () => { From 08c120f9e1f02e0da29d490777eb04ca69aae5b0 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:00:30 +0100 Subject: [PATCH 043/291] SOFIE-288 | add info about AB channels (players) to LSG --- .../pieceStatus/pieceStatus-example.yaml | 4 ++ .../piece/pieceStatus/pieceStatus.yaml | 22 +++++++++++ .../src/generated/asyncapi.yaml | 27 +++++++++++++ .../src/generated/schema.ts | 20 ++++++++++ .../src/topics/activePiecesTopic.ts | 8 +++- .../src/topics/activePlaylistTopic.ts | 2 + .../src/topics/helpers/pieceStatus.ts | 39 ++++++++++++++++++- 7 files changed, 119 insertions(+), 3 deletions(-) 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..12161d0bf5a 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,7 @@ outputLayer: 'PGM' tags: ['camera'] publicData: switcherSource: 1 +abSessions: + - poolName: 'VTR' + sessionName: 'clip_intro' + playerId: 1 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..84dfe9cfa66 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 @@ -1,4 +1,21 @@ $defs: + abSessionAssignment: + 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 pieceStatus: type: object title: PieceStatus @@ -22,6 +39,11 @@ $defs: type: string publicData: description: Optional arbitrary data + abSessions: + description: AB playback session assignments for this Piece + type: array + items: + $ref: '#/$defs/abSessionAssignment' required: [id, name, sourceLayer, outputLayer] additionalProperties: false examples: diff --git a/packages/live-status-gateway-api/src/generated/asyncapi.yaml b/packages/live-status-gateway-api/src/generated/asyncapi.yaml index b747e97a84c..b314a66bcdb 100644 --- a/packages/live-status-gateway-api/src/generated/asyncapi.yaml +++ b/packages/live-status-gateway-api/src/generated/asyncapi.yaml @@ -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: diff --git a/packages/live-status-gateway-api/src/generated/schema.ts b/packages/live-status-gateway-api/src/generated/schema.ts index b8f0970662a..54a4a5e0729 100644 --- a/packages/live-status-gateway-api/src/generated/schema.ts +++ b/packages/live-status-gateway-api/src/generated/schema.ts @@ -245,6 +245,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 } /** @@ -912,6 +931,7 @@ export { ActivePlaylistEvent, CurrentPartStatus, PieceStatus, + AbSessionAssignment, CurrentPartTiming, CurrentSegment, CurrentSegmentTiming, 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..dd5e1c82cf1 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -50,6 +50,8 @@ const PLAYLIST_KEYS = [ 'timing', 'startedPlayback', 'quickLoop', + 'assignedAbSessions', + 'trackedAbSessions', ] as const type Playlist = PickKeys diff --git a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts index eebcb218037..32562ba8a3f 100644 --- a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts +++ b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts @@ -3,13 +3,20 @@ 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 { 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 = [] + + 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 } From b99ad65d2fb8c8eeadf0a230e71ff90ec60a44f0 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:15:10 +0100 Subject: [PATCH 044/291] SOFIE-288 | apply code review suggestions --- .../abSessionAssignment-example.yaml | 3 +++ .../pieceStatus/abSessionAssignment.yaml | 16 ++++++++++++++++ .../pieceStatus/pieceStatus-example.yaml | 4 +--- .../piece/pieceStatus/pieceStatus.yaml | 19 +------------------ .../src/topics/activePlaylistTopic.ts | 2 -- .../src/topics/helpers/pieceStatus.ts | 4 ++-- 6 files changed, 23 insertions(+), 25 deletions(-) create mode 100644 packages/live-status-gateway-api/api/components/piece/pieceStatus/abSessionAssignment-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/piece/pieceStatus/abSessionAssignment.yaml 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 12161d0bf5a..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 @@ -6,6 +6,4 @@ tags: ['camera'] publicData: switcherSource: 1 abSessions: - - poolName: 'VTR' - sessionName: 'clip_intro' - playerId: 1 + - $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 84dfe9cfa66..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 @@ -1,21 +1,4 @@ $defs: - abSessionAssignment: - 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 pieceStatus: type: object title: PieceStatus @@ -43,7 +26,7 @@ $defs: description: AB playback session assignments for this Piece type: array items: - $ref: '#/$defs/abSessionAssignment' + $ref: './abSessionAssignment.yaml' required: [id, name, sourceLayer, outputLayer] additionalProperties: false examples: diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index dd5e1c82cf1..f1f29c940d4 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -50,8 +50,6 @@ const PLAYLIST_KEYS = [ 'timing', 'startedPlayback', 'quickLoop', - 'assignedAbSessions', - 'trackedAbSessions', ] as const type Playlist = PickKeys diff --git a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts index 32562ba8a3f..b82099ef4e5 100644 --- a/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts +++ b/packages/live-status-gateway/src/topics/helpers/pieceStatus.ts @@ -1,7 +1,7 @@ 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' @@ -33,7 +33,7 @@ function getAbSessions(pieceInstance: PieceInstanceMin, playlist?: PlaylistAbSes return [] } - const abSessions = [] + const abSessions: AbSessionAssignment[] = [] for (const session of pieceInstance.piece.abSessions) { const trackedSession = playlist.trackedAbSessions.find( From a20eab9d533ea9464f598b46e34d7470d084ab81 Mon Sep 17 00:00:00 2001 From: olzzon Date: Thu, 31 Jul 2025 12:52:15 +0200 Subject: [PATCH 045/291] feat: directors screen keep showing countdown if first part is untimed --- .../client/ui/ClockView/DirectorScreen.tsx | 173 +++++++++--------- 1 file changed, 91 insertions(+), 82 deletions(-) diff --git a/packages/webui/src/client/ui/ClockView/DirectorScreen.tsx b/packages/webui/src/client/ui/ClockView/DirectorScreen.tsx index 98b36e7f32d..2702e97c754 100644 --- a/packages/webui/src/client/ui/ClockView/DirectorScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/DirectorScreen.tsx @@ -493,15 +493,11 @@ function DirectorScreenRender({ 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 @@ -588,89 +584,102 @@ function DirectorScreenRender({ // Current Part: }
-
- - {playlist.currentPartInfo?.partInstanceId ? ( - - - - ) : null} -
- {currentPartInstance && currentShowStyleBaseId ? ( + {!isFirstPieceAndNoDuration ? ( <> -
- + + {playlist.currentPartInfo?.partInstanceId ? ( + + + + ) : null}
-
-
- - {currentPlayerEl} -
-
- - - - {' '} - - +
+ - +
+
+
+ + {currentPlayerEl} +
+
+ + + + {' '} + + + +
+
+ + )} + + ) : ( + expectedStart && ( +
+
+
+
Time to planned start
- - ) : null} - {expectedStartCountdown} + ) + )}
{ // Next Part: From b4e79c4371f7003a0786e879b1786979d5faa7cf Mon Sep 17 00:00:00 2001 From: olzzon Date: Thu, 31 Jul 2025 12:38:58 +0200 Subject: [PATCH 046/291] feat: count label - time to planned start --- .../src/client/styles/countdown/director.scss | 86 +++++++++++++++---- .../src/client/styles/counterComponents.scss | 2 - .../client/ui/ClockView/DirectorScreen.tsx | 18 ++-- 3 files changed, 77 insertions(+), 29 deletions(-) diff --git a/packages/webui/src/client/styles/countdown/director.scss b/packages/webui/src/client/styles/countdown/director.scss index 7cc5dd8813e..a053bc45864 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; 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/ui/ClockView/DirectorScreen.tsx b/packages/webui/src/client/ui/ClockView/DirectorScreen.tsx index 2702e97c754..8b10d0b427a 100644 --- a/packages/webui/src/client/ui/ClockView/DirectorScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/DirectorScreen.tsx @@ -558,7 +558,7 @@ function DirectorScreenRender({
) : null} {expectedEnd ? ( -
+
@@ -670,16 +670,14 @@ function DirectorScreenRender({ )} - ) : ( - expectedStart && ( -
-
- -
-
Time to planned start
+ ) : expectedStart ? ( +
+
+
- ) - )} +
Time to planned start
+
+ ) : null}
{ // Next Part: From c77557eae62ba1cb28fd0402213c7a9908c51f43 Mon Sep 17 00:00:00 2001 From: ianshade Date: Fri, 26 Sep 2025 13:01:01 +0200 Subject: [PATCH 047/291] feat(EAV-730): allow custom timeout on TSR actions --- .../src/context/executeTsrActionContext.ts | 4 +++- packages/job-worker/src/blueprints/context/OnTakeContext.ts | 5 +++-- .../src/blueprints/context/RundownActivationContext.ts | 5 +++-- packages/job-worker/src/blueprints/context/adlibActions.ts | 5 +++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/blueprints-integration/src/context/executeTsrActionContext.ts b/packages/blueprints-integration/src/context/executeTsrActionContext.ts index a18fa45c823..ef7b8873bca 100644 --- a/packages/blueprints-integration/src/context/executeTsrActionContext.ts +++ b/packages/blueprints-integration/src/context/executeTsrActionContext.ts @@ -8,6 +8,8 @@ export interface IExecuteTSRActionsContext { executeTSRAction( deviceId: PeripheralDeviceId, actionId: string, - payload: Record + payload: Record, + /** Timeout for the action, default: 3000 */ + timeoutMs?: number ): Promise } diff --git a/packages/job-worker/src/blueprints/context/OnTakeContext.ts b/packages/job-worker/src/blueprints/context/OnTakeContext.ts index 1eecd973513..09e49f3f338 100644 --- a/packages/job-worker/src/blueprints/context/OnTakeContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTakeContext.ts @@ -158,9 +158,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) } getCurrentTime(): number { diff --git a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts index a1c6849245f..1019d555010 100644 --- a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts @@ -59,9 +59,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 { diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 05d9a31791d..3537731fd7c 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -241,9 +241,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 { From e3f16dce97dd980353b7004d7c55438e701b9e58 Mon Sep 17 00:00:00 2001 From: ianshade Date: Tue, 28 Oct 2025 10:16:09 +0100 Subject: [PATCH 048/291] feat(EAV-794): provide `infiniteInstanceId` and `infiniteInstanceIndex` in `IBlueprintPieceInstance` --- .../blueprints-integration/src/documents/pieceInstance.ts | 5 +++++ packages/job-worker/src/blueprints/context/lib.ts | 2 ++ 2 files changed, 7 insertions(+) diff --git a/packages/blueprints-integration/src/documents/pieceInstance.ts b/packages/blueprints-integration/src/documents/pieceInstance.ts index 9cca9ff483b..bbcb06da289 100644 --- a/packages/blueprints-integration/src/documents/pieceInstance.ts +++ b/packages/blueprints-integration/src/documents/pieceInstance.ts @@ -29,6 +29,11 @@ export interface IBlueprintPieceInstance diff --git a/packages/job-worker/src/blueprints/context/lib.ts b/packages/job-worker/src/blueprints/context/lib.ts index 23b50b18688..58f7a060adb 100644 --- a/packages/job-worker/src/blueprints/context/lib.ts +++ b/packages/job-worker/src/blueprints/context/lib.ts @@ -160,6 +160,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), From 9a11560ad98f32086b73378a2d028d5eca1de0db Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:39:50 +0100 Subject: [PATCH 049/291] SOFIE-252 | make timing of rehearsal end relative to the first TAKE --- .../src/client/lib/rundownPlaylistUtil.ts | 17 +++ .../src/client/ui/ClockView/ClockView.tsx | 2 +- .../{ => DirectorScreen}/DirectorScreen.tsx | 121 ++++++++---------- .../DirectorScreen/DirectorScreenTop.tsx | 86 +++++++++++++ 4 files changed, 158 insertions(+), 68 deletions(-) rename packages/webui/src/client/ui/ClockView/{ => DirectorScreen}/DirectorScreen.tsx (88%) create mode 100644 packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreenTop.tsx 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/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/DirectorScreen.tsx b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx similarity index 88% rename from packages/webui/src/client/ui/ClockView/DirectorScreen.tsx rename to packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx index 8b10d0b427a..eb0612ae310 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,24 @@ 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/RundownTiming/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 { 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 +138,7 @@ export interface DirectorScreenTrackedProps { nextShowStyleBaseId: ShowStyleBaseId | undefined showStyleBaseIds: ShowStyleBaseId[] rundownIds: RundownId[] + partInstanceToCountTimeFrom: PartInstance | undefined } function getShowStyleBaseIdSegmentPartUi( @@ -248,6 +244,7 @@ const getDirectorScreenReactive = (props: DirectorScreenProps): DirectorScreenTr restoredFromSnapshotId: 0, }, }) + const segments: Array = [] let showStyleBaseIds: ShowStyleBaseId[] = [] let rundowns: Rundown[] = [] @@ -263,17 +260,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 +329,7 @@ const getDirectorScreenReactive = (props: DirectorScreenProps): DirectorScreenTr } } } + return { studio, segments, @@ -341,6 +346,7 @@ const getDirectorScreenReactive = (props: DirectorScreenProps): DirectorScreenTr nextSegment, nextPartInstance: nextPartInstanceUi, nextShowStyleBaseId, + partInstanceToCountTimeFrom: partInstanceToCountTimeFromUi, } } @@ -372,7 +378,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 +396,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 +438,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,11 +509,6 @@ 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 // Show countdown if it is the first segment and the current part is untimed: const currentSegmentIsFirst = currentSegment?._rank === 0 @@ -548,37 +565,7 @@ function DirectorScreenRender({ return (
-
- {expectedEnd ? ( -
-
- -
- {t('Planned End')} -
- ) : null} - {expectedEnd ? ( -
-
- -
- {t('Time to planned end')} -
- ) : ( -
-
- - {t('Time since planned end')} -
-
- )} -
-
- -
- {t('Over/Under')} -
-
+
{ // Current Part: @@ -675,7 +662,7 @@ function DirectorScreenRender({
-
Time to planned start
+
{t('Time to planned start')}
) : null}
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')} +
+
+ ) +} From d469cbd36f3f6633637e8e63e5628790828d2806 Mon Sep 17 00:00:00 2001 From: ianshade Date: Wed, 10 Dec 2025 11:39:36 +0100 Subject: [PATCH 050/291] fix(EAV-737): run timeline fast-track when it's safer and more correct prevents from submitting an incorrect timeline before the model is ready to be saved (if `updateTimeline` is not the last thing before saving, something might still throw, leading to disposal of the model); makes sure it runs after deferBeforeSave callback, as they might need to run before submitting the timeline (e.g. in order for onRundownActivate to complete device preparation before the rundown baseline reaches TSR) --- .../model/implementation/PlayoutModelImpl.ts | 3 +++ .../src/playout/timeline/generate.ts | 22 +++---------------- .../playout/timings/timelineTriggerTime.ts | 16 +++++--------- .../studio/model/StudioPlayoutModelImpl.ts | 3 +++ 4 files changed, 14 insertions(+), 30 deletions(-) diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 52253f1a2f5..bebcd141f7a 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -696,6 +696,9 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou // 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. diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 44acd584a40..13f5b532ea8 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' @@ -128,7 +128,7 @@ export async function updateStudioTimeline( logAnyRemainingNowTimes(context, baselineObjects) } - const timelineHash = saveTimeline(context, playoutModel, baselineObjects, versions, undefined) + const timelineHash = playoutModel.setTimeline(baselineObjects, versions, undefined).timelineHash if (studioBaseline) { updateBaselineExpectedPackagesOnStudio(context, playoutModel, studioBaseline) @@ -163,7 +163,7 @@ export async function updateTimeline(context: JobContext, playoutModel: PlayoutM logAnyRemainingNowTimes(context, timelineObjs) } - const timelineHash = saveTimeline(context, playoutModel, timelineObjs, versions, regenerateTimelineToken) + const timelineHash = playoutModel.setTimeline(timelineObjs, versions, regenerateTimelineToken).timelineHash logger.verbose(`updateTimeline done, hash: "${timelineHash}"`) if (span) span.end() @@ -229,22 +229,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 diff --git a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts index 831b651d9e3..c700e54b989 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, - timelineObjs, - // Preserve some current values: - timeline.generationVersions, + const timelineHash = studioPlayoutModel.setTimeline( + timelineObjs, + timeline.generationVersions, timeline.regenerateTimelineToken - ) + ).timelineHash logger.verbose(`timelineTriggerTime: Updated Timeline, hash: "${timelineHash}"`) } diff --git a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts index 55a8e97808a..c72ea3ff2b2 100644 --- a/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts +++ b/packages/job-worker/src/studio/model/StudioPlayoutModelImpl.ts @@ -127,6 +127,9 @@ export class StudioPlayoutModelImpl implements StudioPlayoutModel { // 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 From 77dba30285026bf01292121afcfc40661ddc4a96 Mon Sep 17 00:00:00 2001 From: ianshade Date: Fri, 12 Dec 2025 14:38:02 +0100 Subject: [PATCH 051/291] fix(EAV-737): prevent latency when running long callbacks after updating the timeline moves timeline generation to `saveAllToDatabase` --- .../src/playout/model/PlayoutModel.ts | 6 +++ .../model/implementation/PlayoutModelImpl.ts | 51 +++++++++++++++++++ .../src/playout/timeline/generate.ts | 33 +++--------- 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/packages/job-worker/src/playout/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index 0dff06ff919..867f0420cd2 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -387,6 +387,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/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index bebcd141f7a..1561fa369ff 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -71,6 +71,14 @@ 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, +} from '../../timeline/generate.js' +import { deNowifyMultiGatewayTimeline } from '../../timeline/multi-gateway.js' export class PlayoutModelReadonlyImpl implements PlayoutModelReadonly { public readonly playlistId: RundownPlaylistId @@ -315,6 +323,7 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou #playlistHasChanged = false #timelineHasChanged = false + #timelineNeedsRegeneration = false #pendingPartInstanceTimingEvents = new Set() @@ -694,6 +703,12 @@ 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: @@ -889,6 +904,10 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou return result } + markTimelineNeedsUpdate(): void { + this.#timelineNeedsRegeneration = true + } + /** Notifications */ async getAllNotifications( @@ -925,6 +944,38 @@ 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 + } = await getTimelineRundown(this.context, this as PlayoutModel) + + flattenAndProcessTimelineObjects(this.context, timelineObjs) + + preserveOrReplaceNowTimesInObjects(this, timelineObjs) + + if (this.isMultiGatewayMode) { + deNowifyMultiGatewayTimeline(this as PlayoutModel, 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/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 13f5b532ea8..1083e571e5f 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -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' @@ -140,39 +139,21 @@ export async function updateStudioTimeline( 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 = playoutModel.setTimeline(timelineObjs, versions, regenerateTimelineToken).timelineHash - 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 +183,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) { @@ -287,7 +268,7 @@ function getPartInstanceTimelineInfo( /** * Returns timeline objects related to rundowns in a studio */ -async function getTimelineRundown( +export async function getTimelineRundown( context: JobContext, playoutModel: PlayoutModel ): Promise<{ @@ -541,7 +522,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: From a79cd69689e2eb3daddfc463ca78982155424ee0 Mon Sep 17 00:00:00 2001 From: ianshade Date: Thu, 29 Jan 2026 22:30:55 +0100 Subject: [PATCH 052/291] fix(EAV-737): prevent latency caused by onRundownDeActivate --- .../model/implementation/PlayoutModelImpl.ts | 9 ++- .../src/playout/timeline/generate.ts | 64 +++++++++++-------- .../playout/timings/timelineTriggerTime.ts | 4 +- .../src/studio/model/StudioPlayoutModel.ts | 6 ++ .../studio/model/StudioPlayoutModelImpl.ts | 42 ++++++++++++ 5 files changed, 93 insertions(+), 32 deletions(-) diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 1561fa369ff..86e2d089150 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -77,6 +77,7 @@ import { flattenAndProcessTimelineObjects, preserveOrReplaceNowTimesInObjects, logAnyRemainingNowTimes, + getStudioTimeline, } from '../../timeline/generate.js' import { deNowifyMultiGatewayTimeline } from '../../timeline/multi-gateway.js' @@ -953,15 +954,17 @@ export class PlayoutModelImpl extends PlayoutModelReadonlyImpl implements Playou versions, objs: timelineObjs, timingContext: timingInfo, - regenerateTimelineToken - } = await getTimelineRundown(this.context, this as PlayoutModel) + 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 as PlayoutModel, timelineObjs, timingInfo) + deNowifyMultiGatewayTimeline(this, timelineObjs, timingInfo) logAnyRemainingNowTimes(this.context, timelineObjs) } diff --git a/packages/job-worker/src/playout/timeline/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index 1083e571e5f..afabc8d7a3f 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -66,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 @@ -118,22 +112,38 @@ 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 = playoutModel.setTimeline(baselineObjects, versions, undefined).timelineHash + 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() } diff --git a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts index c700e54b989..399f999e7ad 100644 --- a/packages/job-worker/src/playout/timings/timelineTriggerTime.ts +++ b/packages/job-worker/src/playout/timings/timelineTriggerTime.ts @@ -203,8 +203,8 @@ function timelineTriggerTimeInner( } if (tlChanged) { const timelineHash = studioPlayoutModel.setTimeline( - timelineObjs, - timeline.generationVersions, + timelineObjs, + timeline.generationVersions, timeline.regenerateTimelineToken ).timelineHash 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 c72ea3ff2b2..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,6 +161,12 @@ 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: From 535c2888f0269bcef57228c3cfdeac53e40e6373 Mon Sep 17 00:00:00 2001 From: ianshade Date: Mon, 26 Jan 2026 13:39:40 +0100 Subject: [PATCH 053/291] fix(EAV-917): preserve other `enable` properties when denowifying infinites --- .../timeline/__tests__/multi-gateway.test.ts | 107 ++++++++++++++++++ .../src/playout/timeline/multi-gateway.ts | 8 +- 2 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 packages/job-worker/src/playout/timeline/__tests__/multi-gateway.test.ts 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/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}"` ) From 5ae693e062b8dc5580e1bc102f7ab6236cb42be5 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Wed, 4 Feb 2026 08:21:53 +0100 Subject: [PATCH 054/291] SOFIE-285 | enable piece actions in context menu --- .../webui/src/client/styles/contextMenu.scss | 1 + .../ui/SegmentTimeline/SegmentContextMenu.tsx | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/webui/src/client/styles/contextMenu.scss b/packages/webui/src/client/styles/contextMenu.scss index 40e4c97e670..c3f21ff1e78 100644 --- a/packages/webui/src/client/styles/contextMenu.scss +++ b/packages/webui/src/client/styles/contextMenu.scss @@ -51,6 +51,7 @@ nav.react-contextmenu { 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; diff --git a/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx index 0118421bd10..2576326f5d6 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentContextMenu.tsx @@ -15,6 +15,7 @@ import { PartUi, SegmentUi } from './SegmentTimelineContainer.js' import { 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' @@ -199,6 +200,22 @@ export const SegmentContextMenu = withTranslation()( isFormEditable={isPartEditAble} /> + {piece && piece.instance.piece.userEditOperations && ( + + )} + {this.props.enableUserEdits && ( <>
From e0ef88094a4d34fa27b44641919f20579371a047 Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:59:19 +0000 Subject: [PATCH 055/291] fix: Add missing imports --- .../webui/src/client/ui/ClockView/CameraScreen/index.tsx | 2 +- packages/webui/src/client/ui/ClockView/PresenterScreen.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx b/packages/webui/src/client/ui/ClockView/CameraScreen/index.tsx index f5f8451fc21..da547030ac6 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' diff --git a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx index 9d19fcaa129..22235b01d24 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' From a22a0a079750c9dafcd07a446958b2521ce0d01a Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:16:16 +0000 Subject: [PATCH 056/291] docs: Fix link --- .../docs/for-developers/for-blueprint-developers/intro.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b9b5b66ab4c..0dfe9486a1b 100644 --- a/packages/documentation/docs/for-developers/for-blueprint-developers/intro.md +++ b/packages/documentation/docs/for-developers/for-blueprint-developers/intro.md @@ -26,7 +26,7 @@ Currently, there are three types of Blueprints: # 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](../concepts-and-architecture.md) such as Segments, Parts, Pieces and AdLibs. +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 From 92b183b201041bdb1197b3763b421e1a84c4bf1e Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:34:28 +0000 Subject: [PATCH 057/291] chore(release): 26.3.0-0 --- meteor/CHANGELOG.md | 242 ++++++++++++++++++ meteor/package.json | 2 +- meteor/yarn.lock | 18 +- packages/blueprints-integration/CHANGELOG.md | 42 +++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 +- packages/lerna.json | 8 +- packages/live-status-gateway-api/package.json | 2 +- packages/live-status-gateway/package.json | 12 +- packages/meteor-lib/package.json | 8 +- packages/mos-gateway/CHANGELOG.md | 22 ++ packages/mos-gateway/package.json | 6 +- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 24 ++ packages/playout-gateway/package.json | 6 +- packages/server-core-integration/CHANGELOG.md | 17 ++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/webui/package.json | 10 +- packages/yarn.lock | 58 ++--- 22 files changed, 426 insertions(+), 79 deletions(-) diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index 62331c2da38..58e28768624 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,248 @@ 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-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/package.json b/meteor/package.json index 8397b76ef54..405e1c3b028 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-0", "private": true, "engines": { "node": ">=22.20.0" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 651ec538050..cfca919193f 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1158,7 +1158,7 @@ __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-0" tslib: "npm:^2.8.1" type-fest: "npm:^4.33.0" languageName: node @@ -1194,8 +1194,8 @@ __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-0" + "@sofie-automation/shared-lib": "npm:26.3.0-0" fast-clone: "npm:^1.5.13" i18next: "npm:^21.10.0" influx: "npm:^5.9.7" @@ -1227,9 +1227,9 @@ __metadata: 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" + "@sofie-automation/blueprints-integration": "npm:26.3.0-0" + "@sofie-automation/corelib": "npm:26.3.0-0" + "@sofie-automation/shared-lib": "npm:26.3.0-0" amqplib: "npm:^0.10.5" deepmerge: "npm:^4.3.1" elastic-apm-node: "npm:^4.11.0" @@ -1249,9 +1249,9 @@ __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-0" + "@sofie-automation/corelib": "npm:26.3.0-0" + "@sofie-automation/shared-lib": "npm:26.3.0-0" deep-extend: "npm:0.6.0" semver: "npm:^7.6.3" type-fest: "npm:^4.33.0" diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index ffe54d1390c..1219a0117eb 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,48 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [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/package.json b/packages/blueprints-integration/package.json index ec0dd2915ae..587f64b14a9 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-0", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -36,7 +36,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "1.53.0-in-development", + "@sofie-automation/shared-lib": "26.3.0-0", "tslib": "^2.8.1", "type-fest": "^4.33.0" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index bf370df72fc..52609eee059 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-0", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -37,8 +37,8 @@ "/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-0", + "@sofie-automation/shared-lib": "26.3.0-0", "fast-clone": "^1.5.13", "i18next": "^21.10.0", "influx": "^5.9.7", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index f89ced26c1b..e62f3fc7bbb 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-0", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 9241feb122c..0dfcf8e422b 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-0", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -37,9 +37,9 @@ ], "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", + "@sofie-automation/blueprints-integration": "26.3.0-0", + "@sofie-automation/corelib": "26.3.0-0", + "@sofie-automation/shared-lib": "26.3.0-0", "amqplib": "^0.10.5", "deepmerge": "^4.3.1", "elastic-apm-node": "^4.11.0", diff --git a/packages/lerna.json b/packages/lerna.json index 26ccbc1db55..18b18377307 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-0", + "npmClient": "yarn", + "$schema": "node_modules/lerna/schemas/lerna-schema.json" +} \ No newline at end of file diff --git a/packages/live-status-gateway-api/package.json b/packages/live-status-gateway-api/package.json index b5b08e44dc2..e59a8a2e2f3 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-0", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index f20b973b745..c94be7ebab0 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-0", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -47,11 +47,11 @@ "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", + "@sofie-automation/blueprints-integration": "26.3.0-0", + "@sofie-automation/corelib": "26.3.0-0", + "@sofie-automation/live-status-gateway-api": "26.3.0-0", + "@sofie-automation/server-core-integration": "26.3.0-0", + "@sofie-automation/shared-lib": "26.3.0-0", "debug": "^4.4.0", "fast-clone": "^1.5.13", "influx": "^5.9.7", diff --git a/packages/meteor-lib/package.json b/packages/meteor-lib/package.json index 5ab52f726cc..7909b59647b 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-0", "private": true, "description": "Temporary internal library for some types shared by meteor and webui", "main": "dist/index.js", @@ -38,9 +38,9 @@ ], "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-0", + "@sofie-automation/corelib": "26.3.0-0", + "@sofie-automation/shared-lib": "26.3.0-0", "deep-extend": "0.6.0", "semver": "^7.6.3", "type-fest": "^4.33.0", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index d924c004fdd..3caa615a20b 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [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/package.json b/packages/mos-gateway/package.json index 55b728c761b..8d155f417a4 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-0", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -62,8 +62,8 @@ ], "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-0", + "@sofie-automation/shared-lib": "26.3.0-0", "tslib": "^2.8.1", "type-fest": "^4.33.0", "underscore": "^1.13.7", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index c4c4a887c4e..317dbe267e8 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-0", "license": "MIT", "repository": { "type": "git", diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index c77632bc668..108b127488b 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,30 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [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/package.json b/packages/playout-gateway/package.json index 1c3033d736b..f464b15a4f3 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-0", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -52,8 +52,8 @@ "production" ], "dependencies": { - "@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-0", + "@sofie-automation/shared-lib": "26.3.0-0", "debug": "^4.4.0", "influx": "^5.9.7", "timeline-state-resolver": "10.0.0-nightly-release53-20251217-143607-df590aa96.0", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index 5853b34a212..61143adee82 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [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/package.json b/packages/server-core-integration/package.json index 5670ac9d608..442cd123c04 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-0", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -73,7 +73,7 @@ }, "dependencies": { "@koa/router": "^14.0.0", - "@sofie-automation/shared-lib": "1.53.0-in-development", + "@sofie-automation/shared-lib": "26.3.0-0", "ejson": "^2.2.3", "faye-websocket": "^0.11.4", "got": "^11.8.6", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 998995392cb..2eb6976979b 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-0", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/webui/package.json b/packages/webui/package.json index b736f7e979c..7587a91a84e 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-0", "type": "module", "license": "MIT", "repository": { @@ -39,10 +39,10 @@ "@jstarpl/react-contextmenu": "^2.15.1", "@nrk/core-icons": "^9.6.0", "@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-0", + "@sofie-automation/corelib": "26.3.0-0", + "@sofie-automation/meteor-lib": "26.3.0-0", + "@sofie-automation/shared-lib": "26.3.0-0", "@sofie-automation/sorensen": "^1.5.11", "@testing-library/user-event": "^14.6.1", "@types/sinon": "^10.0.20", diff --git a/packages/yarn.lock b/packages/yarn.lock index 477f1ce8d35..813fdec8d1f 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -6862,11 +6862,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@npm:1.53.0-in-development, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@npm:26.3.0-0, @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-0" tslib: "npm:^2.8.1" type-fest: "npm:^4.33.0" languageName: unknown @@ -6898,12 +6898,12 @@ __metadata: 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-0, @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-0" + "@sofie-automation/shared-lib": "npm:26.3.0-0" fast-clone: "npm:^1.5.13" i18next: "npm:^21.10.0" influx: "npm:^5.9.7" @@ -6935,9 +6935,9 @@ __metadata: 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" + "@sofie-automation/blueprints-integration": "npm:26.3.0-0" + "@sofie-automation/corelib": "npm:26.3.0-0" + "@sofie-automation/shared-lib": "npm:26.3.0-0" amqplib: "npm:^0.10.5" deepmerge: "npm:^4.3.1" elastic-apm-node: "npm:^4.11.0" @@ -6955,7 +6955,7 @@ __metadata: 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-0, @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: @@ -6970,14 +6970,14 @@ __metadata: 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-0, @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-0" + "@sofie-automation/corelib": "npm:26.3.0-0" + "@sofie-automation/shared-lib": "npm:26.3.0-0" "@types/deep-extend": "npm:^0.6.2" "@types/semver": "npm:^7.5.8" "@types/underscore": "npm:^1.13.0" @@ -7004,12 +7004,12 @@ __metadata: 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-0, @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" + "@sofie-automation/shared-lib": "npm:26.3.0-0" "@types/koa": "npm:^3.0.0" "@types/koa__router": "npm:^12.0.4" ejson: "npm:^2.2.3" @@ -7021,7 +7021,7 @@ __metadata: 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-0, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -7053,10 +7053,10 @@ __metadata: "@jstarpl/react-contextmenu": "npm:^2.15.1" "@nrk/core-icons": "npm:^9.6.0" "@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-0" + "@sofie-automation/corelib": "npm:26.3.0-0" + "@sofie-automation/meteor-lib": "npm:26.3.0-0" + "@sofie-automation/shared-lib": "npm:26.3.0-0" "@sofie-automation/sorensen": "npm:^1.5.11" "@testing-library/dom": "npm:^10.4.0" "@testing-library/jest-dom": "npm:^6.6.3" @@ -19479,11 +19479,11 @@ 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" + "@sofie-automation/blueprints-integration": "npm:26.3.0-0" + "@sofie-automation/corelib": "npm:26.3.0-0" + "@sofie-automation/live-status-gateway-api": "npm:26.3.0-0" + "@sofie-automation/server-core-integration": "npm:26.3.0-0" + "@sofie-automation/shared-lib": "npm:26.3.0-0" debug: "npm:^4.4.0" fast-clone: "npm:^1.5.13" influx: "npm:^5.9.7" @@ -21552,8 +21552,8 @@ 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-0" + "@sofie-automation/shared-lib": "npm:26.3.0-0" tslib: "npm:^2.8.1" type-fest: "npm:^4.33.0" underscore: "npm:^1.13.7" @@ -23919,8 +23919,8 @@ 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" + "@sofie-automation/server-core-integration": "npm:26.3.0-0" + "@sofie-automation/shared-lib": "npm:26.3.0-0" debug: "npm:^4.4.0" influx: "npm:^5.9.7" timeline-state-resolver: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" From 75a0e14c77e49110f2db13873cc65371226b6728 Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:10:13 +0000 Subject: [PATCH 058/291] ci: Prevent OpenAPI client build from breaking tests (#1631) --- .github/workflows/publish-libs.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index 865d33d7b7f..5f927c5b5c3 100644 --- a/.github/workflows/publish-libs.yml +++ b/.github/workflows/publish-libs.yml @@ -119,7 +119,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 @@ -270,4 +275,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 From 3c2c1075b06cd1585744da15fef3185b80beea70 Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:37:09 +0000 Subject: [PATCH 059/291] ci: Fix publish workflow (#1633) --- .github/workflows/publish-libs.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index 5f927c5b5c3..2652a0b9aa1 100644 --- a/.github/workflows/publish-libs.yml +++ b/.github/workflows/publish-libs.yml @@ -151,12 +151,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 From e88d13ceec5c9db7341c571c75660950df9e8dfd Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 12:59:27 +0000 Subject: [PATCH 060/291] chore: update some vulnerable dependencies --- .node-version | 2 +- meteor/package.json | 3 +- meteor/yarn.lock | 937 +++++++++++++----------------------------- package.json | 2 +- packages/package.json | 2 +- packages/yarn.lock | 16 +- yarn.lock | 769 +++++++++------------------------- 7 files changed, 471 insertions(+), 1260 deletions(-) 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/package.json b/meteor/package.json index 405e1c3b028..ed4557165c9 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -46,7 +46,7 @@ "@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", + "bcrypt": "^6.0.0", "body-parser": "^1.20.3", "deep-extend": "0.6.0", "deepmerge": "^4.3.1", @@ -60,7 +60,6 @@ "meteor-node-stubs": "^1.2.12", "moment": "^2.30.1", "nanoid": "^3.3.8", - "node-gyp": "^9.4.1", "ntp-client": "^0.5.3", "object-path": "^0.11.8", "p-lazy": "^3.1.0", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index cfca919193f..50158287ad3 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -510,13 +510,6 @@ __metadata: languageName: node linkType: hard -"@gar/promisify@npm:^1.1.3": - version: 1.1.3 - resolution: "@gar/promisify@npm:1.1.3" - checksum: 10/052dd232140fa60e81588000cbe729a40146579b361f1070bce63e2a761388a22a16d00beeffc504bd3601cb8e055c57b21a185448b3ed550cf50716f4fd442e - languageName: node - linkType: hard - "@gulpjs/to-absolute-glob@npm:^4.0.0": version: 4.0.0 resolution: "@gulpjs/to-absolute-glob@npm:4.0.0" @@ -578,12 +571,12 @@ __metadata: languageName: node linkType: hard -"@isaacs/brace-expansion@npm:^5.0.0": - version: 5.0.0 - resolution: "@isaacs/brace-expansion@npm:5.0.0" +"@isaacs/brace-expansion@npm:^5.0.1": + 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 @@ -601,6 +594,15 @@ __metadata: languageName: node linkType: hard +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: "npm:^7.0.4" + checksum: 10/4412e9e6713c89c1e66d80bb0bb5a2a93192f10477623a27d08f228ba0316bb880affabc5bfe7f838f58a34d26c2c190da726e576cdfc18c49a72e89adabdcf5 + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -903,32 +905,14 @@ __metadata: linkType: hard "@koa/router@npm:^13.1.0": - version: 13.1.0 - resolution: "@koa/router@npm:13.1.0" + version: 13.1.1 + resolution: "@koa/router@npm:13.1.1" dependencies: + debug: "npm:^4.4.1" http-errors: "npm:^2.0.0" koa-compose: "npm:^4.1.0" path-to-regexp: "npm:^6.3.0" - checksum: 10/b5de0745b266dd8cda5873ace9594ca4b020ef6fe0e94e1de81f11a7498d0fcc106c8d972513ad52493b196b5e111ed1d07b318cf4ca4156b85a99ea5d3a3f98 - languageName: node - linkType: hard - -"@mapbox/node-pre-gyp@npm:^1.0.11": - version: 1.0.11 - resolution: "@mapbox/node-pre-gyp@npm:1.0.11" - dependencies: - detect-libc: "npm:^2.0.0" - https-proxy-agent: "npm:^5.0.0" - make-dir: "npm:^3.1.0" - node-fetch: "npm:^2.6.7" - nopt: "npm:^5.0.0" - npmlog: "npm:^5.0.1" - rimraf: "npm:^3.0.2" - semver: "npm:^7.3.5" - tar: "npm:^6.1.11" - bin: - node-pre-gyp: bin/node-pre-gyp - checksum: 10/59529a2444e44fddb63057152452b00705aa58059079191126c79ac1388ae4565625afa84ed4dd1bf017d1111ab6e47907f7c5192e06d83c9496f2f3e708680a + checksum: 10/960a573524d2c315994cdd59e94bca178ccd75931ac804b82c0657830911f8e1c923d17962cc2aac252ce842000c7a3204d629146852a30c8af02e70d5ade8e5 languageName: node linkType: hard @@ -1008,32 +992,25 @@ __metadata: languageName: node linkType: hard -"@npmcli/fs@npm:^2.1.0": - version: 2.1.2 - resolution: "@npmcli/fs@npm:2.1.2" +"@npmcli/agent@npm:^4.0.0": + version: 4.0.0 + resolution: "@npmcli/agent@npm:4.0.0" dependencies: - "@gar/promisify": "npm:^1.1.3" - semver: "npm:^7.3.5" - checksum: 10/c5d4dfee80de2236e1e4ed595d17e217aada72ebd8215183fc46096fa010f583dd2aaaa486758de7cc0b89440dbc31cfe8b276269d75d47af35c716e896f78ec + agent-base: "npm:^7.1.0" + http-proxy-agent: "npm:^7.0.0" + https-proxy-agent: "npm:^7.0.1" + lru-cache: "npm:^11.2.1" + socks-proxy-agent: "npm:^8.0.3" + checksum: 10/1a81573becc60515031accc696e6405e9b894e65c12b98ef4aeee03b5617c41948633159dbf6caf5dde5b47367eeb749bdc7b7dfb21960930a9060a935c6f636 languageName: node linkType: hard -"@npmcli/fs@npm:^3.1.0": - version: 3.1.0 - resolution: "@npmcli/fs@npm:3.1.0" +"@npmcli/fs@npm:^5.0.0": + version: 5.0.0 + resolution: "@npmcli/fs@npm:5.0.0" dependencies: semver: "npm:^7.3.5" - checksum: 10/f3a7ab3a31de65e42aeb6ed03ed035ef123d2de7af4deb9d4a003d27acc8618b57d9fb9d259fe6c28ca538032a028f37337264388ba27d26d37fff7dde22476e - languageName: node - linkType: hard - -"@npmcli/move-file@npm:^2.0.0": - version: 2.0.1 - resolution: "@npmcli/move-file@npm:2.0.1" - dependencies: - mkdirp: "npm:^1.0.4" - rimraf: "npm:^3.0.2" - checksum: 10/52dc02259d98da517fae4cb3a0a3850227bdae4939dda1980b788a7670636ca2b4a01b58df03dd5f65c1e3cb70c50fa8ce5762b582b3f499ec30ee5ce1fd9380 + checksum: 10/4935c7719d17830d0f9fa46c50be17b2a3c945cec61760f6d0909bce47677c42e1810ca673305890f9e84f008ec4d8e841182f371e42100a8159d15f22249208 languageName: node linkType: hard @@ -1087,13 +1064,6 @@ __metadata: languageName: node linkType: hard -"@pkgjs/parseargs@npm:^0.11.0": - version: 0.11.0 - resolution: "@pkgjs/parseargs@npm:0.11.0" - checksum: 10/115e8ceeec6bc69dff2048b35c0ab4f8bbee12d8bb6c1f4af758604586d802b6e669dcb02dda61d078de42c2b4ddce41b3d9e726d7daa6b4b850f4adbf7333ff - languageName: node - linkType: hard - "@pkgr/core@npm:^0.1.0": version: 0.1.1 resolution: "@pkgr/core@npm:0.1.1" @@ -1281,13 +1251,6 @@ __metadata: languageName: node linkType: hard -"@tootallnate/once@npm:2": - version: 2.0.0 - resolution: "@tootallnate/once@npm:2.0.0" - checksum: 10/ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 - languageName: node - linkType: hard - "@types/accepts@npm:*": version: 1.3.5 resolution: "@types/accepts@npm:1.3.5" @@ -1855,13 +1818,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" @@ -1984,12 +1954,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,16 +1970,6 @@ __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" @@ -2107,33 +2065,6 @@ __metadata: 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" @@ -2319,7 +2250,7 @@ __metadata: "@types/underscore": "npm:^1.13.0" app-root-path: "npm:^3.1.0" babel-jest: "npm:^29.7.0" - bcrypt: "npm:^5.1.1" + bcrypt: "npm:^6.0.0" body-parser: "npm:^1.20.3" deep-extend: "npm:0.6.0" deepmerge: "npm:^4.3.1" @@ -2341,7 +2272,6 @@ __metadata: meteor-node-stubs: "npm:^1.2.12" moment: "npm:^2.30.1" nanoid: "npm:^3.3.8" - node-gyp: "npm:^9.4.1" ntp-client: "npm:^0.5.3" object-path: "npm:^0.11.8" open-cli: "npm:^8.0.0" @@ -2480,13 +2410,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 @@ -2708,9 +2639,9 @@ __metadata: linkType: hard "bson@npm:^6.10.1": - version: 6.10.2 - resolution: "bson@npm:6.10.2" - checksum: 10/c729cf609bf96ee3ab8edbd1c5117bfc2f7ea33eb45a49aeeda8144a9d5616bfee6ad78d4b591757151acddaedcf11dc82c0ad6c0712270221cf340da4006962 + version: 6.10.4 + resolution: "bson@npm:6.10.4" + checksum: 10/8a79a452219a13898358a5abc93e32bc3805236334f962661da121ce15bd5cade27718ba3310ee2a143ff508489b08467eed172ecb2a658cb8d2e94fdb76b215 languageName: node linkType: hard @@ -2778,49 +2709,22 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^16.1.0": - version: 16.1.3 - resolution: "cacache@npm:16.1.3" +"cacache@npm:^20.0.1": + version: 20.0.3 + resolution: "cacache@npm:20.0.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" - 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 @@ -2933,10 +2837,10 @@ __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 @@ -2965,13 +2869,6 @@ __metadata: languageName: node linkType: hard -"clean-stack@npm:^2.0.0": - version: 2.2.0 - resolution: "clean-stack@npm:2.2.0" - checksum: 10/2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 - languageName: node - linkType: hard - "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -3087,15 +2984,6 @@ __metadata: 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 - languageName: node - linkType: hard - "color@npm:^3.1.3": version: 3.2.1 resolution: "color@npm:3.2.1" @@ -3175,13 +3063,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" @@ -3564,15 +3445,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.4.1": + 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 @@ -3747,13 +3628,6 @@ __metadata: 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": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" @@ -4568,6 +4442,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" @@ -4710,7 +4596,7 @@ __metadata: languageName: node linkType: hard -"foreground-child@npm:^3.1.0, foreground-child@npm:^3.3.1": +"foreground-child@npm:^3.3.1": version: 3.3.1 resolution: "foreground-child@npm:3.3.1" dependencies: @@ -4761,15 +4647,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" @@ -4841,39 +4718,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" @@ -5072,22 +4916,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2": - version: 10.5.0 - resolution: "glob@npm:10.5.0" - dependencies: - foreground-child: "npm:^3.1.0" - jackspeak: "npm:^3.1.2" - minimatch: "npm:^9.0.4" - minipass: "npm:^7.1.2" - package-json-from-dist: "npm:^1.0.0" - path-scurry: "npm:^1.11.1" - bin: - glob: dist/esm/bin.mjs - checksum: 10/ab3bccfefcc0afaedbd1f480cd0c4a2c0e322eb3f0aa7ceaa31b3f00b825069f17cf0f1fc8b6f256795074b903f37c0ade37ddda6a176aa57f1c2bbfe7240653 - languageName: node - linkType: hard - "glob@npm:^11.0.1": version: 11.1.0 resolution: "glob@npm:11.1.0" @@ -5104,6 +4932,17 @@ __metadata: languageName: node linkType: hard +"glob@npm:^13.0.0": + version: 13.0.1 + resolution: "glob@npm:13.0.1" + dependencies: + minimatch: "npm:^10.1.2" + minipass: "npm:^7.1.2" + path-scurry: "npm:^2.0.0" + checksum: 10/465e8cc269ab88d7415a3906cdc0f4543a2ae54df99207204af5bc28a944396d8d893822f546a8056a78ec714e608ab4f3502532c4d6b9cc5e113adf0fe5109e + languageName: node + linkType: hard + "glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -5118,19 +4957,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" @@ -5269,13 +5095,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" @@ -5379,7 +5198,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 @@ -5433,14 +5252,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 +5269,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 @@ -5643,13 +5461,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" @@ -5706,10 +5517,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 @@ -5867,13 +5678,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" @@ -6059,6 +5863,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" @@ -6131,19 +5942,6 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^3.1.2": - version: 3.4.3 - resolution: "jackspeak@npm:3.4.3" - dependencies: - "@isaacs/cliui": "npm:^8.0.2" - "@pkgjs/parseargs": "npm:^0.11.0" - dependenciesMeta: - "@pkgjs/parseargs": - optional: true - checksum: 10/96f8786eaab98e4bf5b2a5d6d9588ea46c4d06bbc4f2eb861fdd7b6b182b16f71d8a70e79820f335d52653b16d4843b29dd9cdcf38ae80406756db9199497cf3 - languageName: node - linkType: hard - "jackspeak@npm:^4.1.1": version: 4.1.1 resolution: "jackspeak@npm:4.1.1" @@ -7023,17 +6821,17 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": +"lru-cache@npm:^10.0.1": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" checksum: 10/e6e90267360476720fa8e83cc168aa2bf0311f3f2eea20a6ba78b90a885ae72071d9db132f40fda4129c803e7dcec3a6b6a6fbb44ca90b081630b810b5d6a41a 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 +6853,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 +6869,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 @@ -7386,12 +7140,12 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.1.1": - version: 10.1.1 - resolution: "minimatch@npm:10.1.1" +"minimatch@npm:^10.1.1, minimatch@npm:^10.1.2": + version: 10.1.2 + resolution: "minimatch@npm:10.1.2" dependencies: - "@isaacs/brace-expansion": "npm:^5.0.0" - checksum: 10/110f38921ea527022e90f7a5f43721838ac740d0a0c26881c03b57c261354fb9a0430e40b2c56dfcea2ef3c773768f27210d1106f1f2be19cde3eea93f26f45e + "@isaacs/brace-expansion": "npm:^5.0.1" + checksum: 10/6f0ef975463739207144e411bdd54f7205ce38770b162fa3bc4c9be4987a16cb20d0962a82f26c2372598cfba90faa97b327239d303b529b774f17681c163b46 languageName: node linkType: hard @@ -7440,42 +7194,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 +7236,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 +7254,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:^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 +7281,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: @@ -7661,13 +7392,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 +7420,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 +7438,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 +7513,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 @@ -7889,30 +7591,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 +7600,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" @@ -8207,12 +7878,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 @@ -8363,16 +8032,6 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.11.1": - version: 1.11.1 - resolution: "path-scurry@npm:1.11.1" - dependencies: - lru-cache: "npm:^10.2.0" - minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - checksum: 10/5e8845c159261adda6f09814d7725683257fcc85a18f329880ab4d7cc1d12830967eae5d5894e453f341710d5484b8fdbbd4d75181b4d6e1eb2f4dc7aeadc434 - languageName: node - linkType: hard - "path-scurry@npm:^2.0.0": version: 2.0.0 resolution: "path-scurry@npm:2.0.0" @@ -8434,6 +8093,13 @@ __metadata: languageName: node linkType: hard +"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" @@ -8545,6 +8211,13 @@ __metadata: 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 + "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -8576,13 +8249,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" @@ -9076,17 +8742,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" @@ -9198,13 +8853,6 @@ __metadata: languageName: node linkType: hard -"set-blocking@npm:^2.0.0": - version: 2.0.0 - resolution: "set-blocking@npm:2.0.0" - checksum: 10/8980ebf7ae9eb945bb036b6e283c547ee783a1ad557a82babf758a065e2fb6ea337fd82cac30dd565c1e606e423f30024a19fff7afbf4977d784720c4026a8ef - languageName: node - linkType: hard - "set-function-length@npm:^1.2.2": version: 1.2.2 resolution: "set-function-length@npm:1.2.2" @@ -9344,7 +8992,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, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: 10/a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -9395,24 +9043,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 @@ -9569,21 +9217,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 @@ -9708,7 +9347,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: @@ -9913,17 +9552,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.7 + resolution: "tar@npm:7.5.7" 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/0d6938dd32fe5c0f17c8098d92bd9889ee0ed9d11f12381b8146b6e8c87bb5aa49feec7abc42463f0597503d8e89e4c4c0b42bff1a5a38444e918b4878b7fd21 languageName: node linkType: hard @@ -10061,6 +9699,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 +9780,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" @@ -10456,39 +10097,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 @@ -10688,13 +10311,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 +10335,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" @@ -10768,7 +10374,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 +10385,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 @@ -10910,6 +10518,13 @@ __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 + "yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.3": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" diff --git a/package.json b/package.json index 4e521a0f7d7..9c1fed97bae 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "lint-staged": "^15.5.2", "rimraf": "^6.1.2", "semver": "^7.7.3", - "snyk-nodejs-lockfile-parser": "^1.60.1" + "snyk-nodejs-lockfile-parser": "^2.5.0" }, "packageManager": "yarn@4.12.0" } diff --git a/packages/package.json b/packages/package.json index d0646cdcfe1..88d4c4f7112 100644 --- a/packages/package.json +++ b/packages/package.json @@ -60,7 +60,7 @@ "jest-environment-jsdom": "^29.7.0", "jest-mock-extended": "^3.0.7", "json-schema-to-typescript": "^10.1.5", - "lerna": "^9.0.0", + "lerna": "^9.0.3", "nodemon": "^2.0.22", "open-cli": "^8.0.0", "pinst": "^3.0.0", diff --git a/packages/yarn.lock b/packages/yarn.lock index 813fdec8d1f..9c68c75ee04 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -4239,11 +4239,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 @@ -19268,7 +19268,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lerna@npm:^9.0.0": +"lerna@npm:^9.0.3": version: 9.0.3 resolution: "lerna@npm:9.0.3" dependencies: @@ -23218,7 +23218,7 @@ asn1@evs-broadcast/node-asn1: 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" + lerna: "npm:^9.0.3" nodemon: "npm:^2.0.22" open-cli: "npm:^8.0.0" pinst: "npm:^3.0.0" @@ -28421,15 +28421,15 @@ asn1@evs-broadcast/node-asn1: linkType: hard "tar@npm:^7.4.3": - version: 7.5.6 - resolution: "tar@npm:7.5.6" + version: 7.5.7 + resolution: "tar@npm:7.5.7" 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/0d6938dd32fe5c0f17c8098d92bd9889ee0ed9d11f12381b8146b6e8c87bb5aa49feec7abc42463f0597503d8e89e4c4c0b42bff1a5a38444e918b4878b7fd21 languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index f131946d836..461548b0140 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: @@ -22,11 +22,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 @@ -80,9 +80,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 +100,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 +193,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 +216,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 + tslib: "npm:^2.4.0" + checksum: 10/438e0d0e73f22454696575436ee11f27e765df7fa6b6f9bf63e3d5f79f2b7ceb382dd7df1efbb27ca674a974a2c444a5bf346aac3875ffa6a555b26517309b71 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 - 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 +279,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" - 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" +"@yarnpkg/shell@npm:^4.1.3": + version: 4.1.3 + resolution: "@yarnpkg/shell@npm:4.1.3" 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 +356,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 +372,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" @@ -440,35 +388,10 @@ __metadata: lint-staged: "npm:^15.5.2" rimraf: "npm:^6.1.2" semver: "npm:^7.7.3" - snyk-nodejs-lockfile-parser: "npm:^1.60.1" + snyk-nodejs-lockfile-parser: "npm:^2.5.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 - languageName: node - linkType: hard - -"bl@npm:^4.0.3": - version: 4.1.0 - resolution: "bl@npm:4.1.0" - dependencies: - buffer: "npm:^5.5.0" - inherits: "npm:^2.0.4" - readable-stream: "npm:^3.4.0" - checksum: 10/b7904e66ed0bdfc813c06ea6c3e35eafecb104369dbf5356d0f416af90c1546de3b74e5b63506f0629acf5e16a6f87c3798f16233dcff086e9129383aa02ab55 - languageName: node - linkType: hard - "braces@npm:^3.0.3": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -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,7 +565,7 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:7.0.3, cross-spawn@npm:^7.0.3": +"cross-spawn@npm:^7.0.3": version: 7.0.3 resolution: "cross-spawn@npm:7.0.3" dependencies: @@ -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" @@ -975,29 +821,6 @@ __metadata: 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 - languageName: node - linkType: hard - "got@npm:^11.7.0": version: 11.8.6 resolution: "got@npm:11.8.6" @@ -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 @@ -1559,6 +1302,22 @@ __metadata: languageName: node linkType: hard +"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 + "minipass@npm:^7.1.2": version: 7.1.2 resolution: "minipass@npm:7.1.2" @@ -1566,14 +1325,22 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:^0.5.1": - version: 0.5.6 - resolution: "mkdirp@npm:0.5.6" +"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 @@ -1749,13 +1478,6 @@ __metadata: languageName: node linkType: hard -"path-type@npm:^4.0.0": - version: 4.0.0 - resolution: "path-type@npm:4.0.0" - checksum: 10/5b1e2daa247062061325b8fdbfd1fb56dde0a448fb1455453276ea18c60685bdad23a445dc148cf87bc216be1573357509b7d4060494a6fd768c7efad833ee45 - languageName: node - linkType: hard - "picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" @@ -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" @@ -1933,30 +1602,7 @@ __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" - bin: - semver: bin/semver.js - checksum: 10/36b1fbe1a2b6f873559cd57b238f1094a053dbfd997ceeb8757d79d1d2089c56d1321b9f1069ce263dc64cfa922fa1d2ad566b39426fe1ac6c723c1487589e10 - languageName: node - linkType: hard - -"semver@npm:^7.7.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.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -1965,20 +1611,6 @@ __metadata: 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 - languageName: node - linkType: hard - "shebang-command@npm:^2.0.0": version: 2.0.0 resolution: "shebang-command@npm:2.0.0" @@ -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.5.0": + version: 2.5.0 + resolution: "snyk-nodejs-lockfile-parser@npm:2.5.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/5307e29abe0a74809317859648f63f418e93c8eaa8d98aa3ee02de42118a5e3f7606ba0a340352522962108cd118d77c3273129db1fb2c755c45f5dec7486d52 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" From 144623137462aa4a91e35d9becc0eea6e66b07a8 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 13:09:15 +0000 Subject: [PATCH 061/291] chore: safe dependency updates --- packages/blueprints-integration/package.json | 2 +- packages/corelib/package.json | 6 +- packages/job-worker/package.json | 10 +- packages/live-status-gateway-api/package.json | 14 +- packages/live-status-gateway/package.json | 10 +- packages/meteor-lib/package.json | 6 +- packages/mos-gateway/package.json | 4 +- packages/openapi/package.json | 4 +- packages/package.json | 20 +- packages/playout-gateway/package.json | 6 +- packages/server-core-integration/package.json | 6 +- packages/shared-lib/package.json | 2 +- packages/webui/package.json | 50 +- packages/yarn.lock | 3700 ++++++++--------- 14 files changed, 1899 insertions(+), 1941 deletions(-) diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 587f64b14a9..155ad2b047d 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -38,7 +38,7 @@ "dependencies": { "@sofie-automation/shared-lib": "26.3.0-0", "tslib": "^2.8.1", - "type-fest": "^4.33.0" + "type-fest": "^4.41.0" }, "lint-staged": { "*.{css,json,md,scss}": [ diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 52609eee059..d9c61dd8b90 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -41,13 +41,13 @@ "@sofie-automation/shared-lib": "26.3.0-0", "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": { diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 0dfcf8e422b..06be3626c7c 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -36,20 +36,20 @@ "/LICENSE" ], "dependencies": { - "@slack/webhook": "^7.0.4", + "@slack/webhook": "^7.0.6", "@sofie-automation/blueprints-integration": "26.3.0-0", "@sofie-automation/corelib": "26.3.0-0", "@sofie-automation/shared-lib": "26.3.0-0", - "amqplib": "^0.10.5", + "amqplib": "0.10.5", "deepmerge": "^4.3.1", - "elastic-apm-node": "^4.11.0", + "elastic-apm-node": "^4.15.0", "mongodb": "^6.12.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", diff --git a/packages/live-status-gateway-api/package.json b/packages/live-status-gateway-api/package.json index e59a8a2e2f3..5b03968dc76 100644 --- a/packages/live-status-gateway-api/package.json +++ b/packages/live-status-gateway-api/package.json @@ -42,16 +42,16 @@ "/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": { diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index c94be7ebab0..d0a0e0eb14a 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -52,16 +52,16 @@ "@sofie-automation/live-status-gateway-api": "26.3.0-0", "@sofie-automation/server-core-integration": "26.3.0-0", "@sofie-automation/shared-lib": "26.3.0-0", - "debug": "^4.4.0", + "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" + "type-fest": "^4.41.0" }, "lint-staged": { "*.{css,json,md,scss}": [ diff --git a/packages/meteor-lib/package.json b/packages/meteor-lib/package.json index 7909b59647b..f95cf79ddcc 100644 --- a/packages/meteor-lib/package.json +++ b/packages/meteor-lib/package.json @@ -42,13 +42,13 @@ "@sofie-automation/corelib": "26.3.0-0", "@sofie-automation/shared-lib": "26.3.0-0", "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": { diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 8d155f417a4..09980fbb7ec 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -65,9 +65,9 @@ "@sofie-automation/server-core-integration": "26.3.0-0", "@sofie-automation/shared-lib": "26.3.0-0", "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": { diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 317dbe267e8..2a001602ea0 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -39,10 +39,10 @@ "tslib": "^2.8.1" }, "devDependencies": { - "@openapitools/openapi-generator-cli": "^2.20.2", + "@openapitools/openapi-generator-cli": "^2.28.0", "eslint": "^9.18.0", "eslint-plugin-yml": "^1.16.0", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "wget-improved": "^3.4.0" }, "lint-staged": { diff --git a/packages/package.json b/packages/package.json index 88d4c4f7112..1ef488e2538 100644 --- a/packages/package.json +++ b/packages/package.json @@ -41,15 +41,15 @@ "eslint": "cd $INIT_CWD && \"$PROJECT_CWD/node_modules/.bin/eslint\"" }, "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", "@sofie-automation/code-standard-preset": "^3.0.0", - "@types/amqplib": "^0.10.6", + "@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/node": "^22.19.8", "@types/object-path": "^0.11.4", "@types/underscore": "^1.13.0", "babel-jest": "^29.7.0", @@ -59,17 +59,17 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-mock-extended": "^3.0.7", - "json-schema-to-typescript": "^10.1.5", + "json-schema-to-typescript": "^15.0.4", "lerna": "^9.0.3", "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", diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index f464b15a4f3..d0935681385 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -54,12 +54,12 @@ "dependencies": { "@sofie-automation/server-core-integration": "26.3.0-0", "@sofie-automation/shared-lib": "26.3.0-0", - "debug": "^4.4.0", - "influx": "^5.9.7", + "debug": "^4.4.3", + "influx": "^5.12.0", "timeline-state-resolver": "10.0.0-nightly-release53-20251217-143607-df590aa96.0", "tslib": "^2.8.1", "underscore": "^1.13.7", - "winston": "^3.17.0" + "winston": "^3.19.0" }, "lint-staged": { "*.{css,json,md,scss}": [ diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 442cd123c04..3c08e6e14df 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -68,8 +68,8 @@ "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", @@ -77,7 +77,7 @@ "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" }, diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 2eb6976979b..87f8534396e 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -40,7 +40,7 @@ "kairos-lib": "^0.2.3", "timeline-state-resolver-types": "10.0.0-nightly-release53-20251217-143607-df590aa96.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": { diff --git a/packages/webui/package.json b/packages/webui/package.json index 7587a91a84e..8463dfd0266 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -32,11 +32,11 @@ }, "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", + "@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", "@nrk/core-icons": "^9.6.0", "@popperjs/core": "^2.11.8", "@sofie-automation/blueprints-integration": "26.3.0-0", @@ -46,7 +46,7 @@ "@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", @@ -62,8 +62,8 @@ "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-bootstrap": "^2.10.10", + "react-circular-progressbar": "^2.2.0", "react-datepicker": "^3.8.0", "react-dnd": "^14.0.5", "react-dnd-html5-backend": "^14.1.0", @@ -71,45 +71,45 @@ "react-focus-bounder": "^1.1.6", "react-hotkeys": "^2.0.0", "react-i18next": "^11.18.6", - "react-intersection-observer": "^9.15.1", + "react-intersection-observer": "^9.16.0", "react-moment": "^0.9.7", "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", + "semver": "^7.7.3", + "sha.js": "^2.4.12", "shuttle-webhid": "^0.0.2", - "type-fest": "^4.33.0", + "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": "^18.3.27", "@types/react-circular-progressbar": "^1.1.0", "@types/react-datepicker": "^3.1.8", - "@types/react-dom": "^18.3.5", + "@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": "^4.7.0", "@welldone-software/why-did-you-render": "^4.3.2", - "@xmldom/xmldom": "^0.8.10", + "@xmldom/xmldom": "^0.8.11", "babel-jest": "^29.7.0", - "globals": "^15.14.0", - "sass": "^1.83.4", + "globals": "^15.15.0", + "sass": "^1.97.3", "sinon": "^14.0.2", "typescript": "~5.7.3", - "vite": "^6.0.11", + "vite": "^6.4.1", "vite-plugin-node-polyfills": "^0.23.0", "vite-tsconfig-paths": "^5.1.4", "xml2js": "^0.6.2" diff --git a/packages/yarn.lock b/packages/yarn.lock index 9c68c75ee04..dc2fc072f2c 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,9 +417,9 @@ __metadata: 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" +"@asyncapi/generator-react-sdk@npm:*, @asyncapi/generator-react-sdk@npm:^1.1.2": + 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" @@ -436,16 +431,18 @@ __metadata: react: "npm:^17.0.1" rollup: "npm:^2.60.1" source-map-support: "npm:^0.5.19" - checksum: 10/2bdc65653def9e551373c8955d7ea7d2f80ecc5a449b72af52bd10ab0c69aa498dc94bfcfc8d58148d80b7b7b4b16966a7d2be65499cffcda6edfb671c651d98 + checksum: 10/98d31b8083f4740f86b1301fcf551fb7865c840552052cf9d5c441a03ccdf63f3d0179cbaac3e954bbf354672b9807f8e6658d63eee624dfdd13aed93c4a6957 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 +455,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 +471,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 +503,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 +576,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 +596,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 +626,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 +643,7 @@ __metadata: peerDependencies: react: ">=18.0.0" react-dom: ">=18.0.0" - checksum: 10/7105385f8f806200638f10b799ff1a5d1838041d20d14c6e64b1e1a411933727b91cd3957c2057bc7946524be01c8ab7bb03946886534b15f4767c277da38445 + checksum: 10/d74c2264134801be020dadd2e226e0d9ce7531848551c532129bae9a73ebefef8da7045cc2130bf57e141e5966353bd23df666b7271f5b44c30af5bcf1e78d6f languageName: node linkType: hard @@ -641,30 +656,30 @@ __metadata: languageName: node linkType: hard -"@asyncapi/specs@npm:^6.0.0-next-major-spec.9, @asyncapi/specs@npm:^6.8.0": - version: 6.8.1 - resolution: "@asyncapi/specs@npm:6.8.1" +"@asyncapi/specs@npm:^6.0.0-next-major-spec.9, @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/27f945d43157c14d74b36f65571eb9b16043be768d06fd48ce1b9749b11ecdfd36cd2b0f294c50d66f61df19703c8caf62569a406220b492d4fb9cce0b84c0ce + 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.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" +"@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.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 +707,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.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.21.3, @babel/core@npm:^7.25.9, @babel/core@npm:^7.28.0, @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.29.0, @babel/generator@npm:^7.7.2": + 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 +909,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 +944,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.14.7, @babel/parser@npm:^7.20.7, @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 @@ -1045,25 +1067,25 @@ __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.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 @@ -1200,452 +1222,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-module-transforms": "npm:^7.25.9" - "@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/75d34c6e709a23bcfa0e06f722c9a72b1d9ac3e7d72a07ef54a943d32f65f97cbbf0e387d874eb9d9b4c8d33045edfa8e8441d0f8794f3c2b9f1d71b928acf2c + checksum: 10/804121430a6dcd431e6ffe99c6d1fbbc44b43478113b79c677629e7f877b4f78a06b69c6bfb2747fd84ee91879fe2eb32e4620b53124603086cf5b727593ebe8 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-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.26.0" - "@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/f817f02fa04d13f1578f3026239b57f1003bebcf9f9b8d854714bed76a0e4986c79bd6d2e0ac14282c5d309454a8dab683c179709ca753b0152a69c69f3a78e3 + checksum: 10/5ca9257981f2bbddd9dccf9126f1368de1cb335e7a5ff5cca9282266825af5b18b5f06c144320dcf5d2a200d2b53b6d22d9b801a55dc0509ab5a5838af7e61b7 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-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.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" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/03145aa89b7c867941a03755216cfb503df6d475a78df84849a157fa5f2fcc17ba114a968d0579ae34e7c61403f35d1ba5d188fdfb9ad05f19354eb7605792f9 + checksum: 10/ec6ea2958e778a7e0220f4a75cb5816cecddc6bd98efa10499fff7baabaa29a594d50d787a4ebf8a8ba66fefcf76ca2ded602be0b4554ae3317e53b3b3375b37 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-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-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/47d03485fedac828832d9fee33b3b982a6db8197e8651ceb5d001890e276150b5a7ee3e9780749e1ba76453c471af907a159108832c24f93453dd45221788e97 + checksum: 10/b3e64728eef02d829510778226da4c06be740fe52e0d45d4aa68b24083096d8ad7df67f2e9e67198b2e85f3237d42bd66f5771f85846f7a746105d05ca2e0cae 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-modules-umd@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-modules-umd@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-module-transforms": "npm:^7.27.1" + "@babel/helper-plugin-utils": "npm:^7.27.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/7388932863b4ee01f177eb6c2e2df9e2312005e43ada99897624d5565db4b9cef1e30aa7ad2c79bbe5373f284cfcddea98d8fe212714a24c6aba223272163058 + languageName: node + linkType: hard + +"@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.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 +1719,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 +1768,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 +1818,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 +1889,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 @@ -2033,7 +2070,7 @@ __metadata: 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": +"@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.7, @babel/runtime@npm:^7.9.2": version: 7.26.9 resolution: "@babel/runtime@npm:7.26.9" dependencies: @@ -2042,7 +2079,7 @@ __metadata: 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, @babel/template@npm:^7.3.3": version: 7.28.6 resolution: "@babel/template@npm:7.28.6" dependencies: @@ -2053,28 +2090,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.3.3, @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 +2122,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" @@ -2719,14 +2763,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 @@ -3441,177 +3485,184 @@ __metadata: 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.25.12": + version: 0.25.12 + resolution: "@esbuild/aix-ppc64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/android-arm64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/android-arm@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/android-x64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/darwin-arm64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/darwin-x64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/freebsd-arm64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/freebsd-x64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-arm64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-arm@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-ia32@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-loong64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-mips64el@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-ppc64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-riscv64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-s390x@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/linux-x64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/netbsd-arm64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/netbsd-x64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/openbsd-arm64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/openbsd-x64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/openharmony-arm64@npm:0.25.12" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.25.12": + version: 0.25.12 + resolution: "@esbuild/sunos-x64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-arm64@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-ia32@npm:0.25.12" 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.25.12": + version: 0.25.12 + resolution: "@esbuild/win32-x64@npm:0.25.12" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -3704,47 +3755,45 @@ __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 +"@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:^6.7.2": - version: 6.7.2 - resolution: "@fortawesome/fontawesome-free@npm:6.7.2" - checksum: 10/88101fee12470ede1e7f2588b86121924259d98889b950e2ccde71d934f4f344b592d1360700de4e92d81262014d3ae33fe995c7799f2be2abddcee3102413d6 +"@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:^6.7.2": - version: 6.7.2 - resolution: "@fortawesome/fontawesome-svg-core@npm:6.7.2" +"@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:6.7.2" - checksum: 10/a3767631329aaa8c1bfafc9470718628533ceb42365774cd0c121477e0f3125f3cce4c2447058deee2874829ce11aa7a3fe183b0000bad81cf5ed0449c8470ef + "@fortawesome/fontawesome-common-types": "npm:7.1.0" + checksum: 10/a6137c120d2009df445e72e07e1b9fba923847fc7a3d2429a216fb09ee444aaf7904d9e89c90a1aea47876381437041e42837bb2c4f5a1c758f1424218157bb8 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" +"@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:6.7.2" - checksum: 10/efcd90cd5d333995ff4012a9d77a8b23523e246fa418524edf08bb6af8b14db2ee0b08ee5f7460a86474d352af06e1a2581cc827ee3706e9c0e92e178b50e27f + "@fortawesome/fontawesome-common-types": "npm:7.1.0" + checksum: 10/302548ff3fd45272eb927c87c26d555cebfecdbd0744177f24a47b1743eb73f10c6e8017fc9e2eab560851e4a396a2a2be07b1e75e8c7fe7215c1d7629133f02 languageName: node linkType: hard -"@fortawesome/react-fontawesome@npm:^0.2.2": - version: 0.2.2 - resolution: "@fortawesome/react-fontawesome@npm:0.2.2" - dependencies: - prop-types: "npm:^15.8.1" +"@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 @@ -4077,18 +4126,18 @@ __metadata: languageName: node linkType: hard -"@inquirer/external-editor@npm:^1.0.2": - version: 1.0.2 - resolution: "@inquirer/external-editor@npm:1.0.2" +"@inquirer/external-editor@npm:^1.0.0, @inquirer/external-editor@npm:^1.0.2": + version: 1.0.3 + resolution: "@inquirer/external-editor@npm:1.0.3" dependencies: - chardet: "npm:^2.1.0" + chardet: "npm:^2.1.1" iconv-lite: "npm:^0.7.0" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/d0c5c73249b8153f4cf872c4fba01c57a7653142a4cad496f17ed03ef3769330a4b3c519b68d70af69d4bb33003d2599b66b2242be85411c0b027ff383619666 + checksum: 10/c95d7237a885b32031715089f92820525731d4d3c2bd7afdb826307dc296cc2b39e7a644b0bb265441963348cca42e7785feb29c3aaf18fd2b63131769bf6587 languageName: node linkType: hard @@ -4550,14 +4599,23 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.5": - version: 0.3.5 - resolution: "@jridgewell/gen-mapping@npm:0.3.5" +"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.13 + resolution: "@jridgewell/gen-mapping@npm:0.3.13" dependencies: - "@jridgewell/set-array": "npm:^1.2.1" - "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@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/81587b3c4dd8e6c60252122937cea0c637486311f4ed208b52b62aae2e7a87598f63ec330e6cd0984af494bfb16d3f0d60d3b21d7e5b4aedd2602ff3fe9d32e2 + checksum: 10/c2bb01856e65b506d439455f28aceacf130d6c023d1d4e3b48705e88def3571753e1a887daa04b078b562316c92d26ce36408a60534bceca3f830aec88a339ad languageName: node linkType: hard @@ -4568,13 +4626,6 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.2.1": - version: 1.2.1 - resolution: "@jridgewell/set-array@npm:1.2.1" - checksum: 10/832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 - languageName: node - linkType: hard - "@jridgewell/source-map@npm:^0.3.3": version: 0.3.5 resolution: "@jridgewell/source-map@npm:0.3.5" @@ -4585,10 +4636,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 +4653,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.20, @jridgewell/trace-mapping@npm:^0.3.24, @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 +4766,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 +4776,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 @@ -4978,24 +5029,24 @@ __metadata: languageName: node linkType: hard -"@nestjs/axios@npm:4.0.0": - version: 4.0.0 - resolution: "@nestjs/axios@npm:4.0.0" +"@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 +5059,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 +5087,7 @@ __metadata: optional: true "@nestjs/websockets": optional: true - checksum: 10/88f8a3c52a98c059e6d93c5faf9194daee4667b1a855f88233582acf51e3be9aedb3e3f7f992da46e0e45df31ad4e045e0296e6e35ea14e2245d4069176711c0 + checksum: 10/44f122101f411abb2b83cd6bab865006c4a74552d3903ca47a58b396608453b36b59cd6461f8689cd31c3d91b81960cea190d654448921ae0efedbd20ff52284 languageName: node linkType: hard @@ -5837,31 +5888,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 @@ -6187,6 +6237,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 +6359,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:1.0.0-beta.27": + version: 1.0.0-beta.27 + resolution: "@rolldown/pluginutils@npm:1.0.0-beta.27" + checksum: 10/4f7da788d88b33d029d5acf84c63be27c62d7c53017476f2e3026172cf94062cb399cd15194c89574578f192016bbcb1e040ce6811b3bb9ec4d4faa2ad386459 + 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 +6428,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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-android-arm64@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-freebsd-x64@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-loong64-gnu@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-loong64-musl@npm:4.57.1" + conditions: os=linux & cpu=loong64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-ppc64-gnu@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-ppc64-musl@npm:4.57.1" + conditions: os=linux & cpu=ppc64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.57.1" + conditions: os=linux & cpu=riscv64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-openbsd-x64@npm:4.57.1" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-openharmony-arm64@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-openharmony-arm64@npm:4.57.1" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.57.1" 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.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-win32-x64-gnu@npm:4.57.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.57.1": + version: 4.57.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.57.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -6840,14 +6956,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,13 +6978,23 @@ __metadata: 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 + "@sofie-automation/blueprints-integration@npm:26.3.0-0, @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:26.3.0-0" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" languageName: unknown linkType: soft @@ -6906,13 +7032,13 @@ __metadata: "@sofie-automation/shared-lib": "npm:26.3.0-0" 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 @@ -6934,22 +7060,22 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: - "@slack/webhook": "npm:^7.0.4" + "@slack/webhook": "npm:^7.0.6" "@sofie-automation/blueprints-integration": "npm:26.3.0-0" "@sofie-automation/corelib": "npm:26.3.0-0" "@sofie-automation/shared-lib": "npm:26.3.0-0" - amqplib: "npm:^0.10.5" + amqplib: "npm:0.10.5" deepmerge: "npm:^4.3.1" - elastic-apm-node: "npm:^4.11.0" + elastic-apm-node: "npm:^4.15.0" jest: "npm:^29.7.0" jest-mock-extended: "npm:^3.0.7" mongodb: "npm:^6.12.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 @@ -6959,14 +7085,14 @@ __metadata: 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 @@ -6979,11 +7105,11 @@ __metadata: "@sofie-automation/corelib": "npm:26.3.0-0" "@sofie-automation/shared-lib": "npm:26.3.0-0" "@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,10 +7121,10 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/openapi@workspace:openapi" dependencies: - "@openapitools/openapi-generator-cli": "npm:^2.20.2" + "@openapitools/openapi-generator-cli": "npm:^2.28.0" eslint: "npm:^9.18.0" eslint-plugin-yml: "npm:^1.16.0" - js-yaml: "npm:^4.1.0" + js-yaml: "npm:^4.1.1" tslib: "npm:^2.8.1" wget-improved: "npm:^3.4.0" languageName: unknown @@ -7010,12 +7136,12 @@ __metadata: dependencies: "@koa/router": "npm:^14.0.0" "@sofie-automation/shared-lib": "npm:26.3.0-0" - "@types/koa": "npm:^3.0.0" - "@types/koa__router": "npm:^12.0.4" + "@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 @@ -7029,7 +7155,7 @@ __metadata: kairos-lib: "npm:^0.2.3" timeline-state-resolver-types: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" languageName: unknown linkType: soft @@ -7044,13 +7170,13 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/webui@workspace:webui" dependencies: - "@babel/preset-env": "npm:^7.26.7" + "@babel/preset-env": "npm:^7.29.0" "@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" + "@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" "@nrk/core-icons": "npm:^9.6.0" "@popperjs/core": "npm:^2.11.8" "@sofie-automation/blueprints-integration": "npm:26.3.0-0" @@ -7058,33 +7184,33 @@ __metadata: "@sofie-automation/meteor-lib": "npm:26.3.0-0" "@sofie-automation/shared-lib": "npm:26.3.0-0" "@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": "npm:^18.3.27" "@types/react-circular-progressbar": "npm:^1.1.0" "@types/react-datepicker": "npm:^3.1.8" - "@types/react-dom": "npm:^18.3.5" + "@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:^4.7.0" "@welldone-software/why-did-you-render": "npm:^4.3.2" - "@xmldom/xmldom": "npm:^0.8.10" + "@xmldom/xmldom": "npm:^0.8.11" babel-jest: "npm:^29.7.0" - bootstrap: "npm:^5.3.3" + 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:^15.15.0" i18next: "npm:^21.10.0" i18next-browser-languagedetector: "npm:^6.1.8" i18next-http-backend: "npm:^1.4.5" @@ -7096,8 +7222,8 @@ __metadata: 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-bootstrap: "npm:^2.10.10" + react-circular-progressbar: "npm:^2.2.0" react-datepicker: "npm:^3.8.0" react-dnd: "npm:^14.0.5" react-dnd-html5-backend: "npm:^14.1.0" @@ -7105,20 +7231,20 @@ __metadata: 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-intersection-observer: "npm:^9.16.0" react-moment: "npm:^0.9.7" 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" + sass: "npm:^1.97.3" + semver: "npm:^7.7.3" + sha.js: "npm:^2.4.12" shuttle-webhid: "npm:^0.0.2" sinon: "npm:^14.0.2" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" typescript: "npm:~5.7.3" underscore: "npm:^1.13.7" - vite: "npm:^6.0.11" + vite: "npm:^6.4.1" vite-plugin-node-polyfills: "npm:^0.23.0" vite-tsconfig-paths: "npm:^5.1.4" webmidi: "npm:^2.5.3" @@ -7534,40 +7660,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 +7706,7 @@ __metadata: optional: true "@types/react-dom": optional: true - checksum: 10/cf10bfa9a363384e6861417696fff4a464a64f98ec6f0bb7f1fa7cbb550d075d23a2f6a943b7df85dded7bde3234f6ea6b6e36f95211f4544b846ea72c288289 + checksum: 10/0ca88c6f672d00c2afd1bdedeff9b5382dd8157038efeb9762dc016731030075624be7106b92d2b5e5c52812faea85263e69272c14b6f8700eb48a4a8af6feef languageName: node linkType: hard @@ -7594,14 +7719,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 @@ -7717,7 +7841,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: @@ -7793,7 +7917,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 +8318,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": + version: 1.0.8 + resolution: "@types/estree@npm:1.0.8" + checksum: 10/25a4c16a6752538ffde2826c2cc0c6491d90e69cd6187bef4a006dd2c3c45469f049e643d7e516c515f21484dc3d48fd5c870be158a5beb72f5baf3dc43e4099 languageName: node linkType: hard @@ -8239,16 +8363,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" @@ -8375,7 +8489,7 @@ __metadata: 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 +8521,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 +8533,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 +8583,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 +8606,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 +8650,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" @@ -8607,21 +8707,21 @@ __metadata: 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 +8766,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 +8810,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 @@ -9036,18 +9136,19 @@ __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:^4.7.0": + version: 4.7.0 + resolution: "@vitejs/plugin-react@npm:4.7.0" 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.28.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-beta.27" "@types/babel__core": "npm:^7.20.5" - react-refresh: "npm:^0.14.2" + react-refresh: "npm:^0.17.0" peerDependencies: - vite: ^4.2.0 || ^5.0.0 || ^6.0.0 - checksum: 10/3b220908ed9b7b96a380a9c53e82fb428ca1f76b798ab59d1c63765bdff24de61b4778dd3655952b7d3d922645aea2d97644503b879aba6e3fcf467605b9913d + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10/619a5d650ce0e8e2f37dae369b803990c6647e81ec983a00e44a734b3feeefd5a32c20fbee56d496fbc239cad6b949797dddf7c6d9f23c48100c5b2b5dc41e1f languageName: node linkType: hard @@ -9213,10 +9314,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 @@ -9601,7 +9702,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,13 +9793,6 @@ __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": version: 3.1.3 resolution: "anymatch@npm:3.1.3" @@ -10086,25 +10180,37 @@ 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/a2f90bba56820883879f32a237e2b9ff25c250365dcafd41cec41b3406a3df334a148f90010182dfdadb4b41dc59f6f0b3e8898ff41b666d1157b5f3f4523497 + checksum: 10/ae4e06dcd18289f2fd18179256d550d27f9a53ecb2f9c59f2ccc4efd1d7151839ba8c3e0fb533dac793e4a59a576ca8689a19244dce5c396680837674a47a867 languageName: node linkType: hard -"axios@npm:^1.12.0, axios@npm:^1.7.8": - version: 1.13.3 - resolution: "axios@npm:1.13.3" +"axios@npm:^1.11.0, axios@npm:^1.12.0": + version: 1.13.4 + resolution: "axios@npm:1.13.4" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.4" proxy-from-env: "npm:^1.1.0" - checksum: 10/2ceca9215671f9c2bcd5d8a0a1a667e9a35f9f7cfae88f25bba773ed9612de6cac50b2bf8be5e6918cbd2db601b4431ca87a00bffd9682939a8b85da9c89345a + checksum: 10/54b7ef71c64837f9d52475832337f520cf6fa85c94612e03a3a2aad7082804a2544741267122696662147e90e6d2746601346984cf531ae715ecdb56d586a04c + languageName: node + linkType: hard + +"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 @@ -10172,16 +10278,16 @@ asn1@evs-broadcast/node-asn1: 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 +10303,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.6" + core-js-compat: "npm:^3.48.0" + peerDependencies: + "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 + 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.1" + "@babel/helper-define-polyfill-provider": "npm:^0.6.6" peerDependencies: "@babel/core": ^7.4.0 || ^8.0.0-0 <8.0.0 - checksum: 10/9df4a8e9939dd419fed3d9ea26594b4479f2968f37c225e1b2aa463001d7721f5537740e6622909d2a570b61cec23256924a1701404fc9d6fd4474d3e845cedb + checksum: 10/8de7ea32856e75784601cacf8f4e3cbf04ce1fd05d56614b08b7bbe0674d1e59e37ccaa1c7ed16e3b181a63abe5bd43a1ab0e28b8c95618a9ebf0be5e24d6b25 languageName: node linkType: hard @@ -10265,6 +10383,78 @@ asn1@evs-broadcast/node-asn1: 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 + +"bare-path@npm:^3.0.0": + version: 3.0.0 + resolution: "bare-path@npm:3.0.0" + dependencies: + bare-os: "npm:^3.0.1" + checksum: 10/712d90e9cd8c3263cc11b0e0d386d1531a452706d7840c081ee586b34b00d72544e65df7a40013d47c1b177277495225deeede65cb2984db88a979cb65aaa2ff + languageName: node + linkType: hard + +"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 + +"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 + "base64-arraybuffer-es6@npm:^0.3.1": version: 0.3.1 resolution: "base64-arraybuffer-es6@npm:0.3.1" @@ -10279,12 +10469,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 @@ -10449,12 +10639,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 @@ -10617,18 +10807,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 @@ -10685,7 +10875,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: @@ -10993,10 +11183,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 +11272,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,17 +11334,10 @@ 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" - checksum: 10/8085fd8e5b1234fafacb279b4dab84dc127f512f953441daf09fc71ade70106af0dff28e86bfda00bab0de61fb475fa9003c87f82cbad3da02a4f299bfd427da +"chardet@npm:^2.1.1": + version: 2.1.1 + resolution: "chardet@npm:2.1.1" + checksum: 10/d56913b65e45c5c86f331988e2ef6264c131bfeadaae098ee719bf6610546c77740e37221ffec802dde56b5e4466613a4c754786f4da6b5f6c5477243454d324 languageName: node linkType: hard @@ -11250,13 +11423,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 +11444,18 @@ 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 + "ci-info@npm:^3.2.0": version: 3.8.0 resolution: "ci-info@npm:3.8.0" @@ -11339,19 +11517,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" @@ -11502,7 +11667,7 @@ asn1@evs-broadcast/node-asn1: 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 +11685,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 +11701,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 +11733,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,16 +11757,6 @@ 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 - languageName: node - linkType: hard - "columnify@npm:1.6.0": version: 1.6.0 resolution: "columnify@npm:1.6.0" @@ -11686,10 +11856,10 @@ 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 @@ -11736,21 +11906,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,12 +12224,12 @@ 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 @@ -12103,7 +12272,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: @@ -12544,10 +12713,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 +13109,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" @@ -13028,7 +13187,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"date-fns@npm:^2.0.1, date-fns@npm:^2.16.1": +"date-fns@npm:^2.0.1": version: 2.30.0 resolution: "date-fns@npm:2.30.0" dependencies: @@ -13074,7 +13233,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: @@ -13424,10 +13583,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 @@ -13729,7 +13888,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 +13906,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 +13928,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,21 +13940,21 @@ 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 @@ -14196,80 +14355,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.25.0": + version: 0.25.12 + resolution: "esbuild@npm:0.25.12" + dependencies: + "@esbuild/aix-ppc64": "npm:0.25.12" + "@esbuild/android-arm": "npm:0.25.12" + "@esbuild/android-arm64": "npm:0.25.12" + "@esbuild/android-x64": "npm:0.25.12" + "@esbuild/darwin-arm64": "npm:0.25.12" + "@esbuild/darwin-x64": "npm:0.25.12" + "@esbuild/freebsd-arm64": "npm:0.25.12" + "@esbuild/freebsd-x64": "npm:0.25.12" + "@esbuild/linux-arm": "npm:0.25.12" + "@esbuild/linux-arm64": "npm:0.25.12" + "@esbuild/linux-ia32": "npm:0.25.12" + "@esbuild/linux-loong64": "npm:0.25.12" + "@esbuild/linux-mips64el": "npm:0.25.12" + "@esbuild/linux-ppc64": "npm:0.25.12" + "@esbuild/linux-riscv64": "npm:0.25.12" + "@esbuild/linux-s390x": "npm:0.25.12" + "@esbuild/linux-x64": "npm:0.25.12" + "@esbuild/netbsd-arm64": "npm:0.25.12" + "@esbuild/netbsd-x64": "npm:0.25.12" + "@esbuild/openbsd-arm64": "npm:0.25.12" + "@esbuild/openbsd-x64": "npm:0.25.12" + "@esbuild/openharmony-arm64": "npm:0.25.12" + "@esbuild/sunos-x64": "npm:0.25.12" + "@esbuild/win32-arm64": "npm:0.25.12" + "@esbuild/win32-ia32": "npm:0.25.12" + "@esbuild/win32-x64": "npm:0.25.12" dependenciesMeta: "@esbuild/aix-ppc64": optional: true @@ -14313,6 +14428,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 +14440,7 @@ asn1@evs-broadcast/node-asn1: optional: true bin: esbuild: bin/esbuild - checksum: 10/95425071c9f24ff88bf61e0710b636ec0eb24ddf8bd1f7e1edef3044e1221104bbfa7bbb31c18018c8c36fa7902c5c0b843f829b981ebc89160cf5eebdaa58f4 + checksum: 10/bc9c03d64e96a0632a926662c9d29decafb13a40e5c91790f632f02939bc568edc9abe0ee5d8055085a2819a00139eb12e223cfb8126dbf89bbc569f125d91fd languageName: node linkType: hard @@ -14622,18 +14739,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" @@ -14800,16 +14905,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 +14926,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" @@ -14970,15 +15074,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 +15090,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,6 +15142,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"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, fast-glob@npm:^3.3.2": version: 3.3.3 resolution: "fast-glob@npm:3.3.3" @@ -15122,6 +15213,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" @@ -15174,7 +15276,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fdir@npm:^6.4.3, fdir@npm:^6.5.0": +"fdir@npm:^6.4.3, fdir@npm:^6.4.4, fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" peerDependencies: @@ -15202,13 +15304,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 +15334,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 @@ -15559,14 +15654,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 @@ -15758,13 +15853,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 +16003,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,15 +16019,14 @@ 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 @@ -15970,7 +16046,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 +16062,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: @@ -16024,6 +16089,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"glob@npm:^9.2.0": + version: 9.3.5 + resolution: "glob@npm:9.3.5" + 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 + languageName: node + linkType: hard + "global-dirs@npm:^3.0.0": version: 3.0.1 resolution: "global-dirs@npm:3.0.1" @@ -16033,13 +16110,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,7 +16117,7 @@ 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 @@ -16206,7 +16276,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: @@ -16300,15 +16370,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"has@npm:^1.0.3": - version: 1.0.3 - resolution: "has@npm:1.0.3" - dependencies: - function-bind: "npm:^1.1.1" - checksum: 10/a449f3185b1d165026e8d25f6a8c3390bd25c201ff4b8c1aaf948fc6a5fcfd6507310b8c00c13a3325795ea9791fcc3d79d61eafa313b5750438fc19183df57b - languageName: node - linkType: hard - "hash-base@npm:^3.0.0": version: 3.1.0 resolution: "hash-base@npm:3.1.0" @@ -16892,7 +16953,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, https-proxy-agent@npm:^5.0.1": version: 5.0.1 resolution: "https-proxy-agent@npm:5.0.1" dependencies: @@ -16972,7 +17033,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: @@ -17108,15 +17169,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 @@ -17184,10 +17245,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 +17343,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 +17362,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 +17469,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" @@ -17470,12 +17524,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.1": - version: 2.13.0 - resolution: "is-core-module@npm:2.13.0" +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.16.1, is-core-module@npm:^2.5.0, is-core-module@npm:^2.8.1": + version: 2.16.1 + resolution: "is-core-module@npm:2.16.1" dependencies: - has: "npm:^1.0.3" - checksum: 10/55ccb5ccd208a1e088027065ee6438a99367e4c31c366b52fbaeac8fa23111cd17852111836d904da604801b3286d38d3d1ffa6cd7400231af8587f021099dc6 + hasown: "npm:^2.0.2" + checksum: 10/452b2c2fb7f889cbbf7e54609ef92cf6c24637c568acc7e63d166812a0fb365ae8a504c333a29add8bdb1686704068caa7f4e4b639b650dde4f00a038b8941fb languageName: node linkType: hard @@ -17744,13 +17798,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" @@ -18552,7 +18599,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"jest-util@npm:^29.0.0, jest-util@npm:^29.7.0": +"jest-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-util@npm:29.7.0" dependencies: @@ -18784,7 +18831,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 +18840,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 +18918,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" +"json-schema-to-typescript@npm:^15.0.4": + version: 15.0.4 + resolution: "json-schema-to-typescript@npm:15.0.4" 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" - 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 +19043,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.3.0 + resolution: "jsonpath-plus@npm:10.3.0" dependencies: "@jsep-plugin/assignment": "npm:^1.3.0" "@jsep-plugin/regex": "npm:^1.0.4" @@ -19030,7 +19053,7 @@ asn1@evs-broadcast/node-asn1: bin: jsonpath: bin/jsonpath-cli.js jsonpath-plus: bin/jsonpath-cli.js - checksum: 10/3a6bd775d4348f5e014249a11abb635af2f1265d83ba716b3d633ca3f118e79c318223dd685170c50652494a492f3354163bbe4cd5554bb4d7992fecf53c4874 + checksum: 10/082302334414c7c5ab0cc8239563118f7f14bb2949d001b009f436491d00f94a7a293eed3eaf61ffdaf72f6fda9d25198a4280c4f68a4c403154ca7ed2bd0dc9 languageName: node linkType: hard @@ -19182,9 +19205,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 +19227,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 @@ -19484,21 +19507,21 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/live-status-gateway-api": "npm:26.3.0-0" "@sofie-automation/server-core-integration": "npm:26.3.0-0" "@sofie-automation/shared-lib": "npm:26.3.0-0" - debug: "npm:^4.4.0" + 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 @@ -19655,13 +19678,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" @@ -19822,15 +19838,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" @@ -20433,22 +20440,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" @@ -21255,7 +21246,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 @@ -21411,10 +21402,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 @@ -21555,9 +21546,9 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/server-core-integration": "npm:26.3.0-0" "@sofie-automation/shared-lib": "npm:26.3.0-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" + winston: "npm:^3.19.0" languageName: unknown linkType: soft @@ -21665,18 +21656,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: @@ -21750,13 +21730,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" @@ -22561,7 +22534,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 @@ -22800,10 +22773,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 +22866,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 +23142,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,15 +23172,15 @@ 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" + "@babel/core": "npm:^7.29.0" + "@babel/plugin-transform-modules-commonjs": "npm:^7.28.6" "@sofie-automation/code-standard-preset": "npm:^3.0.0" - "@types/amqplib": "npm:^0.10.6" + "@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/node": "npm:^22.19.8" "@types/object-path": "npm:^0.11.4" "@types/underscore": "npm:^1.13.0" babel-jest: "npm:^29.7.0" @@ -23217,17 +23190,17 @@ asn1@evs-broadcast/node-asn1: 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" + json-schema-to-typescript: "npm:^15.0.4" lerna: "npm:^9.0.3" 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: @@ -23660,10 +23633,10 @@ 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.2.0": + version: 8.3.0 + resolution: "path-to-regexp@npm:8.3.0" + checksum: 10/568f148fc64f5fd1ecebf44d531383b28df924214eabf5f2570dce9587a228e36c37882805ff02d71c6209b080ea3ee6a4d2b712b5df09741b67f1f3cf91e55a languageName: node linkType: hard @@ -23683,13 +23656,6 @@ asn1@evs-broadcast/node-asn1: 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 - languageName: node - linkType: hard - "path-type@npm:^3.0.0": version: 3.0.0 resolution: "path-type@npm:3.0.0" @@ -23751,7 +23717,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 @@ -23854,7 +23820,7 @@ asn1@evs-broadcast/node-asn1: 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: @@ -23921,12 +23887,12 @@ asn1@evs-broadcast/node-asn1: dependencies: "@sofie-automation/server-core-integration": "npm:26.3.0-0" "@sofie-automation/shared-lib": "npm:26.3.0-0" - debug: "npm:^4.4.0" - influx: "npm:^5.9.7" + debug: "npm:^4.4.3" + influx: "npm:^5.12.0" timeline-state-resolver: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.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 @@ -24778,7 +24744,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.3, postcss@npm:^8.5.4": version: 8.5.6 resolution: "postcss@npm:8.5.6" dependencies: @@ -24805,21 +24771,12 @@ asn1@evs-broadcast/node-asn1: 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 @@ -24948,7 +24905,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 @@ -25110,7 +25067,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,7 +25083,7 @@ 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 @@ -25208,23 +25165,34 @@ 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 + +"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 @@ -25436,9 +25404,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,16 +25428,16 @@ asn1@evs-broadcast/node-asn1: peerDependenciesMeta: "@types/react": optional: true - checksum: 10/aaa923af1cf724471f8c9dd7aa365b0e5556cb3aa63b023912353b6874d7f274b4362a9b97df91fb0c04e998c7a49fa8c5e071f3ba468e888afa26b2d0d27ca2 + checksum: 10/0ed950e0705779312bc625a422d86981c3dbf5d53ff5a038983dee70463c66430b6f3ccac08dd8129665952745e151ff8c35c7dd48a4de89d8322e8cbc23337b 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" +"react-circular-progressbar@npm:*, react-circular-progressbar@npm:^2.2.0": + version: 2.2.0 + resolution: "react-circular-progressbar@npm:2.2.0" peerDependencies: - react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - checksum: 10/a2ff10b2a070dd2443d6e8a25c36e27e1c3239e43f16970907c83b5fc99f1da9887b4a7a9b29ed420ff1604f1e0415e4705186d1eccce55f68adfbb0a6d75902 + react: ">=0.14.0" + checksum: 10/3b117f58745745dcbfe8b326945343f50761cd434ae241ae8d9ce7ba5a7ee662038e34e341a209cb05141d2ca654c57120a1940b32a0f0c2890edb54b4316bf4 languageName: node linkType: hard @@ -25608,16 +25576,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 @@ -25733,10 +25701,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.17.0": + version: 0.17.0 + resolution: "react-refresh@npm:0.17.0" + checksum: 10/5e94f07d43bb1cfdc9b0c6e0c8c73e754005489950dcff1edb53aa8451d1d69a47b740b195c7c80fb4eb511c56a3585dc55eddd83f0097fb5e015116a1460467 languageName: node linkType: hard @@ -26131,12 +26099,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 @@ -26154,15 +26122,6 @@ asn1@evs-broadcast/node-asn1: 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": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" @@ -26177,17 +26136,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 +26175,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 +26342,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 @@ -26486,16 +26444,16 @@ asn1@evs-broadcast/node-asn1: 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.20.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 +26479,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.20.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 +26563,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: @@ -26627,15 +26585,15 @@ asn1@evs-broadcast/node-asn1: 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 @@ -26679,30 +26637,36 @@ asn1@evs-broadcast/node-asn1: 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" +"rollup@npm:^4.34.9": + version: 4.57.1 + resolution: "rollup@npm:4.57.1" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.57.1" + "@rollup/rollup-android-arm64": "npm:4.57.1" + "@rollup/rollup-darwin-arm64": "npm:4.57.1" + "@rollup/rollup-darwin-x64": "npm:4.57.1" + "@rollup/rollup-freebsd-arm64": "npm:4.57.1" + "@rollup/rollup-freebsd-x64": "npm:4.57.1" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.57.1" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.57.1" + "@rollup/rollup-linux-arm64-gnu": "npm:4.57.1" + "@rollup/rollup-linux-arm64-musl": "npm:4.57.1" + "@rollup/rollup-linux-loong64-gnu": "npm:4.57.1" + "@rollup/rollup-linux-loong64-musl": "npm:4.57.1" + "@rollup/rollup-linux-ppc64-gnu": "npm:4.57.1" + "@rollup/rollup-linux-ppc64-musl": "npm:4.57.1" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.57.1" + "@rollup/rollup-linux-riscv64-musl": "npm:4.57.1" + "@rollup/rollup-linux-s390x-gnu": "npm:4.57.1" + "@rollup/rollup-linux-x64-gnu": "npm:4.57.1" + "@rollup/rollup-linux-x64-musl": "npm:4.57.1" + "@rollup/rollup-openbsd-x64": "npm:4.57.1" + "@rollup/rollup-openharmony-arm64": "npm:4.57.1" + "@rollup/rollup-win32-arm64-msvc": "npm:4.57.1" + "@rollup/rollup-win32-ia32-msvc": "npm:4.57.1" + "@rollup/rollup-win32-x64-gnu": "npm:4.57.1" + "@rollup/rollup-win32-x64-msvc": "npm:4.57.1" + "@types/estree": "npm:1.0.8" fsevents: "npm:~2.3.2" dependenciesMeta: "@rollup/rollup-android-arm-eabi": @@ -26725,29 +26689,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/0451371339e593967c979e498fac4dfd0ba15fadf0dac96875940796307a00d62ab68460366a65f4872ae8edd9339e3d9501e8e5764c1f23e25e0951f75047c6 languageName: node linkType: hard @@ -26830,15 +26806,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" @@ -26908,9 +26875,9 @@ 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@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,7 +26888,7 @@ asn1@evs-broadcast/node-asn1: optional: true bin: sass: sass.js - checksum: 10/9a7d1c6be1a9e711a1c561d189b9816aa7715f6d0ec0b2ec181f64163788d0caaf4741924eeadce558720b58b1de0e9b21b9dae6a0d14489c4d2a142d3f3b12e + checksum: 10/707ef8e525ed32d375e737346140d4b675f44de208df996c2df3407f5e62f3f38226ea1faf41a9fd4b068201e67b3a7e152b9e9c3b098daa847dd480c735f038 languageName: node linkType: hard @@ -27043,7 +27010,7 @@ 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": +"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, semver@npm:^7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -27231,15 +27198,16 @@ asn1@evs-broadcast/node-asn1: 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,7 +27250,7 @@ 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 @@ -27409,15 +27377,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" @@ -27697,13 +27656,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" @@ -27981,6 +27933,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" @@ -28213,12 +28176,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 +28248,15 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"supports-color@npm:8.1.1, supports-color@npm:^8.0.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-color@npm:^5.3.0, supports-color@npm:^5.5.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -28296,15 +28275,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" @@ -28381,19 +28351,35 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tar-fs@npm:2.1.1": - version: 2.1.1 - resolution: "tar-fs@npm:2.1.1" +"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:^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.1.4, tar-stream@npm:~2.2.0": +"tar-stream@npm:~2.2.0": version: 2.2.0 resolution: "tar-stream@npm:2.2.0" dependencies: @@ -28515,6 +28501,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 +28524,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 +28542,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 +28571,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 @@ -28690,16 +28667,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 +28698,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12": +"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.9": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -28766,15 +28733,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 +28747,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"to-buffer@npm:^1.2.0": + 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" @@ -28815,13 +28784,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 @@ -28907,7 +28877,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: @@ -29002,25 +28972,26 @@ asn1@evs-broadcast/node-asn1: 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 +29004,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 +29082,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 @@ -29238,10 +29211,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 +29239,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,6 +29292,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"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 + "typed-styles@npm:^0.0.7": version: 0.0.7 resolution: "typed-styles@npm:0.0.7" @@ -29356,9 +29322,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,10 +29332,10 @@ 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 @@ -29510,16 +29476,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" @@ -29557,10 +29513,10 @@ asn1@evs-broadcast/node-asn1: 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 +29554,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 @@ -29813,9 +29769,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 +29779,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 @@ -30148,14 +30104,17 @@ 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:^6.4.1": + version: 6.4.1 + resolution: "vite@npm:6.4.1" dependencies: - esbuild: "npm:^0.24.2" + esbuild: "npm:^0.25.0" + fdir: "npm:^6.4.4" fsevents: "npm:~2.3.3" - postcss: "npm:^8.4.49" - rollup: "npm:^4.23.0" + picomatch: "npm:^4.0.2" + postcss: "npm:^8.5.3" + rollup: "npm:^4.34.9" + tinyglobby: "npm:^0.2.13" peerDependencies: "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 jiti: ">=1.21.0" @@ -30196,7 +30155,7 @@ asn1@evs-broadcast/node-asn1: optional: true bin: vite: bin/vite.js - checksum: 10/753d06b07a4d90863d3478162cfb18fa5cd7f6eb22a74525348a8fd46593a82875d0f92352c2f4833e15cb6581fc97d6ab434c0c5d83d8d58cfbbe6e7267726d + checksum: 10/ea2083b6b1d1c9e85a13d6797ae989aa1dbc27a5c054319c71141934bf3f8dba8d54b510618040f95751148da63787f28f043df7458a194c81f8b6d8a2d32844 languageName: node linkType: hard @@ -30388,6 +30347,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" @@ -30828,12 +30794,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 +30809,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 @@ -30994,21 +30960,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 +30975,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.11.0, ws@npm:^8.13.0, ws@npm:^8.18.0, ws@npm:^8.18.3, 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 +30986,7 @@ asn1@evs-broadcast/node-asn1: optional: true utf-8-validate: optional: true - checksum: 10/725964438d752f0ab0de582cd48d6eeada58d1511c3f613485b5598a83680bedac6187c765b0fe082e2d8cc4341fc57707c813ae780feee82d0c5efe6a4c61b6 + checksum: 10/26e4901e93abaf73af9f26a93707c95b4845e91a7a347ec8c569e6e9be7f9df066f6c2b817b2d685544e208207898a750b78461e6e8d810c11a370771450c31b languageName: node linkType: hard @@ -31157,12 +31108,12 @@ asn1@evs-broadcast/node-asn1: 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 +31131,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.3.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 +31199,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" From 3fce86508488f78585e2f1942d37f60855ed04f1 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 13:42:21 +0000 Subject: [PATCH 062/291] chore: update ui libs --- packages/webui/package.json | 29 +- packages/yarn.lock | 1111 +++++++++++++++++++++-------------- 2 files changed, 677 insertions(+), 463 deletions(-) diff --git a/packages/webui/package.json b/packages/webui/package.json index 8463dfd0266..72cf5f940a2 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -31,7 +31,7 @@ "license-validate": "run -T sofie-licensecheck" }, "dependencies": { - "@crello/react-lottie": "0.0.9", + "@crello/react-lottie": "0.0.11", "@fortawesome/fontawesome-free": "^7.1.0", "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.1.0", @@ -52,19 +52,19 @@ "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-web": "^5.13.0", "moment": "^2.30.1", - "motion": "^12.4.7", + "motion": "^12.31.0", "promise.allsettled": "^1.0.7", - "query-string": "^6.14.1", + "query-string": "^9.3.1", "rc-tooltip": "^6.4.0", "react": "^18.3.1", "react-bootstrap": "^2.10.10", "react-circular-progressbar": "^2.2.0", - "react-datepicker": "^3.8.0", + "react-datepicker": "^9.1.0", "react-dnd": "^14.0.5", "react-dnd-html5-backend": "^14.1.0", "react-dom": "^18.3.1", @@ -72,13 +72,13 @@ "react-hotkeys": "^2.0.0", "react-i18next": "^11.18.6", "react-intersection-observer": "^9.16.0", - "react-moment": "^0.9.7", + "react-moment": "^1.2.1", "react-popper": "^2.3.0", "react-router-bootstrap": "^0.25.0", "react-router-dom": "^5.3.4", "semver": "^7.7.3", "sha.js": "^2.4.12", - "shuttle-webhid": "^0.0.2", + "shuttle-webhid": "^0.1.3", "type-fest": "^4.41.0", "underscore": "^1.13.7", "webmidi": "^2.5.3", @@ -94,23 +94,22 @@ "@types/deep-extend": "^0.6.2", "@types/react": "^18.3.27", "@types/react-circular-progressbar": "^1.1.0", - "@types/react-datepicker": "^3.1.8", "@types/react-dom": "^18.3.7", "@types/react-router": "^5.1.20", "@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.7.0", + "@vitejs/plugin-react": "^5.1.3", "@welldone-software/why-did-you-render": "^4.3.2", "@xmldom/xmldom": "^0.8.11", "babel-jest": "^29.7.0", - "globals": "^15.15.0", - "sass": "^1.97.3", + "globals": "^17.3.0", + "sass-embedded": "^1.97.3", "sinon": "^14.0.2", "typescript": "~5.7.3", - "vite": "^6.4.1", - "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/yarn.lock b/packages/yarn.lock index dc2fc072f2c..d28303e8998 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -707,7 +707,7 @@ __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.28.0, @babel/core@npm:^7.29.0": +"@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.29.0": version: 7.29.0 resolution: "@babel/core@npm:7.29.0" dependencies: @@ -2070,7 +2070,7 @@ __metadata: 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.7, @babel/runtime@npm:^7.9.2": +"@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.26.9 resolution: "@babel/runtime@npm:7.26.9" dependencies: @@ -2136,6 +2136,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" @@ -2192,15 +2199,15 @@ __metadata: languageName: node linkType: hard -"@crello/react-lottie@npm:0.0.9": - version: 0.0.9 - resolution: "@crello/react-lottie@npm:0.0.9" +"@crello/react-lottie@npm:0.0.11": + version: 0.0.11 + resolution: "@crello/react-lottie@npm:0.0.11" dependencies: - lottie-web: "npm:5.5.9" + lottie-web: "npm:^5.7.3" peerDependencies: react: ~16.9.0 react-dom: ~16.9.0 - checksum: 10/13e751a5995184f44bc01fee7ca70354164564e4c37e063d363bd262756cdd60f3032f2c4b6bbb91cd5f8ef5fb8e75cd60086a6d253ca0c2737651fc40a93cd6 + checksum: 10/23629b6cdd5b703f936ef4263696feecaadf3b642d6442afae94043c4bc0e4c4a98e249257a277812c9214ebca4363c37466fba1c9c7dee951ade50c31e02737 languageName: node linkType: hard @@ -3485,184 +3492,184 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/aix-ppc64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/android-arm64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/android-arm@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/android-x64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/darwin-arm64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/darwin-x64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/freebsd-arm64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/freebsd-x64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-arm64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-arm@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-ia32@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-loong64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-mips64el@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-ppc64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-riscv64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-s390x@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/linux-x64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/netbsd-arm64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/netbsd-x64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/openbsd-arm64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/openbsd-x64@npm:0.25.12" +"@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/openharmony-arm64@npm:0.25.12": - version: 0.25.12 - resolution: "@esbuild/openharmony-arm64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/sunos-x64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/win32-arm64@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/win32-ia32@npm:0.25.12" +"@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.25.12": - version: 0.25.12 - resolution: "@esbuild/win32-x64@npm:0.25.12" +"@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 @@ -3755,6 +3762,58 @@ __metadata: languageName: node linkType: hard +"@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 + +"@floating-ui/dom@npm:^1.7.5": + version: 1.7.5 + resolution: "@floating-ui/dom@npm:1.7.5" + dependencies: + "@floating-ui/core": "npm:^1.7.4" + "@floating-ui/utils": "npm:^0.2.10" + checksum: 10/2764990da82bd5cfe942211480aa82352926326008de93f5f3f19749cc8b171fe05b77526a2652605eadcbeab902c6506f18d60a4c43281f2651802047de100b + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^2.1.7": + version: 2.1.7 + resolution: "@floating-ui/react-dom@npm:2.1.7" + dependencies: + "@floating-ui/dom": "npm:^1.7.5" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10/870eb2109af3ab09ea0076eb8e0ad307da274978c3dfe28e83422136a7f85cac700f62c37663c75bc25b174d33457c0224d1944e80b9a7ca5ff7b28b8f77b7ab + languageName: node + linkType: hard + +"@floating-ui/react@npm:^0.27.15": + version: 0.27.17 + resolution: "@floating-ui/react@npm:0.27.17" + dependencies: + "@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" @@ -3997,19 +4056,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" @@ -6359,10 +6405,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-beta.27": - version: 1.0.0-beta.27 - resolution: "@rolldown/pluginutils@npm:1.0.0-beta.27" - checksum: 10/4f7da788d88b33d029d5acf84c63be27c62d7c53017476f2e3026172cf94062cb399cd15194c89574578f192016bbcb1e040ce6811b3bb9ec4d4faa2ad386459 +"@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 @@ -6759,12 +6805,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 @@ -7171,7 +7218,7 @@ __metadata: resolution: "@sofie-automation/webui@workspace:webui" dependencies: "@babel/preset-env": "npm:^7.29.0" - "@crello/react-lottie": "npm:0.0.9" + "@crello/react-lottie": "npm:0.0.11" "@fortawesome/fontawesome-free": "npm:^7.1.0" "@fortawesome/fontawesome-svg-core": "npm:^7.1.0" "@fortawesome/free-solid-svg-icons": "npm:^7.1.0" @@ -7193,7 +7240,6 @@ __metadata: "@types/deep-extend": "npm:^0.6.2" "@types/react": "npm:^18.3.27" "@types/react-circular-progressbar": "npm:^1.1.0" - "@types/react-datepicker": "npm:^3.1.8" "@types/react-dom": "npm:^18.3.7" "@types/react-router": "npm:^5.1.20" "@types/react-router-bootstrap": "npm:^0.26.8" @@ -7201,7 +7247,7 @@ __metadata: "@types/sha.js": "npm:^2.4.4" "@types/sinon": "npm:^10.0.20" "@types/xml2js": "npm:^0.4.14" - "@vitejs/plugin-react": "npm:^4.7.0" + "@vitejs/plugin-react": "npm:^5.1.3" "@welldone-software/why-did-you-render": "npm:^4.3.2" "@xmldom/xmldom": "npm:^0.8.11" babel-jest: "npm:^29.7.0" @@ -7210,21 +7256,21 @@ __metadata: cubic-spline: "npm:^3.0.3" deep-extend: "npm:0.6.0" ejson: "npm:^2.2.3" - globals: "npm:^15.15.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-web: "npm:^5.13.0" 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.10" react-circular-progressbar: "npm:^2.2.0" - react-datepicker: "npm:^3.8.0" + 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" @@ -7232,20 +7278,20 @@ __metadata: react-hotkeys: "npm:^2.0.0" react-i18next: "npm:^11.18.6" react-intersection-observer: "npm:^9.16.0" - react-moment: "npm:^0.9.7" + 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.97.3" + sass-embedded: "npm:^1.97.3" semver: "npm:^7.7.3" sha.js: "npm:^2.4.12" - shuttle-webhid: "npm:^0.0.2" + shuttle-webhid: "npm:^0.1.3" sinon: "npm:^14.0.2" type-fest: "npm:^4.41.0" typescript: "npm:~5.7.3" underscore: "npm:^1.13.7" - vite: "npm:^6.4.1" - 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" @@ -8696,17 +8742,6 @@ __metadata: 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.7": version: 18.3.7 resolution: "@types/react-dom@npm:18.3.7" @@ -8937,7 +8972,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 @@ -9136,19 +9171,19 @@ __metadata: languageName: node linkType: hard -"@vitejs/plugin-react@npm:^4.7.0": - version: 4.7.0 - resolution: "@vitejs/plugin-react@npm:4.7.0" +"@vitejs/plugin-react@npm:^5.1.3": + version: 5.1.3 + resolution: "@vitejs/plugin-react@npm:5.1.3" dependencies: - "@babel/core": "npm:^7.28.0" + "@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-beta.27" + "@rolldown/pluginutils": "npm:1.0.0-rc.2" "@types/babel__core": "npm:^7.20.5" - react-refresh: "npm:^0.17.0" + react-refresh: "npm:^0.18.0" peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - checksum: 10/619a5d650ce0e8e2f37dae369b803990c6647e81ec983a00e44a734b3feeefd5a32c20fbee56d496fbc239cad6b949797dddf7c6d9f23c48100c5b2b5dc41e1f + checksum: 10/e431b2ea5b33f96e670ccf1c7e597bda46581f9eef5033249cf9fd74f4c3d9927a402d143568befaa22c6f98af571478c6cae84c5212e3f2a124d922d5c04f6d languageName: node linkType: hard @@ -10595,10 +10630,10 @@ asn1@evs-broadcast/node-asn1: 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.2 + resolution: "bn.js@npm:5.2.2" + checksum: 10/51ebb2df83b33e5d8581165206e260d5e9c873752954616e5bf3758952b84d7399a9c6d00852815a0aeefb1150a7f34451b62d4287342d457fa432eee869e83e languageName: node linkType: hard @@ -10747,7 +10782,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: @@ -10770,31 +10805,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 @@ -11487,7 +11522,7 @@ asn1@evs-broadcast/node-asn1: 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": +"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 @@ -11757,6 +11792,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"colorjs.io@npm:^0.5.0": + version: 0.5.2 + resolution: "colorjs.io@npm:0.5.2" + checksum: 10/a6f6345865b177d19481008cb299c46ec9ff1fd206f472cd9ef69ddbca65832c81237b19fdcd24f3f9540c3e6343a22eb486cd800f5eab9815ce7c98c16a0f0e + languageName: node + linkType: hard + "columnify@npm:1.6.0": version: 1.6.0 resolution: "columnify@npm:1.6.0" @@ -12306,7 +12348,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: @@ -12316,7 +12358,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: @@ -12329,7 +12371,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: @@ -12367,12 +12409,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 @@ -12387,22 +12429,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 @@ -13187,15 +13230,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"date-fns@npm:^2.0.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" @@ -13334,20 +13368,6 @@ 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 - languageName: node - linkType: hard - "deep-equal@npm:~1.0.1": version: 1.0.1 resolution: "deep-equal@npm:1.0.1" @@ -13621,7 +13641,7 @@ asn1@evs-broadcast/node-asn1: 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: @@ -13725,10 +13745,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 @@ -13958,9 +13978,9 @@ asn1@evs-broadcast/node-asn1: 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" @@ -13969,7 +13989,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 @@ -14355,36 +14375,36 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"esbuild@npm:^0.25.0": - version: 0.25.12 - resolution: "esbuild@npm:0.25.12" - dependencies: - "@esbuild/aix-ppc64": "npm:0.25.12" - "@esbuild/android-arm": "npm:0.25.12" - "@esbuild/android-arm64": "npm:0.25.12" - "@esbuild/android-x64": "npm:0.25.12" - "@esbuild/darwin-arm64": "npm:0.25.12" - "@esbuild/darwin-x64": "npm:0.25.12" - "@esbuild/freebsd-arm64": "npm:0.25.12" - "@esbuild/freebsd-x64": "npm:0.25.12" - "@esbuild/linux-arm": "npm:0.25.12" - "@esbuild/linux-arm64": "npm:0.25.12" - "@esbuild/linux-ia32": "npm:0.25.12" - "@esbuild/linux-loong64": "npm:0.25.12" - "@esbuild/linux-mips64el": "npm:0.25.12" - "@esbuild/linux-ppc64": "npm:0.25.12" - "@esbuild/linux-riscv64": "npm:0.25.12" - "@esbuild/linux-s390x": "npm:0.25.12" - "@esbuild/linux-x64": "npm:0.25.12" - "@esbuild/netbsd-arm64": "npm:0.25.12" - "@esbuild/netbsd-x64": "npm:0.25.12" - "@esbuild/openbsd-arm64": "npm:0.25.12" - "@esbuild/openbsd-x64": "npm:0.25.12" - "@esbuild/openharmony-arm64": "npm:0.25.12" - "@esbuild/sunos-x64": "npm:0.25.12" - "@esbuild/win32-arm64": "npm:0.25.12" - "@esbuild/win32-ia32": "npm:0.25.12" - "@esbuild/win32-x64": "npm:0.25.12" +"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 @@ -14440,7 +14460,7 @@ asn1@evs-broadcast/node-asn1: optional: true bin: esbuild: bin/esbuild - checksum: 10/bc9c03d64e96a0632a926662c9d29decafb13a40e5c91790f632f02939bc568edc9abe0ee5d8055085a2819a00139eb12e223cfb8126dbf89bbc569f125d91fd + checksum: 10/7f1229328b0efc63c4184a61a7eb303df1e99818cc1d9e309fb92600703008e69821e8e984e9e9f54a627da14e0960d561db3a93029482ef96dc82dd267a60c2 languageName: node linkType: hard @@ -15276,7 +15296,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"fdir@npm:^6.4.3, fdir@npm:^6.4.4, fdir@npm:^6.5.0": +"fdir@npm:^6.4.3, fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" peerDependencies: @@ -15609,12 +15629,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": "*" @@ -15627,7 +15647,7 @@ asn1@evs-broadcast/node-asn1: optional: true react-dom: optional: true - checksum: 10/d277e75f1ed8af69f145f263758aa046083a2c0b4f9a5e48911f3847d38d7c3bdb361e97876f635ae58d5bdb4b9cc50f6e8c631b8e225c6d8233584f106116e2 + checksum: 10/8cd76953b5e4e81e69b7bbec699cd5c913df87897148cd0f9fe85fa2f1c4e2768fbaeb6e40be9cf6d3f4b17a5a8024351d7e0ba93a9cdf9d8358c5031c5cdecd languageName: node linkType: hard @@ -16124,6 +16144,13 @@ asn1@evs-broadcast/node-asn1: 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" @@ -16246,13 +16273,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" @@ -16370,24 +16390,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 @@ -17006,21 +17027,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 @@ -17807,7 +17828,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: @@ -19755,17 +19776,10 @@ 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 - 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.13.0, lottie-web@npm:^5.7.3": + version: 5.13.0 + resolution: "lottie-web@npm:5.13.0" + checksum: 10/ccc65b91ddc569c874de265252ef41cb546798515dd63c5ee366844efd1e10335c080c483ce4305faba0cebd54c4afd6bb918fd0d6f4394dcc284fc0c3944941 languageName: node linkType: hard @@ -21552,27 +21566,27 @@ asn1@evs-broadcast/node-asn1: 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": "*" @@ -21585,7 +21599,7 @@ asn1@evs-broadcast/node-asn1: optional: true react-dom: optional: true - checksum: 10/3f649dc2688bf6a5627a34193dc3b3e9d277d65ca1be2159a65a80f72c140c2f52fbdde50b876ebd0c2b00622a222048a6e72aa15eefe6038cce0192657c1d44 + checksum: 10/abb2253b2e679f3d153e472f63e29fa0f3d9d4dd6bb2f2b0c9026897dd069a2371ddc2374bf70debdd480351ad5311e830ebf98e5ea7a44588e44099381e950b languageName: node linkType: hard @@ -21825,7 +21839,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: @@ -21930,9 +21944,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" @@ -21941,8 +21955,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" @@ -21958,10 +21972,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 @@ -23321,17 +23335,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 @@ -23679,16 +23692,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 @@ -23920,13 +23934,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" @@ -24744,7 +24751,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.5.3, 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: @@ -25104,7 +25111,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: @@ -25203,7 +25210,7 @@ asn1@evs-broadcast/node-asn1: 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: @@ -25212,6 +25219,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" @@ -25297,7 +25313,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: @@ -25441,19 +25457,21 @@ asn1@evs-broadcast/node-asn1: 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 @@ -25649,41 +25667,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 @@ -25701,10 +25692,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-refresh@npm:^0.17.0": - version: 0.17.0 - resolution: "react-refresh@npm:0.17.0" - checksum: 10/5e94f07d43bb1cfdc9b0c6e0c8c73e754005489950dcff1edb53aa8451d1d69a47b740b195c7c80fb4eb511c56a3585dc55eddd83f0097fb5e015116a1460467 +"react-refresh@npm:^0.18.0": + version: 0.18.0 + resolution: "react-refresh@npm:0.18.0" + checksum: 10/504c331c19776bf8320c23bad7f80b3a28de03301ed7523b0dd21d3f02bf2b53bbdd5aa52469b187bc90f358614b2ba303c088a0765c95f4f0a68c43a7d67b1d languageName: node linkType: hard @@ -26122,7 +26113,7 @@ asn1@evs-broadcast/node-asn1: 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: @@ -26606,13 +26597,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 @@ -26637,7 +26628,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"rollup@npm:^4.34.9": +"rollup@npm:^4.43.0": version: 4.57.1 resolution: "rollup@npm:4.57.1" dependencies: @@ -26797,7 +26788,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: @@ -26826,7 +26817,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 @@ -26875,7 +26866,209 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"sass@npm:^1.97.3": +"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: @@ -27257,15 +27450,14 @@ asn1@evs-broadcast/node-asn1: 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 @@ -28248,7 +28440,7 @@ 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, supports-color@npm:^8.0.0, supports-color@npm:^8.1.1": version: 8.1.1 resolution: "supports-color@npm:8.1.1" dependencies: @@ -28325,6 +28517,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" @@ -28334,6 +28535,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"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.9.1": version: 0.9.2 resolution: "synckit@npm:0.9.2" @@ -28344,6 +28552,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"tabbable@npm:^6.0.0": + version: 6.4.0 + resolution: "tabbable@npm:6.4.0" + checksum: 10/0fe8fada2d97bd02058af2e0176bddca26b1100c069e0a096ac19ad8ef61bd0b4f0cf05e1dd68229b8f1cb6fe6bf4c34d50a5f4a3e26b150a92f89b7dc0a4916 + 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" @@ -28698,7 +28913,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.9": +"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: @@ -28747,7 +28962,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"to-buffer@npm:^1.2.0": +"to-buffer@npm:^1.2.0, to-buffer@npm:^1.2.1": version: 1.2.2 resolution: "to-buffer@npm:1.2.2" dependencies: @@ -29299,13 +29514,6 @@ 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 - languageName: node - linkType: hard - "typedarray-to-buffer@npm:^3.1.5": version: 3.1.5 resolution: "typedarray-to-buffer@npm:3.1.5" @@ -29866,13 +30074,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 @@ -30038,6 +30246,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" @@ -30076,15 +30291,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 @@ -30104,26 +30319,26 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"vite@npm:^6.4.1": - version: 6.4.1 - resolution: "vite@npm:6.4.1" +"vite@npm:^7.3.1": + version: 7.3.1 + resolution: "vite@npm:7.3.1" dependencies: - esbuild: "npm:^0.25.0" - fdir: "npm:^6.4.4" + esbuild: "npm:^0.27.0" + fdir: "npm:^6.5.0" fsevents: "npm:~2.3.3" - picomatch: "npm:^4.0.2" - postcss: "npm:^8.5.3" - rollup: "npm:^4.34.9" - tinyglobby: "npm:^0.2.13" + 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 @@ -30155,7 +30370,7 @@ asn1@evs-broadcast/node-asn1: optional: true bin: vite: bin/vite.js - checksum: 10/ea2083b6b1d1c9e85a13d6797ae989aa1dbc27a5c054319c71141934bf3f8dba8d54b510618040f95751148da63787f28f043df7458a194c81f8b6d8a2d32844 + checksum: 10/62e48ffa4283b688f0049005405a004447ad38ffc99a0efea4c3aa9b7eed739f7402b43f00668c0ee5a895b684dc953d62f0722d8a92c5b2f6c95f051bceb208 languageName: node linkType: hard From a7674781a3ce46a192b9f4e307d96687bdf5d332 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 13:59:49 +0000 Subject: [PATCH 063/291] chore: suppress jest warnings --- packages/blueprints-integration/jest.config.js | 5 +++++ packages/corelib/jest.config.js | 5 +++++ packages/job-worker/jest.config.js | 1 + packages/live-status-gateway-api/eslint.config.mjs | 2 +- packages/live-status-gateway-api/jest.config.js | 5 +++++ packages/live-status-gateway/jest.config.js | 5 +++++ packages/meteor-lib/jest.config.js | 5 +++++ packages/mos-gateway/jest.config.js | 5 +++++ packages/openapi/jest.config.js | 5 +++++ packages/playout-gateway/jest.config.js | 5 +++++ packages/server-core-integration/jest.config.js | 5 +++++ packages/shared-lib/jest.config.js | 5 +++++ packages/webui/jest.config.cjs | 5 +++++ 13 files changed, 57 insertions(+), 1 deletion(-) 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/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/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/live-status-gateway-api/eslint.config.mjs b/packages/live-status-gateway-api/eslint.config.mjs index b9e5a88fd88..7b51e4e0352 100644 --- a/packages/live-status-gateway-api/eslint.config.mjs +++ b/packages/live-status-gateway-api/eslint.config.mjs @@ -1,3 +1,3 @@ import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' -export default generateEslintConfig({}) +export default generateEslintConfig({ ignores: ['server'] }) 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/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/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/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/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/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/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/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/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'] }], From 51c67f3d66640f9ff28ce6e65697916984e5c0ac Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 14:10:52 +0000 Subject: [PATCH 064/291] chore: replace sinon with native jest --- packages/webui/package.json | 4 +- .../client/utils/__tests__/dimensions.test.ts | 26 ++-- packages/yarn.lock | 137 +----------------- 3 files changed, 21 insertions(+), 146 deletions(-) diff --git a/packages/webui/package.json b/packages/webui/package.json index 72cf5f940a2..002207e3df0 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -45,7 +45,6 @@ "@sofie-automation/shared-lib": "26.3.0-0", "@sofie-automation/sorensen": "^1.5.11", "@testing-library/user-event": "^14.6.1", - "@types/sinon": "^10.0.20", "bootstrap": "^5.3.8", "classnames": "^2.5.1", "cubic-spline": "^3.0.3", @@ -59,7 +58,7 @@ "moment": "^2.30.1", "motion": "^12.31.0", "promise.allsettled": "^1.0.7", - "query-string": "^9.3.1", + "query-string": "^6.14.1", "rc-tooltip": "^6.4.0", "react": "^18.3.1", "react-bootstrap": "^2.10.10", @@ -106,7 +105,6 @@ "babel-jest": "^29.7.0", "globals": "^17.3.0", "sass-embedded": "^1.97.3", - "sinon": "^14.0.2", "typescript": "~5.7.3", "vite": "^7.3.1", "vite-plugin-node-polyfills": "^0.25.0", 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/yarn.lock b/packages/yarn.lock index d28303e8998..7f27a9a44a1 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -6924,30 +6924,12 @@ __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" + version: 3.0.1 + resolution: "@sinonjs/commons@npm:3.0.1" dependencies: type-detect: "npm:4.0.8" - checksum: 10/086720ae0bc370829322df32612205141cdd44e592a8a9ca97197571f8f970352ea39d3bda75b347c43789013ddab36b34b59e40380a49bdae1c2df3aa85fe4f + checksum: 10/a0af217ba7044426c78df52c23cedede6daf377586f3ac58857c565769358ab1f44ebf95ba04bbe38814fba6e316ca6f02870a009328294fc2c555d0f85a7117 languageName: node linkType: hard @@ -6960,42 +6942,6 @@ __metadata: 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 - languageName: node - linkType: hard - -"@sinonjs/samsam@npm:^7.0.1": - version: 7.0.1 - resolution: "@sinonjs/samsam@npm:7.0.1" - 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 - languageName: node - linkType: hard - "@slack/types@npm:^2.9.0": version: 2.14.0 resolution: "@slack/types@npm:2.14.0" @@ -7245,7 +7191,6 @@ __metadata: "@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:^5.1.3" "@welldone-software/why-did-you-render": "npm:^4.3.2" @@ -7286,7 +7231,6 @@ __metadata: semver: "npm:^7.7.3" sha.js: "npm:^2.4.12" shuttle-webhid: "npm:^0.1.3" - sinon: "npm:^14.0.2" type-fest: "npm:^4.41.0" typescript: "npm:~5.7.3" underscore: "npm:^1.13.7" @@ -8891,22 +8835,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" @@ -13634,13 +13562,6 @@ 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.3": version: 5.0.3 resolution: "diffie-hellman@npm:5.0.3" @@ -19146,13 +19067,6 @@ 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 - languageName: node - linkType: hard - "kairos-lib@npm:^0.2.3": version: 0.2.3 resolution: "kairos-lib@npm:0.2.3" @@ -19650,13 +19564,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" @@ -21763,19 +21670,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" @@ -23662,13 +23556,6 @@ asn1@evs-broadcast/node-asn1: 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-type@npm:^3.0.0": version: 3.0.0 resolution: "path-type@npm:3.0.0" @@ -27578,20 +27465,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" @@ -28458,7 +28331,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: @@ -29363,7 +29236,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 From 331b8097878367c978cd029841cc3d2b719b8cb1 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 14:30:22 +0000 Subject: [PATCH 065/291] fix: some bad promise handling --- packages/job-worker/src/ingest/__tests__/lib.ts | 2 +- packages/job-worker/src/ingest/commit.ts | 2 +- packages/job-worker/src/playout/__tests__/lib.ts | 6 +++--- .../src/playout/model/implementation/LoadPlayoutModel.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) 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/commit.ts b/packages/job-worker/src/ingest/commit.ts index 47e26f850cb..f5b7f0a9531 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -524,7 +524,7 @@ async function updatePartInstancesBasicProperties( ) } - await Promise.all([ps]) + await Promise.all(ps) } /** 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/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 }), ]) From 4b3d427d70ddb594ff98a2c01453470489026f5c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 14:31:42 +0000 Subject: [PATCH 066/291] chore: update eslint and auto-resolve issues --- .../src/api/showStyle.ts | 6 +- .../blueprints-integration/src/api/studio.ts | 6 +- .../src/context/adlibActionContext.ts | 3 +- .../src/context/onTakeContext.ts | 5 +- .../src/documents/adlibPiece.ts | 12 +- .../src/documents/part.ts | 12 +- .../src/documents/piece.ts | 12 +- .../src/documents/pieceInstance.ts | 6 +- .../src/documents/rundown.ts | 3 +- .../src/documents/rundownPiece.ts | 12 +- .../src/documents/segment.ts | 6 +- .../src/ingest-types.ts | 13 +- packages/blueprints-integration/src/ingest.ts | 7 +- .../dataModel/ExpectedPackageWorkStatuses.ts | 6 +- .../src/dataModel/NrcsIngestDataCache.ts | 7 +- packages/corelib/src/dataModel/Piece.ts | 3 +- packages/corelib/src/dataModel/Timeline.ts | 6 +- .../ingest/MutableIngestRundownImpl.ts | 8 +- .../ingest/MutableIngestSegmentImpl.ts | 7 +- packages/job-worker/src/db/collections.ts | 5 +- packages/job-worker/src/jobs/showStyle.ts | 9 +- packages/job-worker/src/jobs/studio.ts | 19 +- packages/job-worker/src/playout/infinites.ts | 8 +- packages/meteor-lib/src/collections/lib.ts | 5 +- packages/openapi/package.json | 4 +- packages/package.json | 6 +- packages/playout-gateway/src/atemUploader.ts | 2 +- packages/playout-gateway/src/coreHandler.ts | 2 +- packages/playout-gateway/src/tsrHandler.ts | 5 +- .../shared-lib/src/core/model/Timeline.ts | 11 +- .../shared-lib/src/lib/JSONSchemaTypes.ts | 3 +- packages/webui/src/client/collections/lib.ts | 6 +- .../lib/Components/BreadCrumbTextInput.tsx | 6 +- .../lib/Components/MultiLineTextInput.tsx | 6 +- .../client/lib/triggers/triggersContext.ts | 6 +- .../ui/ClipTrimPanel/ClipTrimDialog.tsx | 56 +- .../client/ui/RundownView/RundownNotifier.tsx | 46 +- packages/yarn.lock | 482 ++++++++++-------- 38 files changed, 472 insertions(+), 355 deletions(-) diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 43182638f38..dee934188cd 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -57,8 +57,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 */ diff --git a/packages/blueprints-integration/src/api/studio.ts b/packages/blueprints-integration/src/api/studio.ts index 11fbaa9d0c7..199b753b60a 100644 --- a/packages/blueprints-integration/src/api/studio.ts +++ b/packages/blueprints-integration/src/api/studio.ts @@ -37,8 +37,10 @@ import type { MosGatewayConfig } from '@sofie-automation/shared-lib/dist/generat 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 */ diff --git a/packages/blueprints-integration/src/context/adlibActionContext.ts b/packages/blueprints-integration/src/context/adlibActionContext.ts index 6f9931eeea7..d8f1a22b155 100644 --- a/packages/blueprints-integration/src/context/adlibActionContext.ts +++ b/packages/blueprints-integration/src/context/adlibActionContext.ts @@ -21,7 +21,8 @@ export interface IDataStoreMethods { export interface IDataStoreActionExecutionContext extends IDataStoreMethods, IShowStyleUserContext, IEventContext {} export interface IActionExecutionContext - extends IShowStyleUserContext, + extends + IShowStyleUserContext, IEventContext, IDataStoreMethods, IPartAndPieceActionContext, diff --git a/packages/blueprints-integration/src/context/onTakeContext.ts b/packages/blueprints-integration/src/context/onTakeContext.ts index 461f64bfa1d..68894b05e96 100644 --- a/packages/blueprints-integration/src/context/onTakeContext.ts +++ b/packages/blueprints-integration/src/context/onTakeContext.ts @@ -6,10 +6,7 @@ import { IExecuteTSRActionsContext } from './executeTsrActionContext.js' * Context in which 'current' is the partInstance we're leaving, and 'next' is the partInstance we're taking */ export interface IOnTakeContext - extends IPartAndPieceActionContext, - IShowStyleUserContext, - IEventContext, - IExecuteTSRActionsContext { + extends IPartAndPieceActionContext, IShowStyleUserContext, IEventContext, IExecuteTSRActionsContext { /** 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/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..7450f93a26b 100644 --- a/packages/blueprints-integration/src/documents/pieceInstance.ts +++ b/packages/blueprints-integration/src/documents/pieceInstance.ts @@ -31,8 +31,10 @@ 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/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..3222b456396 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 } 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/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/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/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/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/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/openapi/package.json b/packages/openapi/package.json index 2a001602ea0..f9cdc53100a 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -40,8 +40,8 @@ }, "devDependencies": { "@openapitools/openapi-generator-cli": "^2.28.0", - "eslint": "^9.18.0", - "eslint-plugin-yml": "^1.16.0", + "eslint": "^9.39.2", + "eslint-plugin-yml": "^1.19.1", "js-yaml": "^4.1.1", "wget-improved": "^3.4.0" }, diff --git a/packages/package.json b/packages/package.json index 1ef488e2538..d3151f2e62c 100644 --- a/packages/package.json +++ b/packages/package.json @@ -43,7 +43,7 @@ "devDependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-modules-commonjs": "^7.28.6", - "@sofie-automation/code-standard-preset": "^3.0.0", + "@sofie-automation/code-standard-preset": "^3.2.1", "@types/amqplib": "0.10.6", "@types/debug": "^4.1.12", "@types/ejson": "^2.2.2", @@ -54,8 +54,8 @@ "@types/underscore": "^1.13.0", "babel-jest": "^29.7.0", "copyfiles": "^2.4.1", - "eslint": "^9.18.0", - "eslint-plugin-react": "^7.37.4", + "eslint": "^9.39.2", + "eslint-plugin-react": "^7.37.5", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-mock-extended": "^3.0.7", 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/shared-lib/src/core/model/Timeline.ts b/packages/shared-lib/src/core/model/Timeline.ts index 3f2a70c04ff..2a5c72247d2 100644 --- a/packages/shared-lib/src/core/model/Timeline.ts +++ b/packages/shared-lib/src/core/model/Timeline.ts @@ -133,8 +133,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 +147,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/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/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/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/RundownView/RundownNotifier.tsx b/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx index ba7328a21e3..c876d3d082a 100644 --- a/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx @@ -535,17 +535,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 +611,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/yarn.lock b/packages/yarn.lock index 7f27a9a44a1..f11273e132e 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -3674,7 +3674,7 @@ __metadata: 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: @@ -3685,10 +3685,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 @@ -6162,10 +6162,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 @@ -6991,21 +6991,22 @@ __metadata: 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 @@ -7013,7 +7014,7 @@ __metadata: bin: sofie-licensecheck: ./bin/checkLicenses.mjs sofie-version: ./bin/updateVersion.mjs - checksum: 10/fa61dc1f90377ad2196f2e6c33dea9988bbe9cfd6eb8b277a083ae1147c00e83e526b7520bb5548d4935fb91b7f9f1d8f9b701db419da760488c318ea42a243f + checksum: 10/db31f2aa51b504c86b8a4e4cca9cd6a8a80769d4ed020f84ec2780abb0735a14e465693f8de907fbc9e668e07e04141a95c14caeea0e61916546a576671f34aa languageName: node linkType: hard @@ -7038,14 +7039,15 @@ __metadata: 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 @@ -7115,8 +7117,8 @@ __metadata: resolution: "@sofie-automation/openapi@workspace:openapi" dependencies: "@openapitools/openapi-generator-cli": "npm:^2.28.0" - eslint: "npm:^9.18.0" - eslint-plugin-yml: "npm:^1.16.0" + eslint: "npm:^9.39.2" + eslint-plugin-yml: "npm:^1.19.1" js-yaml: "npm:^4.1.1" tslib: "npm:^2.8.1" wget-improved: "npm:^3.4.0" @@ -8973,115 +8975,138 @@ __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 @@ -9115,6 +9140,25 @@ __metadata: 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: + eslint: ">=8.57.0" + typescript: ">=5.0.0" + vitest: "*" + peerDependenciesMeta: + typescript: + optional: true + vitest: + optional: true + checksum: 10/d08d90480547435a3a2324f3138e37f65a78e4fa3b1e9a5030d5b0b2f1ee09547e7631084445586c67e1d465191116e0ef5623f9b1d8d571a3c74099fba21c55 + languageName: node + linkType: hard + "@webassemblyjs/ast@npm:1.14.1, @webassemblyjs/ast@npm:^1.14.1": version: 1.14.1 resolution: "@webassemblyjs/ast@npm:1.14.1" @@ -10041,6 +10085,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" @@ -11046,13 +11104,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 @@ -11068,13 +11126,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 @@ -13548,6 +13606,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"diff-sequences@npm:^27.5.1": + version: 27.5.1 + resolution: "diff-sequences@npm:27.5.1" + checksum: 10/34d852a13eb82735c39944a050613f952038614ce324256e1c3544948fa090f1ca7f329a4f1f57c31fe7ac982c17068d8915b633e300f040b97708c81ceb26cd + languageName: node + linkType: hard + "diff-sequences@npm:^29.6.3": version: 29.6.3 resolution: "diff-sequences@npm:29.6.3" @@ -14255,12 +14320,12 @@ asn1@evs-broadcast/node-asn1: 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 @@ -14406,6 +14471,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"escape-string-regexp@npm:4.0.0, escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 10/98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 + 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" @@ -14420,13 +14492,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"escape-string-regexp@npm:^4.0.0": - version: 4.0.0 - resolution: "escape-string-regexp@npm:4.0.0" - checksum: 10/98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 - languageName: node - linkType: hard - "escape-string-regexp@npm:^5.0.0": version: 5.0.0 resolution: "escape-string-regexp@npm:5.0.0" @@ -14474,14 +14539,14 @@ asn1@evs-broadcast/node-asn1: 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 @@ -14498,9 +14563,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: @@ -14512,51 +14577,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" @@ -14568,7 +14634,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" @@ -14578,22 +14644,23 @@ 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:^1.19.1": + version: 1.19.1 + resolution: "eslint-plugin-yml@npm:1.19.1" dependencies: debug: "npm:^4.3.2" + diff-sequences: "npm:^27.5.1" + escape-string-regexp: "npm:4.0.0" eslint-compat-utils: "npm:^0.6.0" - lodash: "npm:^4.17.21" natural-compare: "npm:^1.4.0" yaml-eslint-parser: "npm:^1.2.1" peerDependencies: eslint: ">=6.0.0" - checksum: 10/523f3016098f2de340a68c1fa11228734b281191d3391b1ab8812c1c446455841f35815b4c4f036c2ab459a5f7b0c6496dd5bc57de912632d8db9da44e45eb44 + checksum: 10/47c31c7c87a0ae601666aa18ab48c373fe79f039da00d8fa42b5f7298338ada45bfba7e07a2a59b612473e97146b34f39e06a84d48d425e020b7be1c9c7ba871 languageName: node linkType: hard @@ -14624,14 +14691,14 @@ asn1@evs-broadcast/node-asn1: 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@npm:^9.39.2": version: 9.39.2 resolution: "eslint@npm:9.39.2" dependencies: @@ -15090,7 +15157,7 @@ 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-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: @@ -15717,6 +15784,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" @@ -15731,21 +15805,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 @@ -16175,13 +16252,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" @@ -17050,7 +17120,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 @@ -22517,14 +22587,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 @@ -23082,7 +23153,7 @@ asn1@evs-broadcast/node-asn1: dependencies: "@babel/core": "npm:^7.29.0" "@babel/plugin-transform-modules-commonjs": "npm:^7.28.6" - "@sofie-automation/code-standard-preset": "npm:^3.0.0" + "@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" @@ -23093,8 +23164,8 @@ asn1@evs-broadcast/node-asn1: "@types/underscore": "npm:^1.13.0" babel-jest: "npm:^29.7.0" copyfiles: "npm:^2.4.1" - eslint: "npm:^9.18.0" - eslint-plugin-react: "npm:^7.37.4" + eslint: "npm:^9.39.2" + eslint-plugin-react: "npm:^7.37.5" jest: "npm:^29.7.0" jest-environment-jsdom: "npm:^29.7.0" jest-mock-extended: "npm:^3.0.7" @@ -24656,12 +24727,12 @@ 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 @@ -27090,7 +27161,7 @@ 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, 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.3 resolution: "semver@npm:7.7.3" bin: @@ -28415,13 +28486,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"synckit@npm:^0.9.1": - version: 0.9.2 - resolution: "synckit@npm:0.9.2" +"synckit@npm:^0.11.12": + 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 @@ -29032,12 +29102,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 @@ -29420,17 +29501,18 @@ asn1@evs-broadcast/node-asn1: 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 From 0b840647c0d59888f4a1de1a2bdb3c49b2153311 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 14:33:39 +0000 Subject: [PATCH 067/291] chore: safe dependency updates --- meteor/package.json | 53 ++-- meteor/yarn.lock | 731 +++++++++++++++++++------------------------- 2 files changed, 346 insertions(+), 438 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index ed4557165c9..88d9959784d 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -35,11 +35,11 @@ "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": "^13.1.1", "@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", @@ -47,62 +47,61 @@ "@sofie-automation/shared-lib": "portal:../packages/shared-lib", "app-root-path": "^3.1.0", "bcrypt": "^6.0.0", - "body-parser": "^1.20.3", + "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": "^2.16.3", "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", + "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/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/koa__router": "^12.0.5", + "@types/node": "^22.19.8", + "@types/semver": "^7.7.1", "@types/underscore": "^1.13.0", "babel-jest": "^29.7.0", "ejson": "^2.2.3", - "eslint": "^9.18.0", + "eslint": "^9.39.2", "fast-clone": "^1.5.13", - "glob": "^11.0.1", + "glob": "^11.1.0", "i18next-conv": "^10.2.0", "i18next-scanner": "^4.6.0", "jest": "^29.7.0", "legally": "^3.5.10", "open-cli": "^8.0.0", - "prettier": "^3.4.2", + "prettier": "^3.8.1", "standard-version": "^9.5.0", - "ts-jest": "^29.2.5", + "ts-jest": "^29.4.6", "typescript": "~5.7.3", "yargs": "^17.7.2" }, diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 50158287ad3..b8df6a83271 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -23,110 +23,107 @@ __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 - -"@babel/code-frame@npm:^7.0.0, @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" +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @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.26.5": - version: 7.26.5 - resolution: "@babel/compat-data@npm:7.26.5" - checksum: 10/afe35751f27bda80390fa221d5e37be55b7fc42cec80de9896086e20394f2306936c4296fcb4d62b683e3b49ba2934661ea7e06196ca2dacdc2e779fbea4a1a9 +"@babel/compat-data@npm:^7.28.6": + version: 7.29.0 + resolution: "@babel/compat-data@npm:7.29.0" + checksum: 10/7f21beedb930ed8fbf7eabafc60e6e6521c1d905646bf1317a61b2163339157fe797efeb85962bf55136e166b01fd1a6b526a15974b92a8b877d564dcb6c9580 languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/core@npm:7.26.7" +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/core@npm:7.29.0" 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/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.26.5, @babel/generator@npm:^7.7.2": - version: 7.26.5 - resolution: "@babel/generator@npm:7.26.5" +"@babel/generator@npm:^7.29.0, @babel/generator@npm:^7.7.2": + 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-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.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-module-imports@npm:^7.25.9": - version: 7.25.9 - resolution: "@babel/helper-module-imports@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-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.26.0": - version: 7.26.0 - resolution: "@babel/helper-module-transforms@npm:7.26.0" +"@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-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.22.5, @babel/helper-plugin-utils@npm:^7.25.9, @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.22.5, @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 @@ -137,21 +134,21 @@ __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.27.1": + version: 7.27.1 + resolution: "@babel/helper-validator-option@npm:7.27.1" + checksum: 10/db73e6a308092531c629ee5de7f0d04390835b21a263be2644276cb27da2384b64676cab9f22cd8d8dbd854c92b1d7d56fc8517cf0070c35d1c14a8c828b0903 languageName: node linkType: hard -"@babel/helpers@npm:^7.26.7": +"@babel/helpers@npm:^7.28.6": version: 7.28.6 resolution: "@babel/helpers@npm:7.28.6" dependencies: @@ -161,14 +158,14 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @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.14.7, @babel/parser@npm:^7.20.7, @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 @@ -326,26 +323,26 @@ __metadata: languageName: node linkType: hard -"@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.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/runtime@npm:^7.17.2, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.26.7": +"@babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.22.5, @babel/runtime@npm:^7.28.6": version: 7.28.6 resolution: "@babel/runtime@npm:7.28.6" checksum: 10/fbcd439cb74d4a681958eb064c509829e3f46d8a4bfaaf441baa81bb6733d1e680bccc676c813883d7741bcaada1d0d04b15aa320ef280b5734e2192b50decf9 languageName: node linkType: hard -"@babel/template@npm:^7.25.9, @babel/template@npm:^7.28.6, @babel/template@npm:^7.3.3": +"@babel/template@npm:^7.28.6, @babel/template@npm:^7.3.3": version: 7.28.6 resolution: "@babel/template@npm:7.28.6" dependencies: @@ -356,28 +353,28 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.7": - version: 7.26.7 - resolution: "@babel/traverse@npm:7.26.7" +"@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.20.7, @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": - version: 7.28.6 - resolution: "@babel/types@npm:7.28.6" +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.3.3": + 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 @@ -395,14 +392,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 @@ -853,14 +850,23 @@ __metadata: languageName: node linkType: hard -"@jridgewell/gen-mapping@npm:^0.3.0, @jridgewell/gen-mapping@npm:^0.3.5": - version: 0.3.8 - resolution: "@jridgewell/gen-mapping@npm:0.3.8" +"@jridgewell/gen-mapping@npm:^0.3.12, @jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.13 + resolution: "@jridgewell/gen-mapping@npm:0.3.13" dependencies: - "@jridgewell/set-array": "npm:^1.2.1" - "@jridgewell/sourcemap-codec": "npm:^1.4.10" + "@jridgewell/sourcemap-codec": "npm:^1.5.0" "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10/9d3a56ab3612ab9b85d38b2a93b87f3324f11c5130859957f6500e4ac8ce35f299d5ccc3ecd1ae87597601ecf83cee29e9afd04c18777c24011073992ff946df + 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 @@ -871,27 +877,20 @@ __metadata: languageName: node linkType: hard -"@jridgewell/set-array@npm:^1.2.1": - version: 1.2.1 - resolution: "@jridgewell/set-array@npm:1.2.1" - checksum: 10/832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 - languageName: node - linkType: hard - -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": - version: 1.4.15 - resolution: "@jridgewell/sourcemap-codec@npm:1.4.15" - checksum: 10/89960ac087781b961ad918978975bcdf2051cd1741880469783c42de64239703eab9db5230d776d8e6a09d73bb5e4cb964e07d93ee6e2e7aea5a7d726e865c09 +"@jridgewell/sourcemap-codec@npm:^1.4.14, @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 -"@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.18, @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.24, @jridgewell/trace-mapping@npm:^0.3.28": + 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 @@ -904,7 +903,7 @@ __metadata: languageName: node linkType: hard -"@koa/router@npm:^13.1.0": +"@koa/router@npm:^13.1.1": version: 13.1.1 resolution: "@koa/router@npm:13.1.1" dependencies: @@ -1113,14 +1112,24 @@ __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 + +"@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 @@ -1130,7 +1139,7 @@ __metadata: dependencies: "@sofie-automation/shared-lib": "npm:26.3.0-0" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" languageName: node linkType: soft @@ -1168,13 +1177,13 @@ __metadata: "@sofie-automation/shared-lib": "npm:26.3.0-0" 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 @@ -1196,20 +1205,20 @@ __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" + "@slack/webhook": "npm:^7.0.6" "@sofie-automation/blueprints-integration": "npm:26.3.0-0" "@sofie-automation/corelib": "npm:26.3.0-0" "@sofie-automation/shared-lib": "npm:26.3.0-0" - amqplib: "npm:^0.10.5" + amqplib: "npm:0.10.5" deepmerge: "npm:^4.3.1" - elastic-apm-node: "npm:^4.11.0" + elastic-apm-node: "npm:^4.15.0" mongodb: "npm:^6.12.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 @@ -1223,8 +1232,8 @@ __metadata: "@sofie-automation/corelib": "npm:26.3.0-0" "@sofie-automation/shared-lib": "npm:26.3.0-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 @@ -1240,7 +1249,7 @@ __metadata: kairos-lib: "npm:^0.2.3" timeline-state-resolver-types: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" tslib: "npm:^2.8.1" - type-fest: "npm:^4.33.0" + type-fest: "npm:^4.41.0" languageName: node linkType: soft @@ -1308,20 +1317,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 @@ -1463,12 +1465,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 @@ -1481,7 +1483,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: @@ -1525,21 +1527,21 @@ __metadata: languageName: node linkType: hard -"@types/koa__cors@npm:^5.0.0": - version: 5.0.0 - resolution: "@types/koa__cors@npm:5.0.0" +"@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/ad8e6a482f1bb0e357e0051faec328a75e2978a24065a953032d5dba58ac08edf5ca66b03059551f0faf9e085b15ee7892e6ab03c9500af4be8bd258965479c9 + checksum: 10/552db24607ce394130c2ae0e1bd443237f0174702feaf81fe45afc430ca467c6760084a1a93e096be3a2a18160220b37bc6ae2cf48ed0e198d273f557ee1bf64 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 @@ -1564,12 +1566,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 @@ -1594,22 +1596,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 @@ -1641,13 +1631,6 @@ __metadata: languageName: node linkType: hard -"@types/tough-cookie@npm:*": - version: 4.0.3 - resolution: "@types/tough-cookie@npm:4.0.3" - checksum: 10/32d17b50766357b0297762d4ee0e42b430f36f0397eec38559b9ce18120f64f07922cca3ecc6bdb5a097ba75ec71e91879bdb89a7c532de43b5eff6775625334 - languageName: node - linkType: hard - "@types/triple-beam@npm:^1.3.2": version: 1.3.3 resolution: "@types/triple-beam@npm:1.3.3" @@ -1931,7 +1914,7 @@ __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, acorn@npm:^8.15.0": version: 8.15.0 resolution: "acorn@npm:8.15.0" bin: @@ -1982,7 +1965,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: @@ -2220,14 +2203,14 @@ __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:^13.1.1" "@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" @@ -2235,58 +2218,57 @@ __metadata: "@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/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/koa__router": "npm:^12.0.5" + "@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:^6.0.0" - body-parser: "npm:^1.20.3" + body-parser: "npm:^1.20.4" 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:^11.1.0" 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" + koa: "npm:^2.16.3" 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" + 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" + prettier: "npm:^3.8.1" + semver: "npm:^7.7.3" standard-version: "npm:^9.5.0" 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 @@ -2300,14 +2282,14 @@ __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.4 + resolution: "axios@npm:1.13.4" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.4" proxy-from-env: "npm:^1.1.0" - checksum: 10/2ceca9215671f9c2bcd5d8a0a1a667e9a35f9f7cfae88f25bba773ed9612de6cac50b2bf8be5e6918cbd2db601b4431ca87a00bffd9682939a8b85da9c89345a + checksum: 10/54b7ef71c64837f9d52475832337f520cf6fa85c94612e03a3a2aad7082804a2544741267122696662147e90e6d2746601346984cf531ae715ecdb56d586a04c languageName: node linkType: hard @@ -2467,7 +2449,7 @@ __metadata: 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: @@ -2820,7 +2802,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": version: 4.1.2 resolution: "chalk@npm:4.1.2" dependencies: @@ -2942,7 +2924,7 @@ __metadata: 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: @@ -2960,6 +2942,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" @@ -2967,40 +2958,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 +"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-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:~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 @@ -3445,7 +3432,7 @@ __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.4, debug@npm:^4.4.1": +"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.1": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -3714,17 +3701,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" @@ -3732,9 +3708,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" @@ -3754,7 +3730,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" @@ -3766,14 +3742,14 @@ __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 + checksum: 10/6207a28ee1ab4b1d0459e2f545745377d6108a7d32587c51607f1f46bb6e08d051d67c73978ba2b089026bf22e6d1abcf44f8558a10013e6629577276688b199 languageName: node linkType: hard @@ -3784,7 +3760,7 @@ __metadata: languageName: node linkType: hard -"elliptic@npm:^6.5.3, elliptic@npm:^6.5.5, elliptic@npm:^6.6.0": +"elliptic@npm:^6.5.3, elliptic@npm:^6.5.5": version: 6.6.1 resolution: "elliptic@npm:6.6.1" dependencies: @@ -4158,7 +4134,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: @@ -4490,15 +4466,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" @@ -4606,20 +4573,6 @@ __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": version: 4.0.5 resolution: "form-data@npm:4.0.5" @@ -4916,7 +4869,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^11.0.1": +"glob@npm:^11.1.0": version: 11.1.0 resolution: "glob@npm:11.1.0" dependencies: @@ -4957,13 +4910,6 @@ __metadata: 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" @@ -5017,7 +4963,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: @@ -5409,15 +5355,15 @@ __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 @@ -5478,10 +5424,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 @@ -5552,13 +5498,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" @@ -5951,20 +5890,6 @@ __metadata: 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" @@ -6329,7 +6254,7 @@ __metadata: languageName: node linkType: hard -"jest-util@npm:^29.0.0, jest-util@npm:^29.7.0": +"jest-util@npm:^29.7.0": version: 29.7.0 resolution: "jest-util@npm:29.7.0" dependencies: @@ -6579,13 +6504,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 @@ -6610,7 +6535,7 @@ __metadata: languageName: node linkType: hard -"koa@npm:^2.13.4, koa@npm:^2.15.3": +"koa@npm:^2.13.4, koa@npm:^2.16.3": version: 2.16.3 resolution: "koa@npm:2.16.3" dependencies: @@ -7026,9 +6951,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" @@ -7037,7 +6962,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" @@ -7046,6 +6970,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" @@ -7054,7 +6979,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 @@ -7094,7 +7019,7 @@ __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-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: @@ -7158,15 +7083,6 @@ __metadata: 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 - languageName: node - linkType: hard - "minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": version: 9.0.5 resolution: "minimatch@npm:9.0.5" @@ -7376,12 +7292,12 @@ __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 @@ -8191,12 +8107,12 @@ __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 @@ -8628,14 +8544,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 @@ -8702,7 +8617,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.10.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1": +"resolve@npm:^1.10.0, resolve@npm:^1.20.0": version: 1.22.6 resolution: "resolve@npm:1.22.6" dependencies: @@ -8715,7 +8630,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, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin": version: 1.22.6 resolution: "resolve@patch:resolve@npm%3A1.22.6#optional!builtin::version=1.22.6&hash=c3c19d" dependencies: @@ -8844,12 +8759,12 @@ __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.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, 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 @@ -9006,15 +8921,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" @@ -9636,15 +9542,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 @@ -9810,25 +9716,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": @@ -9841,9 +9748,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 @@ -9933,10 +9842,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 @@ -10066,10 +9975,10 @@ __metadata: 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 @@ -10407,12 +10316,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" @@ -10422,7 +10331,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 From 3b75af983c004ceeb1310ff0f0c31f5697a8749b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 14:37:32 +0000 Subject: [PATCH 068/291] chore: update koa --- meteor/package.json | 10 +- meteor/server/api/blueprints/http.ts | 2 +- meteor/server/api/rest/koa.ts | 6 +- meteor/server/api/rest/v1/index.ts | 8 +- meteor/yarn.lock | 288 +++++++++++---------------- 5 files changed, 128 insertions(+), 186 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 88d9959784d..38eb7c34069 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -37,7 +37,7 @@ "dependencies": { "@babel/runtime": "^7.28.6", "@koa/cors": "^5.0.0", - "@koa/router": "^13.1.1", + "@koa/router": "^15.3.0", "@mos-connection/helper": "^5.0.0-alpha.0", "@slack/webhook": "^7.0.6", "@sofie-automation/blueprints-integration": "portal:../packages/blueprints-integration", @@ -53,7 +53,7 @@ "elastic-apm-node": "^4.15.0", "i18next": "^21.10.0", "indexof": "0.0.1", - "koa": "^2.16.3", + "koa": "^3.1.1", "koa-bodyparser": "^4.4.1", "koa-mount": "^4.2.0", "koa-static": "^5.0.0", @@ -76,11 +76,11 @@ "@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/app-root-path": "^3.1.0", "@types/body-parser": "^1.19.6", "@types/deep-extend": "^0.6.2", "@types/jest": "^29.5.14", - "@types/koa": "^2.15.0", + "@types/koa": "^3.0.1", "@types/koa-bodyparser": "^4.3.13", "@types/koa-mount": "^4.0.5", "@types/koa-static": "^4.0.4", @@ -93,7 +93,7 @@ "ejson": "^2.2.3", "eslint": "^9.39.2", "fast-clone": "^1.5.13", - "glob": "^11.1.0", + "glob": "^13.0.1", "i18next-conv": "^10.2.0", "i18next-scanner": "^4.6.0", "jest": "^29.7.0", 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/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..0c113879a70 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -23,13 +23,7 @@ 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}` } diff --git a/meteor/yarn.lock b/meteor/yarn.lock index b8df6a83271..e98c63fcbbd 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -577,20 +577,6 @@ __metadata: languageName: node linkType: hard -"@isaacs/cliui@npm:^8.0.2": - version: 8.0.2 - resolution: "@isaacs/cliui@npm:8.0.2" - dependencies: - string-width: "npm:^5.1.2" - string-width-cjs: "npm:string-width@^4.2.0" - strip-ansi: "npm:^7.0.1" - strip-ansi-cjs: "npm:strip-ansi@^6.0.1" - wrap-ansi: "npm:^8.1.0" - wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" - checksum: 10/e9ed5fd27c3aec1095e3a16e0c0cf148d1fee55a38665c35f7b3f86a9b5d00d042ddaabc98e8a1cb7463b9378c15f22a94eb35e99469c201453eb8375191f243 - languageName: node - linkType: hard - "@isaacs/fs-minipass@npm:^4.0.0": version: 4.0.1 resolution: "@isaacs/fs-minipass@npm:4.0.1" @@ -903,15 +889,20 @@ __metadata: languageName: node linkType: hard -"@koa/router@npm:^13.1.1": - version: 13.1.1 - resolution: "@koa/router@npm:13.1.1" +"@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:^6.3.0" - checksum: 10/960a573524d2c315994cdd59e94bca178ccd75931ac804b82c0657830911f8e1c923d17962cc2aac252ce842000c7a3204d629146852a30c8af02e70d5ade8e5 + 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 @@ -1269,10 +1260,12 @@ __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 +"@types/app-root-path@npm:^3.1.0": + version: 3.1.0 + resolution: "@types/app-root-path@npm:3.1.0" + dependencies: + app-root-path: "npm:*" + checksum: 10/e62ce16359e91a708d8acefa2a05dd5e6c723dd7789c8914e884b13a032e0e84a5c92bf826728fa63bbb9d96b3647c2ef40fced36f2d8a8210b44a1f6f30dd29 languageName: node linkType: hard @@ -1409,10 +1402,10 @@ __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 @@ -1511,19 +1504,19 @@ __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 @@ -1824,7 +1817,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: @@ -1992,13 +1985,6 @@ __metadata: languageName: node linkType: hard -"ansi-regex@npm:^6.0.1": - version: 6.0.1 - resolution: "ansi-regex@npm:6.0.1" - checksum: 10/1ff8b7667cded1de4fa2c9ae283e979fc87036864317da86a2e546725f96406746411d0d85e87a2d12fa5abd715d90006de7fa4fa0477c92321ad3b4c7d4e169 - languageName: node - linkType: hard - "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -2024,13 +2010,6 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.1.0": - version: 6.2.1 - resolution: "ansi-styles@npm:6.2.1" - checksum: 10/70fdf883b704d17a5dfc9cde206e698c16bcd74e7f196ab821511651aee4f9f76c9514bdfa6ca3a27b5e49138b89cb222a28caf3afe4567570139577f991df32 - languageName: node - linkType: hard - "anymatch@npm:^3.0.3, anymatch@npm:^3.1.3": version: 3.1.3 resolution: "anymatch@npm:3.1.3" @@ -2041,7 +2020,7 @@ __metadata: languageName: node linkType: hard -"app-root-path@npm:^3.1.0": +"app-root-path@npm:*, app-root-path@npm:^3.1.0": version: 3.1.0 resolution: "app-root-path@npm:3.1.0" checksum: 10/b4cdab5f7e51ec43fa04c97eca2adedf8e18d6c3dd21cd775b70457c5e71f0441c692a49dcceb426f192640b7393dcd41d85c36ef98ecb7c785a53159c912def @@ -2207,7 +2186,7 @@ __metadata: "@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.1" + "@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.6" @@ -2217,11 +2196,11 @@ __metadata: "@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/app-root-path": "npm:^3.1.0" "@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": "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" @@ -2240,13 +2219,13 @@ __metadata: elastic-apm-node: "npm:^4.15.0" eslint: "npm:^9.39.2" fast-clone: "npm:^1.5.13" - glob: "npm:^11.1.0" + 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.16.3" + koa: "npm:^3.1.1" koa-bodyparser: "npm:^4.4.1" koa-mount: "npm:^4.2.0" koa-static: "npm:^5.0.0" @@ -3064,7 +3043,7 @@ __metadata: languageName: node linkType: hard -"content-disposition@npm:^0.5.3, content-disposition@npm:~0.5.2": +"content-disposition@npm:^0.5.3, content-disposition@npm:~0.5.2, content-disposition@npm:~0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" dependencies: @@ -3073,7 +3052,7 @@ __metadata: languageName: node linkType: hard -"content-type@npm:^1.0.4, content-type@npm:~1.0.5": +"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 @@ -3304,7 +3283,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: @@ -3432,7 +3411,7 @@ __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.4, debug@npm:^4.3.5, debug@npm:^4.4.1": +"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: @@ -3601,7 +3580,7 @@ __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 @@ -3687,13 +3666,6 @@ __metadata: languageName: node linkType: hard -"eastasianwidth@npm:^0.2.0": - version: 0.2.0 - resolution: "eastasianwidth@npm:0.2.0" - checksum: 10/9b1d3e1baefeaf7d70799db8774149cef33b97183a6addceeba0cf6b85ba23ee2686f302f14482006df32df75d32b17c509c143a3689627929e4a8efaf483952 - languageName: node - linkType: hard - "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -3789,13 +3761,6 @@ __metadata: languageName: node linkType: hard -"emoji-regex@npm:^9.2.2": - version: 9.2.2 - resolution: "emoji-regex@npm:9.2.2" - checksum: 10/915acf859cea7131dac1b2b5c9c8e35c4849e325a1d114c30adb8cd615970f6dca0e27f64f3a4949d7d6ed86ecd79a1c5c63f02e697513cddd7b5835c90948b8 - languageName: node - linkType: hard - "enabled@npm:2.0.x": version: 2.0.0 resolution: "enabled@npm:2.0.0" @@ -3810,6 +3775,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" @@ -4563,16 +4535,6 @@ __metadata: languageName: node linkType: hard -"foreground-child@npm:^3.3.1": - version: 3.3.1 - resolution: "foreground-child@npm:3.3.1" - dependencies: - cross-spawn: "npm:^7.0.6" - signal-exit: "npm:^4.0.1" - checksum: 10/427b33f997a98073c0424e5c07169264a62cda806d8d2ded159b5b903fdfc8f0a1457e06b5fc35506497acb3f1e353f025edee796300209ac6231e80edece835 - languageName: node - linkType: hard - "form-data@npm:^4.0.4": version: 4.0.5 resolution: "form-data@npm:4.0.5" @@ -4869,23 +4831,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^11.1.0": - version: 11.1.0 - resolution: "glob@npm:11.1.0" - dependencies: - foreground-child: "npm:^3.3.1" - jackspeak: "npm:^4.1.1" - minimatch: "npm:^10.1.1" - 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 - languageName: node - linkType: hard - -"glob@npm:^13.0.0": +"glob@npm:^13.0.0, glob@npm:^13.0.1": version: 13.0.1 resolution: "glob@npm:13.0.1" dependencies: @@ -5134,7 +5080,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: @@ -5164,7 +5110,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: @@ -5881,15 +5827,6 @@ __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 - "jest-changed-files@npm:^29.7.0": version: 29.7.0 resolution: "jest-changed-files@npm:29.7.0" @@ -6535,7 +6472,7 @@ __metadata: languageName: node linkType: hard -"koa@npm:^2.13.4, koa@npm:^2.16.3": +"koa@npm:^2.13.4": version: 2.16.3 resolution: "koa@npm:2.16.3" dependencies: @@ -6566,6 +6503,32 @@ __metadata: languageName: node linkType: hard +"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" + 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/b9f53e98752e73d2d3ed2df28a8062387e116d7053f3d655815ea7f1bae672f4f6afe41d6ff5f6cf429aad76aea4fd4424655bdcf6f8291a658108fe3aa2cf43 + languageName: node + linkType: hard + "kuler@npm:^2.0.0": version: 2.0.0 resolution: "kuler@npm:2.0.0" @@ -6890,6 +6853,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" @@ -7019,6 +6989,13 @@ __metadata: languageName: node linkType: hard +"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" @@ -7028,6 +7005,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" @@ -7065,7 +7051,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^10.1.1, minimatch@npm:^10.1.2": +"minimatch@npm:^10.1.2": version: 10.1.2 resolution: "minimatch@npm:10.1.2" dependencies: @@ -7593,7 +7579,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: @@ -7822,13 +7808,6 @@ __metadata: languageName: node linkType: hard -"package-json-from-dist@npm:^1.0.0": - version: 1.0.1 - resolution: "package-json-from-dist@npm:1.0.1" - checksum: 10/58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602 - languageName: node - linkType: hard - "pako@npm:~1.0.5": version: 1.0.11 resolution: "pako@npm:1.0.11" @@ -7958,10 +7937,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 @@ -8914,13 +8893,6 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": - version: 4.1.0 - resolution: "signal-exit@npm:4.1.0" - checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f - languageName: node - linkType: hard - "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -9186,7 +9158,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 @@ -9253,7 +9225,7 @@ __metadata: languageName: node linkType: hard -"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": +"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: @@ -9264,17 +9236,6 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^5.0.1, string-width@npm:^5.1.2": - version: 5.1.2 - resolution: "string-width@npm:5.1.2" - dependencies: - eastasianwidth: "npm:^0.2.0" - emoji-regex: "npm:^9.2.2" - strip-ansi: "npm:^7.0.1" - checksum: 10/7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 - languageName: node - linkType: hard - "string.prototype.trim@npm:^1.2.8": version: 1.2.8 resolution: "string.prototype.trim@npm:1.2.8" @@ -9333,7 +9294,7 @@ __metadata: 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": +"strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" dependencies: @@ -9342,15 +9303,6 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1": - version: 7.1.0 - resolution: "strip-ansi@npm:7.1.0" - dependencies: - ansi-regex: "npm:^6.0.1" - checksum: 10/475f53e9c44375d6e72807284024ac5d668ee1d06010740dec0b9744f2ddf47de8d7151f80e5f6190fc8f384e802fdf9504b76a7e9020c9faee7103623338be2 - languageName: node - linkType: hard - "strip-bom@npm:^3.0.0": version: 3.0.0 resolution: "strip-bom@npm:3.0.0" @@ -9859,6 +9811,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" @@ -10342,7 +10305,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": +"wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -10353,17 +10316,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^8.1.0": - version: 8.1.0 - resolution: "wrap-ansi@npm:8.1.0" - dependencies: - ansi-styles: "npm:^6.1.0" - string-width: "npm:^5.0.1" - strip-ansi: "npm:^7.0.1" - checksum: 10/7b1e4b35e9bb2312d2ee9ee7dc95b8cb5f8b4b5a89f7dde5543fe66c1e3715663094defa50d75454ac900bd210f702d575f15f3f17fa9ec0291806d2578d1ddf - languageName: node - linkType: hard - "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" From 0ae7f2323aa166c0068add74f42cd9058a4049f9 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 14:40:53 +0000 Subject: [PATCH 069/291] chore: update jest --- meteor/package.json | 6 +- meteor/scripts/babel-jest.js | 2 +- meteor/yarn.lock | 1850 ++++++++++------ packages/job-worker/package.json | 4 +- packages/job-worker/scripts/babel-jest.mjs | 2 +- .../__snapshots__/mosIngest.test.ts.snap | 2 +- .../__snapshots__/playout.test.ts.snap | 2 +- .../__snapshots__/timeline.test.ts.snap | 2 +- .../__snapshots__/lookahead.test.ts.snap | 2 +- .../__snapshots__/rundown.test.ts.snap | 2 +- packages/package.json | 10 +- packages/webui/package.json | 2 +- .../__snapshots__/rundown.test.ts.snap | 2 +- packages/yarn.lock | 1906 +++++++++-------- 14 files changed, 2164 insertions(+), 1630 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 38eb7c34069..5381765cd26 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -79,7 +79,7 @@ "@types/app-root-path": "^3.1.0", "@types/body-parser": "^1.19.6", "@types/deep-extend": "^0.6.2", - "@types/jest": "^29.5.14", + "@types/jest": "^30.0.0", "@types/koa": "^3.0.1", "@types/koa-bodyparser": "^4.3.13", "@types/koa-mount": "^4.0.5", @@ -89,14 +89,14 @@ "@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", "ejson": "^2.2.3", "eslint": "^9.39.2", "fast-clone": "^1.5.13", "glob": "^13.0.1", "i18next-conv": "^10.2.0", "i18next-scanner": "^4.6.0", - "jest": "^29.7.0", + "jest": "^30.2.0", "legally": "^3.5.10", "open-cli": "^8.0.0", "prettier": "^3.8.1", 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/yarn.lock b/meteor/yarn.lock index e98c63fcbbd..e83059ae4e5 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -23,7 +23,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.28.6, @babel/code-frame@npm:^7.29.0": +"@babel/code-frame@npm:^7.0.0, @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: @@ -41,7 +41,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.29.0": +"@babel/core@npm:^7.23.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: @@ -64,7 +64,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.29.0, @babel/generator@npm:^7.7.2": +"@babel/generator@npm:^7.27.5, @babel/generator@npm:^7.29.0": version: 7.29.0 resolution: "@babel/generator@npm:7.29.0" dependencies: @@ -120,7 +120,7 @@ __metadata: 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.22.5, @babel/helper-plugin-utils@npm:^7.28.6, @babel/helper-plugin-utils@npm:^7.8.0": +"@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.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 @@ -158,7 +158,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.28.6, @babel/parser@npm:^7.29.0": +"@babel/parser@npm:^7.1.0, @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: @@ -191,7 +191,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: @@ -202,7 +202,29 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-meta@npm:^7.8.3": +"@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-import-attributes@npm:^7.24.7": + version: 7.28.6 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.28.6" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.28.6" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/6c8c6a5988dbb9799d6027360d1a5ba64faabf551f2ef11ba4eade0c62253b5c85d44ddc8eb643c74b9acb2bcaa664a950bd5de9a5d4aef291c4f2a48223bb4b + languageName: node + linkType: hard + +"@babel/plugin-syntax-import-meta@npm:^7.10.4": version: 7.10.4 resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" dependencies: @@ -224,18 +246,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.22.5 - resolution: "@babel/plugin-syntax-jsx@npm:7.22.5" +"@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.22.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/8829d30c2617ab31393d99cec2978e41f014f4ac6f01a1cecf4c4dd8320c3ec12fdc3ce121126b2d8d32f6887e99ca1a0bad53dedb1e6ad165640b92b24980ce + checksum: 10/572e38f5c1bb4b8124300e7e3dd13e82ae84a21f90d3f0786c98cd05e63c78ca1f32d1cfe462dfbaf5e7d5102fa7cd8fd741dfe4f3afc2e01a3b2877dcc8c866 languageName: node linkType: hard -"@babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": +"@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: @@ -257,7 +279,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: @@ -301,7 +323,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: @@ -312,14 +345,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.22.5 - resolution: "@babel/plugin-syntax-typescript@npm:7.22.5" +"@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.22.5" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/8ab7718fbb026d64da93681a57797d60326097fd7cb930380c8bffd9eb101689e90142c760a14b51e8e69c88a73ba3da956cb4520a3b0c65743aee5c71ef360a + checksum: 10/5c55f9c63bd36cf3d7e8db892294c8f85000f9c1526c3a1cc310d47d1e174f5c6f6605e5cc902c4636d885faba7a9f3d5e5edc6b35e4f3b1fd4c2d58d0304fa5 languageName: node linkType: hard @@ -342,7 +375,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.28.6, @babel/template@npm:^7.3.3": +"@babel/template@npm:^7.28.6": version: 7.28.6 resolution: "@babel/template@npm:7.28.6" dependencies: @@ -368,7 +401,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0, @babel/types@npm:^7.3.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.6, @babel/types@npm:^7.29.0": version: 7.29.0 resolution: "@babel/types@npm:7.29.0" dependencies: @@ -419,6 +452,34 @@ __metadata: languageName: node linkType: hard +"@emnapi/core@npm:^1.4.3": + version: 1.8.1 + resolution: "@emnapi/core@npm:1.8.1" + dependencies: + "@emnapi/wasi-threads": "npm:1.1.0" + tslib: "npm:^2.4.0" + checksum: 10/904ea60c91fc7d8aeb4a8f2c433b8cfb47c50618f2b6f37429fc5093c857c6381c60628a5cfbc3a7b0d75b0a288f21d4ed2d4533e82f92c043801ef255fd6a5c + languageName: node + linkType: hard + +"@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/26725e202d4baefdc4a6ba770f703dfc80825a27c27a08c22bac1e1ce6f8f75c47b4fe9424d9b63239463c33ef20b650f08d710da18dfa1164a95e5acb865dba + languageName: node + linkType: hard + +"@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/0d557e75262d2f4c95cb2a456ba0785ef61f919ce488c1d76e5e3acfd26e00c753ef928cd80068363e0c166ba8cc0141305daf0f81aad5afcd421f38f11e0f4e + 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": version: 4.9.1 resolution: "@eslint-community/eslint-utils@npm:4.9.1" @@ -577,6 +638,20 @@ __metadata: languageName: node linkType: hard +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 10/e9ed5fd27c3aec1095e3a16e0c0cf148d1fee55a38665c35f7b3f86a9b5d00d042ddaabc98e8a1cb7463b9378c15f22a94eb35e99469c201453eb8375191f243 + languageName: node + linkType: hard + "@isaacs/fs-minipass@npm:^4.0.0": version: 4.0.1 resolution: "@isaacs/fs-minipass@npm:4.0.1" @@ -599,240 +674,276 @@ __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" +"@jest/core@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/core@npm:30.2.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/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 + +"@jest/diff-sequences@npm:30.0.1": + version: 30.0.1 + resolution: "@jest/diff-sequences@npm:30.0.1" + checksum: 10/0ddb7c7ba92d6057a2ee51a9cfc2155b77cca707fe959167466ea02dcb0687018cc3c22b9622f25f3a417d6ad370e2d4dcfedf9f1410dc9c02954a7484423cc7 languageName: node linkType: hard -"@jest/environment@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/environment@npm:29.7.0" +"@jest/environment@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/environment@npm:30.2.0" dependencies: - "@jest/fake-timers": "npm:^29.7.0" - "@jest/types": "npm:^29.6.3" + "@jest/fake-timers": "npm:30.2.0" + "@jest/types": "npm:30.2.0" "@types/node": "npm:*" - jest-mock: "npm:^29.7.0" - checksum: 10/90b5844a9a9d8097f2cf107b1b5e57007c552f64315da8c1f51217eeb0a9664889d3f145cdf8acf23a84f4d8309a6675e27d5b059659a004db0ea9546d1c81a8 + jest-mock: "npm:30.2.0" + checksum: 10/e168a4ff328980eb9fde5e43aea80807fd0b2dbd4579ae8f68a03415a1e58adf5661db298054fa2351c7cb2b5a74bf67b8ab996656cf5927d0b0d0b6e2c2966b languageName: node linkType: hard -"@jest/expect-utils@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/expect-utils@npm:29.7.0" +"@jest/expect-utils@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/expect-utils@npm:30.2.0" dependencies: - jest-get-type: "npm:^29.6.3" - checksum: 10/ef8d379778ef574a17bde2801a6f4469f8022a46a5f9e385191dc73bb1fc318996beaed4513fbd7055c2847227a1bed2469977821866534593a6e52a281499ee + "@jest/get-type": "npm:30.1.0" + checksum: 10/f2442f1bceb3411240d0f16fd0074377211b4373d3b8b2dc28929e861b6527a6deb403a362c25afa511d933cda4dfbdc98d4a08eeb51ee4968f7cb0299562349 languageName: node linkType: hard -"@jest/expect@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/expect@npm:29.7.0" +"@jest/expect@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/expect@npm:30.2.0" dependencies: - expect: "npm:^29.7.0" - jest-snapshot: "npm:^29.7.0" - checksum: 10/fea6c3317a8da5c840429d90bfe49d928e89c9e89fceee2149b93a11b7e9c73d2f6e4d7cdf647163da938fc4e2169e4490be6bae64952902bc7a701033fd4880 + expect: "npm:30.2.0" + jest-snapshot: "npm:30.2.0" + checksum: 10/d950d95a64d5c6a39d56171dabb8dbe59423096231bb4f21d8ee0019878e6626701ac9d782803dc2589e2799ed39704031f818533f8a3e571b57032eafa85d12 languageName: node linkType: hard -"@jest/fake-timers@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/fake-timers@npm:29.7.0" +"@jest/fake-timers@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/fake-timers@npm:30.2.0" dependencies: - "@jest/types": "npm:^29.6.3" - "@sinonjs/fake-timers": "npm:^10.0.2" + "@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 -"@jest/globals@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/globals@npm:29.7.0" +"@jest/get-type@npm:30.1.0": + version: 30.1.0 + resolution: "@jest/get-type@npm:30.1.0" + checksum: 10/e2a95fbb49ce2d15547db8af5602626caf9b05f62a5e583b4a2de9bd93a2bfe7175f9bbb2b8a5c3909ce261d467b6991d7265bb1d547cb60e7e97f571f361a70 + languageName: node + linkType: hard + +"@jest/globals@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/globals@npm:30.2.0" + dependencies: + "@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/pattern@npm:30.0.1": + version: 30.0.1 + resolution: "@jest/pattern@npm:30.0.1" 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 + "@types/node": "npm:*" + jest-regex-util: "npm:30.0.1" + checksum: 10/afd03b4d3eadc9c9970cf924955dee47984a7e767901fe6fa463b17b246f0ddeec07b3e82c09715c54bde3c8abb92074160c0d79967bd23778724f184e7f5b7b languageName: node linkType: hard -"@jest/reporters@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/reporters@npm:29.7.0" +"@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 -"@jest/schemas@npm:^29.6.3": - version: 29.6.3 - resolution: "@jest/schemas@npm:29.6.3" +"@jest/schemas@npm:30.0.5": + version: 30.0.5 + resolution: "@jest/schemas@npm:30.0.5" dependencies: - "@sinclair/typebox": "npm:^0.27.8" - checksum: 10/910040425f0fc93cd13e68c750b7885590b8839066dfa0cd78e7def07bbb708ad869381f725945d66f2284de5663bbecf63e8fdd856e2ae6e261ba30b1687e93 + "@sinclair/typebox": "npm:^0.34.0" + checksum: 10/40df4db55d4aeed09d1c7e19caf23788309cea34490a1c5d584c913494195e698b9967e996afc27226cac6d76e7512fe73ae6b9584480695c60dd18a5459cdba 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:^29.6.3": - version: 29.6.3 - resolution: "@jest/types@npm:29.6.3" +"@jest/types@npm:30.2.0": + version: 30.2.0 + resolution: "@jest/types@npm:30.2.0" dependencies: - "@jest/schemas": "npm:^29.6.3" - "@types/istanbul-lib-coverage": "npm:^2.0.0" - "@types/istanbul-reports": "npm:^3.0.0" + "@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.8" - chalk: "npm:^4.0.0" - checksum: 10/f74bf512fd09bbe2433a2ad460b04668b7075235eea9a0c77d6a42222c10a79b9747dc2b2a623f140ed40d6865a2ed8f538f3cbb75169120ea863f29a7ed76cd + "@types/yargs": "npm:^17.0.33" + chalk: "npm:^4.1.2" + checksum: 10/f50fcaea56f873a51d19254ab16762f2ea8ca88e3e08da2e496af5da2b67c322915a4fcd0153803cc05063ffe87ebef2ab4330e0a1b06ab984a26c916cbfc26b languageName: node linkType: hard @@ -870,7 +981,7 @@ __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.24, @jridgewell/trace-mapping@npm:^0.3.28": +"@jridgewell/trace-mapping@npm:^0.3.12, @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": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: @@ -955,6 +1066,17 @@ __metadata: languageName: node linkType: hard +"@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 + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -1054,6 +1176,13 @@ __metadata: languageName: node linkType: hard +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 10/115e8ceeec6bc69dff2048b35c0ab4f8bbee12d8bb6c1f4af758604586d802b6e669dcb02dda61d078de42c2b4ddce41b3d9e726d7daa6b4b850f4adbf7333ff + languageName: node + linkType: hard + "@pkgr/core@npm:^0.1.0": version: 0.1.1 resolution: "@pkgr/core@npm:0.1.1" @@ -1061,6 +1190,13 @@ __metadata: languageName: node linkType: hard +"@pkgr/core@npm:^0.2.9": + version: 0.2.9 + resolution: "@pkgr/core@npm:0.2.9" + checksum: 10/bb2fb86977d63f836f8f5b09015d74e6af6488f7a411dcd2bfdca79d76b5a681a9112f41c45bdf88a9069f049718efc6f3900d7f1de66a2ec966068308ae517f + languageName: node + linkType: hard + "@shopify/jest-koa-mocks@npm:^5.3.1": version: 5.3.1 resolution: "@shopify/jest-koa-mocks@npm:5.3.1" @@ -1071,28 +1207,28 @@ __metadata: languageName: node linkType: hard -"@sinclair/typebox@npm:^0.27.8": - version: 0.27.8 - resolution: "@sinclair/typebox@npm:0.27.8" - checksum: 10/297f95ff77c82c54de8c9907f186076e715ff2621c5222ba50b8d40a170661c0c5242c763cba2a4791f0f91cb1d8ffa53ea1d7294570cf8cd4694c0e383e484d +"@sinclair/typebox@npm:^0.34.0": + version: 0.34.48 + resolution: "@sinclair/typebox@npm:0.34.48" + checksum: 10/186eebb338255db7cfd77c2f94be0ad91816c7b5ee994c3adb95e0474ae98b769574c2b6b1f26a81613d7148ed20b11e02528f4263d8d95e3ca8dcf8faaf5306 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 + checksum: 10/a0af217ba7044426c78df52c23cedede6daf377586f3ac58857c565769358ab1f44ebf95ba04bbe38814fba6e316ca6f02870a009328294fc2c555d0f85a7117 languageName: node linkType: hard -"@sinonjs/fake-timers@npm:^10.0.2": - version: 10.3.0 - resolution: "@sinonjs/fake-timers@npm:10.3.0" +"@sinonjs/fake-timers@npm:^13.0.0": + version: 13.0.5 + resolution: "@sinonjs/fake-timers@npm:13.0.5" dependencies: - "@sinonjs/commons": "npm:^3.0.0" - checksum: 10/78155c7bd866a85df85e22028e046b8d46cf3e840f72260954f5e3ed5bd97d66c595524305a6841ffb3f681a08f6e5cef572a2cce5442a8a232dc29fb409b83e + "@sinonjs/commons": "npm:^3.0.1" + checksum: 10/11ee417968fc4dce1896ab332ac13f353866075a9d2a88ed1f6258f17cc4f7d93e66031b51fcddb8c203aa4d53fd980b0ae18aba06269f4682164878a992ec3f languageName: node linkType: hard @@ -1251,6 +1387,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 + "@types/accepts@npm:*": version: 1.3.5 resolution: "@types/accepts@npm:1.3.5" @@ -1269,16 +1414,16 @@ __metadata: 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 @@ -1301,7 +1446,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: @@ -1386,15 +1531,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" @@ -1409,10 +1545,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.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 @@ -1425,22 +1561,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 @@ -1617,10 +1753,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 @@ -1661,12 +1797,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 @@ -1782,6 +1918,148 @@ __metadata: 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 + +"@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 + "JSONStream@npm:^1.0.4": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" @@ -1969,7 +2247,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: @@ -1985,6 +2263,13 @@ __metadata: languageName: node linkType: hard +"ansi-regex@npm:^6.0.1": + version: 6.2.2 + resolution: "ansi-regex@npm:6.2.2" + checksum: 10/9b17ce2c6daecc75bcd5966b9ad672c23b184dc3ed9bf3c98a0702f0d2f736c15c10d461913568f2cf527a5e64291c7473358885dd493305c84a1cfed66ba94f + languageName: node + linkType: hard + "ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -2003,14 +2288,21 @@ __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 languageName: node linkType: hard -"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3": +"ansi-styles@npm:^6.1.0": + version: 6.2.3 + resolution: "ansi-styles@npm:6.2.3" + checksum: 10/c49dad7639f3e48859bd51824c93b9eb0db628afc243c51c3dd2410c4a15ede1a83881c6c7341aa2b159c4f90c11befb38f2ba848c07c66c9f9de4bcd7cb9f30 + languageName: node + linkType: hard + +"anymatch@npm:^3.1.3": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -2199,7 +2491,7 @@ __metadata: "@types/app-root-path": "npm:^3.1.0" "@types/body-parser": "npm:^1.19.6" "@types/deep-extend": "npm:^0.6.2" - "@types/jest": "npm:^29.5.14" + "@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" @@ -2210,7 +2502,7 @@ __metadata: "@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" + babel-jest: "npm:^30.2.0" bcrypt: "npm:^6.0.0" body-parser: "npm:^1.20.4" deep-extend: "npm:0.6.0" @@ -2224,7 +2516,7 @@ __metadata: i18next-conv: "npm:^10.2.0" i18next-scanner: "npm:^4.6.0" indexof: "npm:0.0.1" - jest: "npm:^29.7.0" + jest: "npm:^30.2.0" koa: "npm:^3.1.1" koa-bodyparser: "npm:^4.4.1" koa-mount: "npm:^4.2.0" @@ -2272,79 +2564,79 @@ __metadata: 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 @@ -2756,7 +3048,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 @@ -2781,7 +3073,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0, 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: @@ -2805,10 +3097,10 @@ __metadata: 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 @@ -2823,13 +3115,20 @@ __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 +"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 + "cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -2896,10 +3195,10 @@ __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 @@ -3344,23 +3643,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" @@ -3456,15 +3738,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 @@ -3489,7 +3771,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 @@ -3594,7 +3876,7 @@ __metadata: 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 @@ -3611,13 +3893,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" @@ -3666,6 +3941,13 @@ __metadata: languageName: node linkType: hard +"eastasianwidth@npm:^0.2.0": + version: 0.2.0 + resolution: "eastasianwidth@npm:0.2.0" + checksum: 10/9b1d3e1baefeaf7d70799db8774149cef33b97183a6addceeba0cf6b85ba23ee2686f302f14482006df32df75d32b17c509c143a3689627929e4a8efaf483952 + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -3761,6 +4043,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^9.2.2": + version: 9.2.2 + resolution: "emoji-regex@npm:9.2.2" + checksum: 10/915acf859cea7131dac1b2b5c9c8e35c4849e325a1d114c30adb8cd615970f6dca0e27f64f3a4949d7d6ed86ecd79a1c5c63f02e697513cddd7b5835c90948b8 + languageName: node + linkType: hard + "enabled@npm:2.0.x": version: 2.0.0 resolution: "enabled@npm:2.0.0" @@ -4250,7 +4539,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: @@ -4267,23 +4556,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 @@ -4381,7 +4671,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: @@ -4535,6 +4825,16 @@ __metadata: languageName: node linkType: hard +"foreground-child@npm:^3.1.0": + version: 3.3.1 + resolution: "foreground-child@npm:3.3.1" + dependencies: + cross-spawn: "npm:^7.0.6" + signal-exit: "npm:^4.0.1" + checksum: 10/427b33f997a98073c0424e5c07169264a62cda806d8d2ded159b5b903fdfc8f0a1457e06b5fc35506497acb3f1e353f025edee796300209ac6231e80edece835 + languageName: node + linkType: hard + "form-data@npm:^4.0.4": version: 4.0.5 resolution: "form-data@npm:4.0.5" @@ -4588,7 +4888,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: @@ -4598,7 +4898,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: @@ -4831,6 +5131,22 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.3.10": + version: 10.5.0 + resolution: "glob@npm:10.5.0" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10/ab3bccfefcc0afaedbd1f480cd0c4a2c0e322eb3f0aa7ceaa31b3f00b825069f17cf0f1fc8b6f256795074b903f37c0ade37ddda6a176aa57f1c2bbfe7240653 + languageName: node + linkType: hard + "glob@npm:^13.0.0, glob@npm:^13.0.1": version: 13.0.1 resolution: "glob@npm:13.0.1" @@ -4842,7 +5158,7 @@ __metadata: 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: @@ -4886,7 +5202,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 @@ -5313,15 +5629,15 @@ __metadata: 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 @@ -5518,7 +5834,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 @@ -5769,29 +6085,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 @@ -5806,14 +6109,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 @@ -5827,238 +6130,248 @@ __metadata: languageName: node linkType: hard -"jest-changed-files@npm:^29.7.0": - version: 29.7.0 - resolution: "jest-changed-files@npm:29.7.0" +"jackspeak@npm:^3.1.2": + version: 3.4.3 + resolution: "jackspeak@npm:3.4.3" dependencies: - execa: "npm:^5.0.0" - jest-util: "npm:^29.7.0" + "@isaacs/cliui": "npm:^8.0.2" + "@pkgjs/parseargs": "npm:^0.11.0" + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 10/96f8786eaab98e4bf5b2a5d6d9588ea46c4d06bbc4f2eb861fdd7b6b182b16f71d8a70e79820f335d52653b16d4843b29dd9cdcf38ae80406756db9199497cf3 + languageName: node + linkType: hard + +"jest-changed-files@npm:30.2.0": + version: 30.2.0 + resolution: "jest-changed-files@npm:30.2.0" + dependencies: + 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: @@ -6070,199 +6383,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.7.0": - version: 29.7.0 - resolution: "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:^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 @@ -6406,13 +6721,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" @@ -6709,7 +7017,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.0.1": +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" checksum: 10/e6e90267360476720fa8e83cc168aa2bf0311f3f2eea20a6ba78b90a885ae72071d9db132f40fda4129c803e7dcec3a6b6a6fbb44ca90b081630b810b5d6a41a @@ -6960,7 +7268,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: @@ -7156,7 +7464,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4, 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 @@ -7287,6 +7595,15 @@ __metadata: 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" @@ -7808,6 +8125,13 @@ __metadata: languageName: node linkType: hard +"package-json-from-dist@npm:^1.0.0": + version: 1.0.1 + resolution: "package-json-from-dist@npm:1.0.1" + checksum: 10/58ee9538f2f762988433da00e26acc788036914d57c71c246bf0be1b60cdbd77dd60b6a3e1a30465f0b248aeb80079e0b34cb6050b1dfa18c06953bb1cbc7602 + languageName: node + linkType: hard + "pako@npm:~1.0.5": version: 1.0.11 resolution: "pako@npm:1.0.11" @@ -7927,6 +8251,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^1.11.1": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" + dependencies: + lru-cache: "npm:^10.2.0" + minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" + checksum: 10/5e8845c159261adda6f09814d7725683257fcc85a18f329880ab4d7cc1d12830967eae5d5894e453f341710d5484b8fdbbd4d75181b4d6e1eb2f4dc7aeadc434 + languageName: node + linkType: hard + "path-scurry@npm:^2.0.0": version: 2.0.0 resolution: "path-scurry@npm:2.0.0" @@ -7981,14 +8315,14 @@ __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.3": +"picomatch@npm:^4.0.2, picomatch@npm:^4.0.3": version: 4.0.3 resolution: "picomatch@npm:4.0.3" checksum: 10/57b99055f40b16798f2802916d9c17e9744e620a0db136554af01d19598b96e45e2f00014c91d1b8b13874b80caa8c295b3d589a3f72373ec4aaf54baa5962d5 @@ -8047,10 +8381,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 @@ -8095,14 +8429,14 @@ __metadata: 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 @@ -8154,16 +8488,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" @@ -8199,10 +8523,10 @@ __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 +"pure-rand@npm:^7.0.0": + version: 7.0.1 + resolution: "pure-rand@npm:7.0.1" + checksum: 10/c61a576fda5032ec9763ecb000da4a8f19263b9e2f9ae9aa2759c8fbd9dc6b192b2ce78391ebd41abb394a5fedb7bcc4b03c9e6141ac8ab20882dd5717698b80 languageName: node linkType: hard @@ -8302,10 +8626,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 @@ -8589,14 +8913,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.10.0": version: 1.22.6 resolution: "resolve@npm:1.22.6" dependencies: @@ -8609,7 +8926,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.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: @@ -8729,7 +9046,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.0.0, semver@npm:^6.3.0, semver@npm:^6.3.1": +"semver@npm:^6.0.0, semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -8738,7 +9055,7 @@ __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, semver@npm:^7.7.3": +"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, semver@npm:^7.7.2, semver@npm:^7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -8886,17 +9203,17 @@ __metadata: languageName: node linkType: hard -"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 languageName: node linkType: hard -"sisteransi@npm:^1.0.5": - version: 1.0.5 - resolution: "sisteransi@npm:1.0.5" - checksum: 10/aba6438f46d2bfcef94cf112c835ab395172c75f67453fe05c340c770d3c402363018ae1ab4172a1026a90c47eaccf3af7b6ff6fa749a680c2929bd7fa2b37a4 +"signal-exit@npm:^4.0.1": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 10/c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f languageName: node linkType: hard @@ -9111,7 +9428,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: @@ -9215,7 +9532,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: @@ -9225,7 +9542,7 @@ __metadata: languageName: node linkType: hard -"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: @@ -9236,6 +9553,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": + version: 5.1.2 + resolution: "string-width@npm:5.1.2" + dependencies: + eastasianwidth: "npm:^0.2.0" + emoji-regex: "npm:^9.2.2" + strip-ansi: "npm:^7.0.1" + checksum: 10/7369deaa29f21dda9a438686154b62c2c5f661f8dda60449088f9f980196f7908fc39fdd1803e3e01541970287cf5deae336798337e9319a7055af89dafa7193 + languageName: node + linkType: hard + "string.prototype.trim@npm:^1.2.8": version: 1.2.8 resolution: "string.prototype.trim@npm:1.2.8" @@ -9294,7 +9622,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": +"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" dependencies: @@ -9303,6 +9631,15 @@ __metadata: languageName: node linkType: hard +"strip-ansi@npm:^7.0.1": + version: 7.1.2 + resolution: "strip-ansi@npm:7.1.2" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10/db0e3f9654e519c8a33c50fc9304d07df5649388e7da06d3aabf66d29e5ad65d5e6315d8519d409c15b32fa82c1df7e11ed6f8cd50b0e4404463f0c9d77c8d0b + languageName: node + linkType: hard + "strip-bom@npm:^3.0.0": version: 3.0.0 resolution: "strip-bom@npm:3.0.0" @@ -9377,7 +9714,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: @@ -9393,6 +9730,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" @@ -9715,7 +10061,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 @@ -10003,6 +10349,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" @@ -10305,7 +10718,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" dependencies: @@ -10316,6 +10729,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: "npm:^6.1.0" + string-width: "npm:^5.0.1" + strip-ansi: "npm:^7.0.1" + checksum: 10/7b1e4b35e9bb2312d2ee9ee7dc95b8cb5f8b4b5a89f7dde5543fe66c1e3715663094defa50d75454ac900bd210f702d575f15f3f17fa9ec0291806d2578d1ddf + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" @@ -10323,13 +10747,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 @@ -10415,7 +10839,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/packages/job-worker/package.json b/packages/job-worker/package.json index 06be3626c7c..e1a3b200539 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -63,8 +63,8 @@ }, "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/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap b/packages/job-worker/src/ingest/mosDevice/__tests__/__snapshots__/mosIngest.test.ts.snap index 8c1b68d4433..68033783d5c 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`] = ` { 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..53709ddf99a 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`] = ` [ 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..e0de2f4a7ea 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,4 +1,4 @@ -// 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`] = ` [ 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/timeline/__tests__/__snapshots__/rundown.test.ts.snap b/packages/job-worker/src/playout/timeline/__tests__/__snapshots__/rundown.test.ts.snap index 60c9724e57a..5790c8a5eae 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`] = ` { diff --git a/packages/package.json b/packages/package.json index d3151f2e62c..18b88cde000 100644 --- a/packages/package.json +++ b/packages/package.json @@ -48,17 +48,17 @@ "@types/debug": "^4.1.12", "@types/ejson": "^2.2.2", "@types/got": "^9.6.12", - "@types/jest": "^29.5.14", + "@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.39.2", "eslint-plugin-react": "^7.37.5", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "jest-mock-extended": "^3.0.7", + "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.3", "nodemon": "^2.0.22", diff --git a/packages/webui/package.json b/packages/webui/package.json index 002207e3df0..f7d154c2c87 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -102,7 +102,7 @@ "@vitejs/plugin-react": "^5.1.3", "@welldone-software/why-did-you-render": "^4.3.2", "@xmldom/xmldom": "^0.8.11", - "babel-jest": "^29.7.0", + "babel-jest": "^30.2.0", "globals": "^17.3.0", "sass-embedded": "^1.97.3", "typescript": "~5.7.3", 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/yarn.lock b/packages/yarn.lock index f11273e132e..949216ecea8 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -665,7 +665,7 @@ __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.28.6, @babel/code-frame@npm:^7.29.0": +"@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: @@ -707,7 +707,7 @@ __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.29.0": +"@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: @@ -730,7 +730,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.5, @babel/generator@npm:^7.25.9, @babel/generator@npm:^7.29.0, @babel/generator@npm:^7.7.2": +"@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: @@ -944,7 +944,7 @@ __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.28.6, @babel/parser@npm:^7.29.0": +"@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: @@ -1045,7 +1045,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: @@ -1056,6 +1056,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" @@ -1078,7 +1089,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-import-attributes@npm:^7.28.6": +"@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: @@ -1089,7 +1100,7 @@ __metadata: 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: @@ -1111,18 +1122,18 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-jsx@npm:^7.25.9, @babel/plugin-syntax-jsx@npm:^7.7.2": - version: 7.25.9 - resolution: "@babel/plugin-syntax-jsx@npm:7.25.9" +"@babel/plugin-syntax-jsx@npm:^7.25.9, @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.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/bb609d1ffb50b58f0c1bac8810d0e46a4f6c922aa171c458f3a19d66ee545d36e782d3bffbbc1fed0dc65a558bdce1caf5279316583c0fff5a2c1658982a8563 + checksum: 10/572e38f5c1bb4b8124300e7e3dd13e82ae84a21f90d3f0786c98cd05e63c78ca1f32d1cfe462dfbaf5e7d5102fa7cd8fd741dfe4f3afc2e01a3b2877dcc8c866 languageName: node linkType: hard -"@babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": +"@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: @@ -1144,7 +1155,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: @@ -1188,7 +1199,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: @@ -1199,14 +1221,14 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.25.9, @babel/plugin-syntax-typescript@npm:^7.7.2": - version: 7.25.9 - resolution: "@babel/plugin-syntax-typescript@npm:7.25.9" +"@babel/plugin-syntax-typescript@npm:^7.25.9, @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.25.9" + "@babel/helper-plugin-utils": "npm:^7.28.6" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/0e9821e8ba7d660c36c919654e4144a70546942ae184e85b8102f2322451eae102cbfadbcadd52ce077a2b44b400ee52394c616feab7b5b9f791b910e933fd33 + checksum: 10/5c55f9c63bd36cf3d7e8db892294c8f85000f9c1526c3a1cc310d47d1e174f5c6f6605e5cc902c4636d885faba7a9f3d5e5edc6b35e4f3b1fd4c2d58d0304fa5 languageName: node linkType: hard @@ -2079,7 +2101,7 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.12.7, @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: @@ -2105,7 +2127,7 @@ __metadata: 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.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.3.3, @babel/types@npm:^7.4.4": +"@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: @@ -3464,31 +3486,31 @@ __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 @@ -4385,65 +4407,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 @@ -4454,48 +4476,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 @@ -4506,52 +4549,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 @@ -4573,61 +4625,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 @@ -4699,7 +4778,7 @@ __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.28, @jridgewell/trace-mapping@npm:^0.3.9": +"@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.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: @@ -5075,6 +5154,17 @@ __metadata: languageName: node linkType: hard +"@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" @@ -6924,7 +7014,7 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^3.0.0": +"@sinonjs/commons@npm:^3.0.1": version: 3.0.1 resolution: "@sinonjs/commons@npm:3.0.1" dependencies: @@ -6933,12 +7023,12 @@ __metadata: languageName: node linkType: hard -"@sinonjs/fake-timers@npm:^10.0.2": - version: 10.3.0 - resolution: "@sinonjs/fake-timers@npm:10.3.0" +"@sinonjs/fake-timers@npm:^13.0.0": + version: 13.0.5 + resolution: "@sinonjs/fake-timers@npm:13.0.5" dependencies: - "@sinonjs/commons": "npm:^3.0.0" - checksum: 10/78155c7bd866a85df85e22028e046b8d46cf3e840f72260954f5e3ed5bd97d66c595524305a6841ffb3f681a08f6e5cef572a2cce5442a8a232dc29fb409b83e + "@sinonjs/commons": "npm:^3.0.1" + checksum: 10/11ee417968fc4dce1896ab332ac13f353866075a9d2a88ed1f6258f17cc4f7d93e66031b51fcddb8c203aa4d53fd980b0ae18aba06269f4682164878a992ec3f languageName: node linkType: hard @@ -7062,8 +7152,8 @@ __metadata: amqplib: "npm:0.10.5" deepmerge: "npm:^4.3.1" elastic-apm-node: "npm:^4.15.0" - jest: "npm:^29.7.0" - jest-mock-extended: "npm:^3.0.7" + jest: "npm:^30.2.0" + jest-mock-extended: "npm:^4.0.0" mongodb: "npm:^6.12.0" p-lazy: "npm:^3.1.0" p-timeout: "npm:^4.1.0" @@ -7197,7 +7287,7 @@ __metadata: "@vitejs/plugin-react": "npm:^5.1.3" "@welldone-software/why-did-you-render": "npm:^4.3.2" "@xmldom/xmldom": "npm:^0.8.11" - babel-jest: "npm:^29.7.0" + babel-jest: "npm:^30.2.0" bootstrap: "npm:^5.3.8" classnames: "npm:^2.5.1" cubic-spline: "npm:^3.0.3" @@ -7806,6 +7896,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" @@ -7849,7 +7948,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: @@ -7881,7 +7980,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: @@ -8366,15 +8465,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/gtag.js@npm:^0.0.12": version: 0.0.12 resolution: "@types/gtag.js@npm:0.0.12" @@ -8435,10 +8525,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 @@ -8451,33 +8541,33 @@ __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 @@ -8846,10 +8936,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 @@ -8957,12 +9047,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, @types/yargs@npm:^17.0.8": + 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 @@ -9110,10 +9200,145 @@ __metadata: 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 @@ -9389,13 +9614,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" @@ -9436,16 +9654,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" @@ -9464,7 +9672,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: @@ -9473,7 +9681,7 @@ __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": +"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.4.1, acorn@npm:^8.8.2": version: 8.15.0 resolution: "acorn@npm:8.15.0" bin: @@ -9800,7 +10008,7 @@ __metadata: 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: @@ -10235,20 +10443,20 @@ 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" +"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 @@ -10274,28 +10482,25 @@ 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 @@ -10356,37 +10561,40 @@ 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:^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 @@ -11178,7 +11386,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 @@ -11484,10 +11692,10 @@ 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 @@ -11501,13 +11709,20 @@ asn1@evs-broadcast/node-asn1: 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 +"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" @@ -11681,10 +11896,10 @@ 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 @@ -12371,23 +12586,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" @@ -12709,29 +12907,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" @@ -13162,17 +13337,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" @@ -13310,10 +13474,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 @@ -13342,7 +13506,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: @@ -13354,6 +13518,18 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"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 + "deep-equal@npm:~1.0.1": version: 1.0.1 resolution: "deep-equal@npm:1.0.1" @@ -13375,7 +13551,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 @@ -13553,7 +13729,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 @@ -13613,13 +13789,6 @@ asn1@evs-broadcast/node-asn1: 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 - "diff@npm:^4.0.1": version: 4.0.4 resolution: "diff@npm:4.0.4" @@ -13745,15 +13914,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" @@ -14499,7 +14659,7 @@ asn1@evs-broadcast/node-asn1: 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: @@ -14985,7 +15145,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: @@ -15009,23 +15169,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 @@ -15266,7 +15427,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: @@ -15576,7 +15737,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"form-data@npm:^4.0.0, form-data@npm:^4.0.1, form-data@npm:^4.0.4": +"form-data@npm:^4.0.4": version: 4.0.4 resolution: "form-data@npm:4.0.4" dependencies: @@ -15721,7 +15882,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: @@ -15731,7 +15892,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: @@ -16048,7 +16209,7 @@ asn1@evs-broadcast/node-asn1: 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: @@ -16683,15 +16844,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" @@ -16965,7 +17117,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"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: @@ -17200,7 +17352,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: @@ -17212,6 +17364,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" @@ -17628,7 +17792,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 @@ -18070,29 +18234,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 @@ -18107,14 +18258,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 @@ -18202,110 +18353,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" - 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-cli@npm:30.2.0": + version: 30.2.0 + resolution: "jest-cli@npm:30.2.0" + dependencies: + "@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: @@ -18317,168 +18472,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: @@ -18490,124 +18624,139 @@ 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: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 @@ -18625,33 +18774,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 @@ -18666,7 +18828,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: @@ -18678,22 +18840,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 @@ -18763,53 +18925,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" @@ -18819,12 +18941,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: @@ -18832,7 +18954,7 @@ asn1@evs-broadcast/node-asn1: peerDependenciesMeta: canvas: optional: true - checksum: 10/8c230ee4657240bbbca6b4ebb484be53fc6a777a22a3357c80c5537222813666e3e1f54740bc13e769c461d9598ba7dac402c245949c6cef7ef7014ce6f36f01 + checksum: 10/39d78c4889cac20826393400dce1faed1666e9244fe0c8342a8f08c315375878e6be7fcfe339a33d6ff1a083bfe9e71b16d56ecf4d9a87db2da8c795925ea8c1 languageName: node linkType: hard @@ -21025,7 +21147,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: @@ -21663,6 +21785,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" @@ -22420,7 +22551,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 @@ -23158,17 +23289,17 @@ asn1@evs-broadcast/node-asn1: "@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/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.39.2" eslint-plugin-react: "npm:^7.37.5" - jest: "npm:^29.7.0" - jest-environment-jsdom: "npm:^29.7.0" - jest-mock-extended: "npm:^3.0.7" + 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.3" nodemon: "npm:^2.0.22" @@ -23419,7 +23550,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: @@ -23785,10 +23916,10 @@ 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 @@ -24755,7 +24886,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: @@ -24777,17 +24908,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" @@ -24939,7 +25059,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: @@ -25055,13 +25175,6 @@ asn1@evs-broadcast/node-asn1: 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" @@ -25114,7 +25227,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 @@ -25161,10 +25274,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"pure-rand@npm:^6.0.0": - version: 6.0.3 - resolution: "pure-rand@npm:6.0.3" - checksum: 10/68e6ebbc918d0022870cc436c26fd07b8ae6a71acc9aa83145d6e2ec0022e764926cbffc70c606fd25213c3b7234357d10458939182fb6568c2a364d1098cf34 +"pure-rand@npm:^7.0.0": + version: 7.0.1 + resolution: "pure-rand@npm:7.0.1" + checksum: 10/c61a576fda5032ec9763ecb000da4a8f19263b9e2f9ae9aa2759c8fbd9dc6b192b2ce78391ebd41abb394a5fedb7bcc4b03c9e6141ac8ab20882dd5717698b80 languageName: node linkType: hard @@ -25579,7 +25692,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 @@ -26386,14 +26499,14 @@ 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.17.0, resolve@npm:^1.20.0, resolve@npm:^1.22.11, resolve@npm:^1.3.2": +"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: @@ -26428,7 +26541,7 @@ 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.17.0#optional!builtin, resolve@patch:resolve@npm%3A^1.20.0#optional!builtin, resolve@patch:resolve@npm%3A^1.22.11#optional!builtin, resolve@patch:resolve@npm%3A^1.3.2#optional!builtin": +"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: @@ -27152,7 +27265,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: @@ -27985,7 +28098,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: @@ -28087,7 +28200,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: @@ -28486,7 +28599,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"synckit@npm:^0.11.12": +"synckit@npm:^0.11.12, synckit@npm:^0.11.8": version: 0.11.12 resolution: "synckit@npm:0.11.12" dependencies: @@ -28971,19 +29084,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: @@ -29001,21 +29102,12 @@ 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" +"tr46@npm:^5.1.0": + version: 5.1.1 + resolution: "tr46@npm:5.1.1" dependencies: punycode: "npm:^2.3.1" - checksum: 10/29155adb167d048d3c95d181f7cb5ac71948b4e8f3070ec455986e1f34634acae50ae02a3c8d448121c3afe35b76951cd46ed4c128fd80264280ca9502237a3e + checksum: 10/833a0e1044574da5790148fd17866d4ddaea89e022de50279967bcd6b28b4ce0d30d59eb3acf9702b60918975b3bad481400337e3a2e6326cffa5c77b874753d languageName: node linkType: hard @@ -29129,15 +29221,15 @@ 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 @@ -29897,13 +29989,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" @@ -29918,6 +30003,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" @@ -30019,7 +30171,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: @@ -30392,15 +30544,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" @@ -30751,15 +30894,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" @@ -30769,13 +30903,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" @@ -30783,23 +30910,13 @@ 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" - dependencies: - tr46: "npm:^3.0.0" - webidl-conversions: "npm:^7.0.0" - checksum: 10/dfcd51c6f4bfb54685528fb10927f3fd3d7c809b5671beef4a8cdd7b1408a7abf3343a35bc71dab83a1424f1c1e92cc2700d7930d95d231df0fac361de0c7648 - 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.0.0, whatwg-url@npm:^14.1.0 || ^13.0.0, 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 @@ -31037,7 +31154,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: @@ -31070,7 +31187,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: @@ -31145,7 +31262,7 @@ 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, ws@npm:^8.19.0": +"ws@npm:^8.13.0, ws@npm:^8.18.0, ws@npm:^8.18.3, ws@npm:^8.19.0": version: 8.19.0 resolution: "ws@npm:8.19.0" peerDependencies: @@ -31187,13 +31304,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" @@ -31301,7 +31411,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.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: From 193a9d05acfeaa91adbbc32b1b379852884c524d Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 14:43:10 +0000 Subject: [PATCH 070/291] chore: couple of missed libs --- packages/openapi/package.json | 2 +- packages/server-core-integration/package.json | 2 +- packages/yarn.lock | 139 +++++++++++------- 3 files changed, 88 insertions(+), 55 deletions(-) diff --git a/packages/openapi/package.json b/packages/openapi/package.json index f9cdc53100a..73ab8212bda 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -41,7 +41,7 @@ "devDependencies": { "@openapitools/openapi-generator-cli": "^2.28.0", "eslint": "^9.39.2", - "eslint-plugin-yml": "^1.19.1", + "eslint-plugin-yml": "^3.0.0", "js-yaml": "^4.1.1", "wget-improved": "^3.4.0" }, diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 3c08e6e14df..a7858e63618 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -72,7 +72,7 @@ "@types/koa__router": "^12.0.5" }, "dependencies": { - "@koa/router": "^14.0.0", + "@koa/router": "^15.3.0", "@sofie-automation/shared-lib": "26.3.0-0", "ejson": "^2.2.3", "faye-websocket": "^0.11.4", diff --git a/packages/yarn.lock b/packages/yarn.lock index 949216ecea8..22346cf8658 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -3743,6 +3743,15 @@ __metadata: languageName: node linkType: hard +"@eslint/core@npm:^1.0.1": + 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" @@ -3784,6 +3793,16 @@ __metadata: languageName: node linkType: hard +"@eslint/plugin-kit@npm:^0.5.1": + version: 0.5.1 + resolution: "@eslint/plugin-kit@npm:0.5.1" + dependencies: + "@eslint/core": "npm:^1.0.1" + levn: "npm:^0.4.1" + checksum: 10/ea68c0a01279daf3b0f5aa65d82f7d65690b9a1aa6e2c7998dfc431039d22dcf0bdacee644ce77eb8dcec8e61128f2cbc6d57c7fe5b108e8cd875d47e3f79cda + languageName: node + linkType: hard + "@floating-ui/core@npm:^1.7.4": version: 1.7.4 resolution: "@floating-ui/core@npm:1.7.4" @@ -4923,15 +4942,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 @@ -7208,7 +7232,7 @@ __metadata: dependencies: "@openapitools/openapi-generator-cli": "npm:^2.28.0" eslint: "npm:^9.39.2" - eslint-plugin-yml: "npm:^1.19.1" + eslint-plugin-yml: "npm:^3.0.0" js-yaml: "npm:^4.1.1" tslib: "npm:^2.8.1" wget-improved: "npm:^3.4.0" @@ -7219,7 +7243,7 @@ __metadata: version: 0.0.0-use.local resolution: "@sofie-automation/server-core-integration@workspace:server-core-integration" dependencies: - "@koa/router": "npm:^14.0.0" + "@koa/router": "npm:^15.3.0" "@sofie-automation/shared-lib": "npm:26.3.0-0" "@types/koa": "npm:^3.0.1" "@types/koa__router": "npm:^12.0.5" @@ -13782,10 +13806,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"diff-sequences@npm:^27.5.1": - version: 27.5.1 - resolution: "diff-sequences@npm:27.5.1" - checksum: 10/34d852a13eb82735c39944a050613f952038614ce324256e1c3544948fa090f1ca7f329a4f1f57c31fe7ac982c17068d8915b633e300f040b97708c81ceb26cd +"diff-sequences@npm:^29.0.0": + version: 29.6.3 + resolution: "diff-sequences@npm:29.6.3" + checksum: 10/179daf9d2f9af5c57ad66d97cb902a538bcf8ed64963fa7aa0c329b3de3665ce2eb6ffdc2f69f29d445fa4af2517e5e55e5b6e00c00a9ae4f43645f97f7078cb languageName: node linkType: hard @@ -14631,10 +14655,10 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"escape-string-regexp@npm:4.0.0, escape-string-regexp@npm:^4.0.0": - version: 4.0.0 - resolution: "escape-string-regexp@npm:4.0.0" - checksum: 10/98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 +"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 @@ -14652,10 +14676,10 @@ 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 +"escape-string-regexp@npm:^4.0.0": + version: 4.0.0 + resolution: "escape-string-regexp@npm:4.0.0" + checksum: 10/98b48897d93060f2322108bf29db0feba7dd774be96cd069458d1453347b25ce8682ecc39859d4bca2203cc0ab19c237bcc71755eff49a0f8d90beadeeba5cc5 languageName: node linkType: hard @@ -14688,17 +14712,6 @@ 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.1.8": version: 10.1.8 resolution: "eslint-config-prettier@npm:10.1.8" @@ -14808,19 +14821,20 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"eslint-plugin-yml@npm:^1.19.1": - version: 1.19.1 - resolution: "eslint-plugin-yml@npm:1.19.1" +"eslint-plugin-yml@npm:^3.0.0": + version: 3.0.0 + resolution: "eslint-plugin-yml@npm:3.0.0" dependencies: + "@eslint/core": "npm:^1.0.1" + "@eslint/plugin-kit": "npm:^0.5.1" debug: "npm:^4.3.2" - diff-sequences: "npm:^27.5.1" - escape-string-regexp: "npm:4.0.0" - eslint-compat-utils: "npm:^0.6.0" + 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/47c31c7c87a0ae601666aa18ab48c373fe79f039da00d8fa42b5f7298338ada45bfba7e07a2a59b612473e97146b34f39e06a84d48d425e020b7be1c9c7ba871 + eslint: ">=9.38.0" + checksum: 10/501d78791d45e7c0e722e727bc51e782fe80cb5762b0d1f3b72b27fabcb8610bd8dfbe3c2ba1dc03108cdb08811047152ba3564ba49d030bbe27b39cf3f0b9dd languageName: node linkType: hard @@ -14844,7 +14858,7 @@ 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 @@ -14858,6 +14872,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"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" @@ -16986,7 +17007,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: @@ -16999,6 +17020,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" @@ -23742,7 +23776,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"path-to-regexp@npm:8.3.0, path-to-regexp@npm:^8.2.0": +"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 @@ -27455,7 +27489,7 @@ 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 @@ -28128,7 +28162,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 @@ -29038,7 +29072,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 @@ -31377,14 +31411,13 @@ 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 From 92b358d04a6af9637e955f888a1abe7d39529c6e Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 15:09:05 +0000 Subject: [PATCH 071/291] chore: replace standard-version with fork --- meteor/package.json | 6 +- meteor/yarn.lock | 454 +++++++++++++++++++++----------------------- 2 files changed, 221 insertions(+), 239 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 5381765cd26..571d9a64f88 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -28,7 +28,7 @@ "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", @@ -90,6 +90,7 @@ "@types/semver": "^7.7.1", "@types/underscore": "^1.13.0", "babel-jest": "^30.2.0", + "commit-and-tag-version": "^12.6.1", "ejson": "^2.2.3", "eslint": "^9.39.2", "fast-clone": "^1.5.13", @@ -100,7 +101,6 @@ "legally": "^3.5.10", "open-cli": "^8.0.0", "prettier": "^3.8.1", - "standard-version": "^9.5.0", "ts-jest": "^29.4.6", "typescript": "~5.7.3", "yargs": "^17.7.2" @@ -120,7 +120,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/yarn.lock b/meteor/yarn.lock index e83059ae4e5..b7c039aa8f8 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1017,13 +1017,50 @@ __metadata: languageName: node linkType: hard +"@meteorjs/browserify-sign@npm:^4.2.3": + version: 4.2.6 + resolution: "@meteorjs/browserify-sign@npm:4.2.6" + dependencies: + bn.js: "npm:^5.2.1" + brorand: "npm:^1.1.0" + browserify-rsa: "npm:^4.1.0" + create-hash: "npm:^1.2.0" + create-hmac: "npm:^1.1.7" + hash-base: "npm:~3.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" + parse-asn1: "npm:^5.1.7" + readable-stream: "npm:^2.3.8" + safe-buffer: "npm:^5.2.1" + checksum: 10/a4e5dc58d348f373a28ba3e55b27967780e8b674a180f6408db944de888e647f6d10c1b2f7a544f8fafcfcd50e0e990a4a5cc974f38346655c3a4f029001c640 + languageName: node + linkType: hard + +"@meteorjs/create-ecdh@npm:^4.0.4": + version: 4.0.5 + resolution: "@meteorjs/create-ecdh@npm:4.0.5" + 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/e2173d7594eb0be2dd5cdd8840a9c49f0c653948ab73a6b5370bbfc22519255cc44cbd7dcd093cf9900064bf275adba3feb3de710e1fe98428e6a2f228f8c2ec + languageName: node + linkType: hard + "@meteorjs/crypto-browserify@npm:^3.12.1": - version: 3.12.1 - resolution: "@meteorjs/crypto-browserify@npm:3.12.1" + version: 3.12.4 + resolution: "@meteorjs/crypto-browserify@npm:3.12.4" dependencies: + "@meteorjs/browserify-sign": "npm:^4.2.3" + "@meteorjs/create-ecdh": "npm:^4.0.4" 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" @@ -1033,7 +1070,7 @@ __metadata: public-encrypt: "npm:^4.0.3" randombytes: "npm:^2.1.0" randomfill: "npm:^1.0.4" - checksum: 10/6b15dab879d0717768280f852bb6358eee294695fffc75a5d44d2eb6c814d307803fd8c36e7b579a84d5291229e9c10a910c5941bd2f21604acd4e0ffd0a737c + checksum: 10/c6c033ee5efda6e2340dc8719eb17e838c256f0c52c7cfedb711f0e002c78d97f1466c3ececa7ba4cd042dda804987fa093113f81e65028a132728c7c5c75cad languageName: node linkType: hard @@ -2060,7 +2097,7 @@ __metadata: 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: @@ -2505,6 +2542,7 @@ __metadata: 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" @@ -2531,7 +2569,6 @@ __metadata: p-lazy: "npm:^3.1.0" prettier: "npm:^3.8.1" semver: "npm:^7.7.3" - standard-version: "npm:^9.5.0" superfly-timeline: "npm:9.2.0" threadedclass: "npm:^1.3.0" timecode: "npm:0.0.4" @@ -2832,24 +2869,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" @@ -3292,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" @@ -3358,31 +3402,26 @@ __metadata: 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 @@ -3393,171 +3432,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 @@ -3606,16 +3626,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" @@ -3677,7 +3687,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 @@ -3869,7 +3879,7 @@ __metadata: 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 @@ -4014,21 +4024,6 @@ __metadata: languageName: node linkType: hard -"elliptic@npm:^6.5.3, elliptic@npm:^6.5.5": - 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 - languageName: node - linkType: hard - "emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" @@ -4662,6 +4657,17 @@ __metadata: languageName: node linkType: hard +"fast-xml-parser@npm:^5.2.5": + 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 + "fastq@npm:^1.13.0, fastq@npm:^1.6.0": version: 1.15.0 resolution: "fastq@npm:1.15.0" @@ -4699,7 +4705,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: @@ -4982,7 +4988,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: @@ -5051,18 +5057,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 @@ -5076,15 +5080,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 @@ -6989,7 +6993,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.0.0, lodash@npm:^4.17.15": +"lodash@npm:^4.0.0": version: 4.17.23 resolution: "lodash@npm:4.17.23" checksum: 10/82504c88250f58da7a5a4289f57a4f759c44946c005dd232821c7688b5fcfbf4a6268f6a6cdde4b792c91edd2f3b5398c1d2a0998274432cff76def48735e233 @@ -7189,7 +7193,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: @@ -7500,7 +7504,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 @@ -7755,7 +7759,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: @@ -8530,13 +8534,6 @@ __metadata: languageName: node linkType: hard -"q@npm:^1.5.1": - version: 1.5.1 - resolution: "q@npm:1.5.1" - checksum: 10/70c4a30b300277165cd855889cd3aa681929840a5940413297645c5691e00a3549a2a4153131efdf43fe8277ee8cf5a34c9636dcb649d83ad47f311a015fd380 - languageName: node - linkType: hard - "qs@npm:^6.12.3, qs@npm:^6.5.2, qs@npm:~6.14.0": version: 6.14.1 resolution: "qs@npm:6.14.1" @@ -9046,7 +9043,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^6.0.0, semver@npm:^6.3.1": +"semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" bin: @@ -9055,7 +9052,7 @@ __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, semver@npm:^7.7.2, semver@npm:^7.7.3": +"semver@npm:^7.0.0, 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, semver@npm:^7.7.2, semver@npm:^7.7.3": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -9373,7 +9370,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: @@ -9389,7 +9386,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: @@ -9444,30 +9441,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" @@ -9615,13 +9588,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" @@ -9677,6 +9643,13 @@ __metadata: languageName: node linkType: hard +"strnum@npm:^2.1.0": + version: 2.1.2 + resolution: "strnum@npm:2.1.2" + checksum: 10/7d894dff385e3a5c5b29c012cf0a7ea7962a92c6a299383c3d6db945ad2b6f3e770511356a9774dbd54444c56af1dc7c435dad6466c47293c48173274dd6c631 + languageName: node + linkType: hard + "strtok3@npm:^7.0.0": version: 7.0.0 resolution: "strtok3@npm:7.0.0" @@ -10810,6 +10783,15 @@ __metadata: 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" @@ -10824,7 +10806,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: From 4d3a2f4f49e97e0be360063c5c7bb34389788a13 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 15:11:37 +0000 Subject: [PATCH 072/291] chore: remove unused lib --- meteor/package.json | 2 -- meteor/yarn.lock | 18 ------------------ packages/webui/package.json | 2 -- packages/yarn.lock | 20 -------------------- 4 files changed, 42 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 571d9a64f88..99dc28e25f9 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -45,7 +45,6 @@ "@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": "^6.0.0", "body-parser": "^1.20.4", "deep-extend": "0.6.0", @@ -76,7 +75,6 @@ "@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": "^3.1.0", "@types/body-parser": "^1.19.6", "@types/deep-extend": "^0.6.2", "@types/jest": "^30.0.0", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index b7c039aa8f8..c09751d916d 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1442,15 +1442,6 @@ __metadata: languageName: node linkType: hard -"@types/app-root-path@npm:^3.1.0": - version: 3.1.0 - resolution: "@types/app-root-path@npm:3.1.0" - dependencies: - app-root-path: "npm:*" - checksum: 10/e62ce16359e91a708d8acefa2a05dd5e6c723dd7789c8914e884b13a032e0e84a5c92bf826728fa63bbb9d96b3647c2ef40fced36f2d8a8210b44a1f6f30dd29 - languageName: node - linkType: hard - "@types/babel__core@npm:^7.20.5": version: 7.20.5 resolution: "@types/babel__core@npm:7.20.5" @@ -2349,13 +2340,6 @@ __metadata: languageName: node linkType: hard -"app-root-path@npm:*, 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 - "argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -2525,7 +2509,6 @@ __metadata: "@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:^3.1.0" "@types/body-parser": "npm:^1.19.6" "@types/deep-extend": "npm:^0.6.2" "@types/jest": "npm:^30.0.0" @@ -2538,7 +2521,6 @@ __metadata: "@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:^30.2.0" bcrypt: "npm:^6.0.0" body-parser: "npm:^1.20.4" diff --git a/packages/webui/package.json b/packages/webui/package.json index f7d154c2c87..fa4cc85e285 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -62,7 +62,6 @@ "rc-tooltip": "^6.4.0", "react": "^18.3.1", "react-bootstrap": "^2.10.10", - "react-circular-progressbar": "^2.2.0", "react-datepicker": "^9.1.0", "react-dnd": "^14.0.5", "react-dnd-html5-backend": "^14.1.0", @@ -92,7 +91,6 @@ "@types/classnames": "^2.3.4", "@types/deep-extend": "^0.6.2", "@types/react": "^18.3.27", - "@types/react-circular-progressbar": "^1.1.0", "@types/react-dom": "^18.3.7", "@types/react-router": "^5.1.20", "@types/react-router-bootstrap": "^0.26.8", diff --git a/packages/yarn.lock b/packages/yarn.lock index 22346cf8658..bb06cf29e14 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -7301,7 +7301,6 @@ __metadata: "@types/classnames": "npm:^2.3.4" "@types/deep-extend": "npm:^0.6.2" "@types/react": "npm:^18.3.27" - "@types/react-circular-progressbar": "npm:^1.1.0" "@types/react-dom": "npm:^18.3.7" "@types/react-router": "npm:^5.1.20" "@types/react-router-bootstrap": "npm:^0.26.8" @@ -7330,7 +7329,6 @@ __metadata: rc-tooltip: "npm:^6.4.0" react: "npm:^18.3.1" react-bootstrap: "npm:^2.10.10" - react-circular-progressbar: "npm:^2.2.0" react-datepicker: "npm:^9.1.0" react-dnd: "npm:^14.0.5" react-dnd-html5-backend: "npm:^14.1.0" @@ -8793,15 +8791,6 @@ __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-dom@npm:^18.3.7": version: 18.3.7 resolution: "@types/react-dom@npm:18.3.7" @@ -25553,15 +25542,6 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"react-circular-progressbar@npm:*, react-circular-progressbar@npm:^2.2.0": - version: 2.2.0 - resolution: "react-circular-progressbar@npm:2.2.0" - peerDependencies: - react: ">=0.14.0" - checksum: 10/3b117f58745745dcbfe8b326945343f50761cd434ae241ae8d9ce7ba5a7ee662038e34e341a209cb05141d2ca654c57120a1940b32a0f0c2890edb54b4316bf4 - languageName: node - linkType: hard - "react-datepicker@npm:^9.1.0": version: 9.1.0 resolution: "react-datepicker@npm:9.1.0" From 3b4335cfb9708769fe0fa55d17a690434c2e7717 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 4 Feb 2026 15:23:02 +0000 Subject: [PATCH 073/291] chore: update mongodb client --- meteor/yarn.lock | 30 +++++++++++++------------- packages/job-worker/package.json | 2 +- packages/yarn.lock | 36 ++++++++++++++++---------------- 3 files changed, 34 insertions(+), 34 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index c09751d916d..eb49a506e7a 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1074,12 +1074,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 @@ -1376,7 +1376,7 @@ __metadata: amqplib: "npm:0.10.5" deepmerge: "npm:^4.3.1" elastic-apm-node: "npm:^4.15.0" - mongodb: "npm:^6.12.0" + mongodb: "npm:^6.21.0" p-lazy: "npm:^3.1.0" p-timeout: "npm:^4.1.0" superfly-timeline: "npm:9.2.0" @@ -2892,7 +2892,7 @@ __metadata: languageName: node linkType: hard -"bson@npm:^6.10.1": +"bson@npm:^6.10.4": version: 6.10.4 resolution: "bson@npm:6.10.4" checksum: 10/8a79a452219a13898358a5abc93e32bc3805236334f962661da121ce15bd5cade27718ba3310ee2a143ff508489b08467eed172ecb2a658cb8d2e94fdb76b215 @@ -7507,7 +7507,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: @@ -7517,20 +7517,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": @@ -7547,7 +7547,7 @@ __metadata: optional: true socks: optional: true - checksum: 10/769cc18eb3e34dabdbe56abd4862a1d79214fab79a96f8e8b0c67f2681dd002e88e0f8c869871b82a257297f87b318f930900eb65ad3378edc36bac5d2a7d542 + checksum: 10/28d2cab1c55c4cf58e410529ac6ae4c79a233adeb2147ba872d912819a0b496ee2dc5b9819ccbf0527618ced3b841e733b221fd1c627901e8e87ae60a8dc0553 languageName: node linkType: hard diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index e1a3b200539..dd7ad9c1fcd 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -43,7 +43,7 @@ "amqplib": "0.10.5", "deepmerge": "^4.3.1", "elastic-apm-node": "^4.15.0", - "mongodb": "^6.12.0", + "mongodb": "^6.21.0", "p-lazy": "^3.1.0", "p-timeout": "^4.1.0", "superfly-timeline": "9.2.0", diff --git a/packages/yarn.lock b/packages/yarn.lock index bb06cf29e14..4f74ef5080e 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -5116,12 +5116,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 @@ -7178,7 +7178,7 @@ __metadata: elastic-apm-node: "npm:^4.15.0" jest: "npm:^30.2.0" jest-mock-extended: "npm:^4.0.0" - mongodb: "npm:^6.12.0" + mongodb: "npm:^6.21.0" p-lazy: "npm:^3.1.0" p-timeout: "npm:^4.1.0" superfly-timeline: "npm:9.2.0" @@ -11082,10 +11082,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 @@ -21616,7 +21616,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: @@ -21626,20 +21626,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": @@ -21656,7 +21656,7 @@ asn1@evs-broadcast/node-asn1: optional: true socks: optional: true - checksum: 10/769cc18eb3e34dabdbe56abd4862a1d79214fab79a96f8e8b0c67f2681dd002e88e0f8c869871b82a257297f87b318f930900eb65ad3378edc36bac5d2a7d542 + checksum: 10/28d2cab1c55c4cf58e410529ac6ae4c79a233adeb2147ba872d912819a0b496ee2dc5b9819ccbf0527618ced3b841e733b221fd1c627901e8e87ae60a8dc0553 languageName: node linkType: hard From 7bf5d1896f62d2e22178b191531aac72c7864fd9 Mon Sep 17 00:00:00 2001 From: romain-garcia-rodriguez Date: Thu, 16 Oct 2025 10:49:14 +0200 Subject: [PATCH 074/291] fix: Add publicData property to query result for rundownBaselineAdlibAction --- packages/job-worker/src/playout/adlibAction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index fa85af74050..7fc484a7791 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -83,7 +83,7 @@ export async function executeAdlibActionAndSaveModel( context.directCollections.RundownBaselineAdLibActions.findOne( data.actionDocId as RundownBaselineAdLibActionId, { - projection: { _id: 1, privateData: 1 }, + projection: { _id: 1, privateData: 1, publicData: 1 }, } ), context.directCollections.BucketAdLibActions.findOne(data.actionDocId as BucketAdLibActionId, { From aa6209783d84849ab1c8545fb5a3094104f57cfe Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:06:55 +0000 Subject: [PATCH 075/291] docs: Fix broken doc links (#1632) --- .../docs/user-guide/configuration/settings-view.md | 8 ++++---- .../installation/installing-a-gateway/input-gateway.md | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/documentation/docs/user-guide/configuration/settings-view.md b/packages/documentation/docs/user-guide/configuration/settings-view.md index 2969071e76d..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) 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 index 0aa8dcef30e..eeb3dc03600 100644 --- 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 @@ -8,7 +8,7 @@ The Input Gateway handles control devices that are not capable of running a Web 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), the Sofie instance that Input Gateway needs to connect to is configured through command line arguments. A minimal configuration could look something like this. +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 @@ -16,7 +16,7 @@ input-gateway.exe --host --port --https --i 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. +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. From 89ce4b24264b4e965ab0a4efaa1092c8149a43f5 Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:06:55 +0000 Subject: [PATCH 076/291] docs: Fix broken doc links (#1632) --- .../docs/user-guide/configuration/settings-view.md | 8 ++++---- .../installation/installing-a-gateway/input-gateway.md | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/documentation/docs/user-guide/configuration/settings-view.md b/packages/documentation/docs/user-guide/configuration/settings-view.md index 2969071e76d..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) 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 index 0aa8dcef30e..eeb3dc03600 100644 --- 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 @@ -8,7 +8,7 @@ The Input Gateway handles control devices that are not capable of running a Web 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), the Sofie instance that Input Gateway needs to connect to is configured through command line arguments. A minimal configuration could look something like this. +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 @@ -16,7 +16,7 @@ input-gateway.exe --host --port --https --i 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. +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. From 9bad64309f278a2b8fd5f5f425a450413187f74c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 9 Feb 2026 10:32:43 +0000 Subject: [PATCH 077/291] chore: remove deprecated @types/koa__router --- meteor/package.json | 1 - meteor/yarn.lock | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 99dc28e25f9..8d7a51ff830 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -83,7 +83,6 @@ "@types/koa-mount": "^4.0.5", "@types/koa-static": "^4.0.4", "@types/koa__cors": "^5.0.1", - "@types/koa__router": "^12.0.5", "@types/node": "^22.19.8", "@types/semver": "^7.7.1", "@types/underscore": "^1.13.0", diff --git a/meteor/yarn.lock b/meteor/yarn.lock index eb49a506e7a..b96608d8347 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1693,15 +1693,6 @@ __metadata: languageName: node linkType: hard -"@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/c619137a2871835b5918ea67b15f2e01052ae94c8de4d27f8b26b366cddd543fa1c623c6588a839dfcbd45ca961c78bfb46c4f824de2c7c3c2cdcd491d3c7170 - languageName: node - linkType: hard - "@types/mime@npm:*": version: 3.0.1 resolution: "@types/mime@npm:3.0.1" @@ -2517,7 +2508,6 @@ __metadata: "@types/koa-mount": "npm:^4.0.5" "@types/koa-static": "npm:^4.0.4" "@types/koa__cors": "npm:^5.0.1" - "@types/koa__router": "npm:^12.0.5" "@types/node": "npm:^22.19.8" "@types/semver": "npm:^7.7.1" "@types/underscore": "npm:^1.13.0" From 88ccf8b40749799c371815520fedcb23b107c8ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:40:29 +0000 Subject: [PATCH 078/291] Merge pull request #1639 from Sofie-Automation/dependabot/github_actions/actions/cache-5 chore(deps): bump actions/cache from 4 to 5 --- .github/workflows/node.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index c6a672ef1b4..17342c73945 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -120,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 @@ -319,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 From 14d8f65adfdb1575c42662d5cc6eb5845681b988 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 9 Feb 2026 10:44:06 +0000 Subject: [PATCH 079/291] chore: fix lint and test --- meteor/package.json | 1 + meteor/server/api/deviceTriggers/triggersContext.ts | 6 +++--- meteor/server/collections/collection.ts | 5 +++-- .../server/collections/implementations/asyncCollection.ts | 6 +++--- .../server/collections/implementations/readonlyWrapper.ts | 6 +++--- .../pieceContentStatusUI/checkPieceContentStatus.ts | 6 ++++-- meteor/yarn.lock | 3 ++- 7 files changed, 19 insertions(+), 14 deletions(-) diff --git a/meteor/package.json b/meteor/package.json index 8d7a51ff830..893a5b3f3ed 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -95,6 +95,7 @@ "i18next-conv": "^10.2.0", "i18next-scanner": "^4.6.0", "jest": "^30.2.0", + "jest-util": "^30.2.0", "legally": "^3.5.10", "open-cli": "^8.0.0", "prettier": "^3.8.1", diff --git a/meteor/server/api/deviceTriggers/triggersContext.ts b/meteor/server/api/deviceTriggers/triggersContext.ts index bbe5df5c51d..79eee1de0e7 100644 --- a/meteor/server/api/deviceTriggers/triggersContext.ts +++ b/meteor/server/api/deviceTriggers/triggersContext.ts @@ -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) { 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/publications/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index f6bf71d0a70..c4df7cf748b 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -211,8 +211,10 @@ export type PieceContentStatusPiece = Pick< */ previousPieceInstanceId?: PieceInstanceId } -export interface PieceContentStatusStudio - extends Pick { +export interface PieceContentStatusStudio extends Pick< + DBStudio, + '_id' | 'previewContainerIds' | 'thumbnailContainerIds' +> { /** Mappings between the physical devices / outputs and logical ones */ mappings: MappingsExt /** Route sets with overrides */ diff --git a/meteor/yarn.lock b/meteor/yarn.lock index b96608d8347..d56cffa4659 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -2527,6 +2527,7 @@ __metadata: i18next-scanner: "npm:^4.6.0" indexof: "npm:0.0.1" 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.2.0" @@ -6481,7 +6482,7 @@ __metadata: languageName: node linkType: hard -"jest-util@npm:30.2.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: From 1f0425984f000beb1a33b32bfe8444c83f11c847 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 9 Feb 2026 10:45:20 +0000 Subject: [PATCH 080/291] chore: fix docs publish (#1640) --- .github/workflows/deploy-docs.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 056422a92ec..ad9c1a294ed 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -28,6 +28,7 @@ jobs: 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: @@ -38,9 +39,13 @@ jobs: run: | corepack enable - cd packages 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 @@ -81,4 +86,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 From 84330f71fb25a6f84e0b9bf1683e5827070c3da2 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 9 Feb 2026 10:45:20 +0000 Subject: [PATCH 081/291] chore: fix docs publish (#1640) --- .github/workflows/deploy-docs.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 056422a92ec..ad9c1a294ed 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -28,6 +28,7 @@ jobs: 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: @@ -38,9 +39,13 @@ jobs: run: | corepack enable - cd packages 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 @@ -81,4 +86,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v4 From 1c2df5e6cc1287731866c90b883d620cb667aeb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Mon, 9 Feb 2026 13:56:27 +0100 Subject: [PATCH 082/291] fix: include publicData for AdLibActions and BucketAdLibActions --- packages/job-worker/src/playout/adlibAction.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 7fc484a7791..e2dc816487f 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -78,7 +78,7 @@ export async function executeAdlibActionAndSaveModel( const [adLibAction, baselineAdLibAction, bucketAdLibAction] = await Promise.all([ context.directCollections.AdLibActions.findOne(data.actionDocId as AdLibActionId, { - projection: { _id: 1, privateData: 1 }, + projection: { _id: 1, privateData: 1, publicData: 1 }, }), context.directCollections.RundownBaselineAdLibActions.findOne( data.actionDocId as RundownBaselineAdLibActionId, @@ -87,7 +87,7 @@ export async function executeAdlibActionAndSaveModel( } ), context.directCollections.BucketAdLibActions.findOne(data.actionDocId as BucketAdLibActionId, { - projection: { _id: 1, privateData: 1 }, + projection: { _id: 1, privateData: 1, publicData: 1 }, }), ]) const adLibActionDoc = adLibAction ?? baselineAdLibAction ?? bucketAdLibAction From 0fc5bb40312f56408d5d72ae042ea63e0c0ab33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Fri, 8 Nov 2024 15:18:28 +0100 Subject: [PATCH 083/291] feat: add activate-adlib-testing API endpoint --- meteor/server/api/rest/v1/playlists.ts | 42 +++++++++++++++++++ meteor/server/lib/rest/v1/playlists.ts | 10 +++++ packages/openapi/api/actions.yaml | 2 + .../openapi/api/definitions/playlists.yaml | 26 ++++++++++++ .../openapi/src/__tests__/playlists.spec.ts | 8 ++++ 5 files changed, 88 insertions(+) diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 837e3dd39aa..f3015e41c0d 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' @@ -123,6 +124,29 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { ) } + async activateAdLibTesting( + connection: Meteor.Connection, + event: string, + rundownPlaylistId: RundownPlaylistId, + rundownId: RundownId + ): Promise> { + return ServerClientAPI.runUserActionInLogForPlaylistOnWorker( + this.context.getMethodContext(connection), + event, + getCurrentTime(), + rundownPlaylistId, + () => { + check(rundownPlaylistId, String) + check(rundownId, String) + }, + StudioJobs.ActivateAdlibTesting, + { + playlistId: rundownPlaylistId, + rundownId: rundownId, + } + ) + } + async deactivate( connection: Meteor.Connection, event: string, @@ -663,6 +687,24 @@ 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) + return await serverAPI.activateAdLibTesting(connection, event, rundownPlaylistId, rundownId) + } + ) + registerRoute<{ playlistId: string }, never, void>( 'put', '/playlists/:playlistId/deactivate', diff --git a/meteor/server/lib/rest/v1/playlists.ts b/meteor/server/lib/rest/v1/playlists.ts index 74a60a29762..227f161ba67 100644 --- a/meteor/server/lib/rest/v1/playlists.ts +++ b/meteor/server/lib/rest/v1/playlists.ts @@ -7,6 +7,7 @@ import { PartInstanceId, PieceId, RundownBaselineAdLibActionId, + RundownId, RundownPlaylistId, SegmentId, } from '@sofie-automation/corelib/dist/dataModel/Ids' @@ -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. * diff --git a/packages/openapi/api/actions.yaml b/packages/openapi/api/actions.yaml index ce598e0daa9..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: diff --git a/packages/openapi/api/definitions/playlists.yaml b/packages/openapi/api/definitions/playlists.yaml index 943778f6418..74ed9cadc03 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 diff --git a/packages/openapi/src/__tests__/playlists.spec.ts b/packages/openapi/src/__tests__/playlists.spec.ts index 4e5ed1d69ad..455e316f994 100644 --- a/packages/openapi/src/__tests__/playlists.spec.ts +++ b/packages/openapi/src/__tests__/playlists.spec.ts @@ -62,6 +62,14 @@ describe('Network client', () => { expect(active.status).toBe(200) }) + test('can activate adlib testing mode', async () => { + const active = await playlistsApi.activateAdlibTesting({ + playlistId: playlistIds[0], + rundownId: 'rundownId', + }) + expect(active.status).toBe(200) + }) + let partId = '' test('can move next part in a playlist', async () => { const move = await playlistsApi.moveNextPart({ From 5b0fc8dec8f25a63947fdf53abf5db581777cc79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Mareti=C4=87?= Date: Tue, 10 Feb 2026 10:27:47 +0100 Subject: [PATCH 084/291] fix: tweaks to activate adlib testing mode --- meteor/server/api/rest/v1/playlists.ts | 3 ++- packages/openapi/src/__tests__/playlists.spec.ts | 16 ++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index f3015e41c0d..3af165bfcd5 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -698,9 +698,10 @@ export function registerRoutes(registerRoute: APIRegisterHook) 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}}`) + 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) } ) diff --git a/packages/openapi/src/__tests__/playlists.spec.ts b/packages/openapi/src/__tests__/playlists.spec.ts index 455e316f994..efa545be3a0 100644 --- a/packages/openapi/src/__tests__/playlists.spec.ts +++ b/packages/openapi/src/__tests__/playlists.spec.ts @@ -62,13 +62,17 @@ describe('Network client', () => { expect(active.status).toBe(200) }) - test('can activate adlib testing mode', async () => { - const active = await playlistsApi.activateAdlibTesting({ - playlistId: playlistIds[0], - rundownId: 'rundownId', + if (testServer) { + test('can activate adlib testing mode', async () => { + const active = await playlistsApi.activateAdlibTesting({ + playlistId: playlistIds[0], + rundownId: 'rundownId', + }) + expect(active.status).toBe(200) }) - 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 () => { From ad2410a16c5745c06750dd1978b150a57e9b02f7 Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:40:36 +0000 Subject: [PATCH 085/291] chore(release): 26.3.0-1 --- meteor/CHANGELOG.md | 2 + meteor/package.json | 2 +- meteor/yarn.lock | 18 +++--- packages/blueprints-integration/CHANGELOG.md | 8 +++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 +-- packages/lerna.json | 2 +- packages/live-status-gateway-api/package.json | 2 +- packages/live-status-gateway/package.json | 12 ++-- packages/meteor-lib/package.json | 8 +-- packages/mos-gateway/CHANGELOG.md | 8 +++ packages/mos-gateway/package.json | 6 +- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 8 +++ packages/playout-gateway/package.json | 6 +- packages/server-core-integration/CHANGELOG.md | 8 +++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/webui/package.json | 10 ++-- packages/yarn.lock | 58 +++++++++---------- 22 files changed, 110 insertions(+), 76 deletions(-) diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index 58e28768624..6ad55b67d9d 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,8 @@ 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-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) diff --git a/meteor/package.json b/meteor/package.json index 405e1c3b028..532841b7b48 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "26.3.0-0", + "version": "26.3.0-1", "private": true, "engines": { "node": ">=22.20.0" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index cfca919193f..c5122ba281a 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1158,7 +1158,7 @@ __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:26.3.0-0" + "@sofie-automation/shared-lib": "npm:26.3.0-1" tslib: "npm:^2.8.1" type-fest: "npm:^4.33.0" languageName: node @@ -1194,8 +1194,8 @@ __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:26.3.0-0" - "@sofie-automation/shared-lib": "npm:26.3.0-0" + "@sofie-automation/blueprints-integration": "npm:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-1" fast-clone: "npm:^1.5.13" i18next: "npm:^21.10.0" influx: "npm:^5.9.7" @@ -1227,9 +1227,9 @@ __metadata: 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:26.3.0-0" - "@sofie-automation/corelib": "npm:26.3.0-0" - "@sofie-automation/shared-lib": "npm:26.3.0-0" + "@sofie-automation/blueprints-integration": "npm:26.3.0-1" + "@sofie-automation/corelib": "npm:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-1" amqplib: "npm:^0.10.5" deepmerge: "npm:^4.3.1" elastic-apm-node: "npm:^4.11.0" @@ -1249,9 +1249,9 @@ __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:26.3.0-0" - "@sofie-automation/corelib": "npm:26.3.0-0" - "@sofie-automation/shared-lib": "npm:26.3.0-0" + "@sofie-automation/blueprints-integration": "npm:26.3.0-1" + "@sofie-automation/corelib": "npm:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-1" deep-extend: "npm:0.6.0" semver: "npm:^7.6.3" type-fest: "npm:^4.33.0" diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index 1219a0117eb..d122dcf11ba 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [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) diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 587f64b14a9..7c8050b9ce9 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "26.3.0-0", + "version": "26.3.0-1", "description": "Library to define the interaction between core and the blueprints.", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -36,7 +36,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "26.3.0-0", + "@sofie-automation/shared-lib": "26.3.0-1", "tslib": "^2.8.1", "type-fest": "^4.33.0" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index 52609eee059..ac4b5ec3e6f 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "26.3.0-0", + "version": "26.3.0-1", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -37,8 +37,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "26.3.0-0", - "@sofie-automation/shared-lib": "26.3.0-0", + "@sofie-automation/blueprints-integration": "26.3.0-1", + "@sofie-automation/shared-lib": "26.3.0-1", "fast-clone": "^1.5.13", "i18next": "^21.10.0", "influx": "^5.9.7", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index e62f3fc7bbb..09781a84b9a 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "26.3.0-0", + "version": "26.3.0-1", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index 0dfcf8e422b..d9b664fdb3c 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "26.3.0-0", + "version": "26.3.0-1", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -37,9 +37,9 @@ ], "dependencies": { "@slack/webhook": "^7.0.4", - "@sofie-automation/blueprints-integration": "26.3.0-0", - "@sofie-automation/corelib": "26.3.0-0", - "@sofie-automation/shared-lib": "26.3.0-0", + "@sofie-automation/blueprints-integration": "26.3.0-1", + "@sofie-automation/corelib": "26.3.0-1", + "@sofie-automation/shared-lib": "26.3.0-1", "amqplib": "^0.10.5", "deepmerge": "^4.3.1", "elastic-apm-node": "^4.11.0", diff --git a/packages/lerna.json b/packages/lerna.json index 18b18377307..2dbe1e61dd9 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "26.3.0-0", + "version": "26.3.0-1", "npmClient": "yarn", "$schema": "node_modules/lerna/schemas/lerna-schema.json" } \ No newline at end of file diff --git a/packages/live-status-gateway-api/package.json b/packages/live-status-gateway-api/package.json index e59a8a2e2f3..fb415751779 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": "26.3.0-0", + "version": "26.3.0-1", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index c94be7ebab0..28ed0561b37 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "26.3.0-0", + "version": "26.3.0-1", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -47,11 +47,11 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "26.3.0-0", - "@sofie-automation/corelib": "26.3.0-0", - "@sofie-automation/live-status-gateway-api": "26.3.0-0", - "@sofie-automation/server-core-integration": "26.3.0-0", - "@sofie-automation/shared-lib": "26.3.0-0", + "@sofie-automation/blueprints-integration": "26.3.0-1", + "@sofie-automation/corelib": "26.3.0-1", + "@sofie-automation/live-status-gateway-api": "26.3.0-1", + "@sofie-automation/server-core-integration": "26.3.0-1", + "@sofie-automation/shared-lib": "26.3.0-1", "debug": "^4.4.0", "fast-clone": "^1.5.13", "influx": "^5.9.7", diff --git a/packages/meteor-lib/package.json b/packages/meteor-lib/package.json index 7909b59647b..585a075a611 100644 --- a/packages/meteor-lib/package.json +++ b/packages/meteor-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/meteor-lib", - "version": "26.3.0-0", + "version": "26.3.0-1", "private": true, "description": "Temporary internal library for some types shared by meteor and webui", "main": "dist/index.js", @@ -38,9 +38,9 @@ ], "dependencies": { "@mos-connection/helper": "^5.0.0-alpha.0", - "@sofie-automation/blueprints-integration": "26.3.0-0", - "@sofie-automation/corelib": "26.3.0-0", - "@sofie-automation/shared-lib": "26.3.0-0", + "@sofie-automation/blueprints-integration": "26.3.0-1", + "@sofie-automation/corelib": "26.3.0-1", + "@sofie-automation/shared-lib": "26.3.0-1", "deep-extend": "0.6.0", "semver": "^7.6.3", "type-fest": "^4.33.0", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index 3caa615a20b..bd182b08f8c 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [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) diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index 8d155f417a4..c7f9ae55e7a 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "26.3.0-0", + "version": "26.3.0-1", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -62,8 +62,8 @@ ], "dependencies": { "@mos-connection/connector": "^5.0.0-alpha.0", - "@sofie-automation/server-core-integration": "26.3.0-0", - "@sofie-automation/shared-lib": "26.3.0-0", + "@sofie-automation/server-core-integration": "26.3.0-1", + "@sofie-automation/shared-lib": "26.3.0-1", "tslib": "^2.8.1", "type-fest": "^4.33.0", "underscore": "^1.13.7", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 317dbe267e8..64254a33e29 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "26.3.0-0", + "version": "26.3.0-1", "license": "MIT", "repository": { "type": "git", diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index 108b127488b..1379dc8e350 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [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) diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index f464b15a4f3..3f96eeccfd1 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "26.3.0-0", + "version": "26.3.0-1", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -52,8 +52,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "26.3.0-0", - "@sofie-automation/shared-lib": "26.3.0-0", + "@sofie-automation/server-core-integration": "26.3.0-1", + "@sofie-automation/shared-lib": "26.3.0-1", "debug": "^4.4.0", "influx": "^5.9.7", "timeline-state-resolver": "10.0.0-nightly-release53-20251217-143607-df590aa96.0", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index 61143adee82..baf3103f06a 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [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) diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 442cd123c04..8dc210a789d 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": "26.3.0-0", + "version": "26.3.0-1", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -73,7 +73,7 @@ }, "dependencies": { "@koa/router": "^14.0.0", - "@sofie-automation/shared-lib": "26.3.0-0", + "@sofie-automation/shared-lib": "26.3.0-1", "ejson": "^2.2.3", "faye-websocket": "^0.11.4", "got": "^11.8.6", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 2eb6976979b..dc6c91f23b0 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "26.3.0-0", + "version": "26.3.0-1", "description": "Library for types & values shared by core, workers and gateways", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/webui/package.json b/packages/webui/package.json index 7587a91a84e..01e702a6f08 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -1,7 +1,7 @@ { "name": "@sofie-automation/webui", "private": true, - "version": "26.3.0-0", + "version": "26.3.0-1", "type": "module", "license": "MIT", "repository": { @@ -39,10 +39,10 @@ "@jstarpl/react-contextmenu": "^2.15.1", "@nrk/core-icons": "^9.6.0", "@popperjs/core": "^2.11.8", - "@sofie-automation/blueprints-integration": "26.3.0-0", - "@sofie-automation/corelib": "26.3.0-0", - "@sofie-automation/meteor-lib": "26.3.0-0", - "@sofie-automation/shared-lib": "26.3.0-0", + "@sofie-automation/blueprints-integration": "26.3.0-1", + "@sofie-automation/corelib": "26.3.0-1", + "@sofie-automation/meteor-lib": "26.3.0-1", + "@sofie-automation/shared-lib": "26.3.0-1", "@sofie-automation/sorensen": "^1.5.11", "@testing-library/user-event": "^14.6.1", "@types/sinon": "^10.0.20", diff --git a/packages/yarn.lock b/packages/yarn.lock index 813fdec8d1f..417781df9b8 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -6862,11 +6862,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@npm:26.3.0-0, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@sofie-automation/blueprints-integration@npm:26.3.0-1, @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:26.3.0-0" + "@sofie-automation/shared-lib": "npm:26.3.0-1" tslib: "npm:^2.8.1" type-fest: "npm:^4.33.0" languageName: unknown @@ -6898,12 +6898,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@npm:26.3.0-0, @sofie-automation/corelib@workspace:corelib": +"@sofie-automation/corelib@npm:26.3.0-1, @sofie-automation/corelib@workspace:corelib": version: 0.0.0-use.local resolution: "@sofie-automation/corelib@workspace:corelib" dependencies: - "@sofie-automation/blueprints-integration": "npm:26.3.0-0" - "@sofie-automation/shared-lib": "npm:26.3.0-0" + "@sofie-automation/blueprints-integration": "npm:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-1" fast-clone: "npm:^1.5.13" i18next: "npm:^21.10.0" influx: "npm:^5.9.7" @@ -6935,9 +6935,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": "npm:^7.0.4" - "@sofie-automation/blueprints-integration": "npm:26.3.0-0" - "@sofie-automation/corelib": "npm:26.3.0-0" - "@sofie-automation/shared-lib": "npm:26.3.0-0" + "@sofie-automation/blueprints-integration": "npm:26.3.0-1" + "@sofie-automation/corelib": "npm:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-1" amqplib: "npm:^0.10.5" deepmerge: "npm:^4.3.1" elastic-apm-node: "npm:^4.11.0" @@ -6955,7 +6955,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/live-status-gateway-api@npm:26.3.0-0, @sofie-automation/live-status-gateway-api@workspace:live-status-gateway-api": +"@sofie-automation/live-status-gateway-api@npm:26.3.0-1, @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: @@ -6970,14 +6970,14 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/meteor-lib@npm:26.3.0-0, @sofie-automation/meteor-lib@workspace:meteor-lib": +"@sofie-automation/meteor-lib@npm:26.3.0-1, @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:26.3.0-0" - "@sofie-automation/corelib": "npm:26.3.0-0" - "@sofie-automation/shared-lib": "npm:26.3.0-0" + "@sofie-automation/blueprints-integration": "npm:26.3.0-1" + "@sofie-automation/corelib": "npm:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-1" "@types/deep-extend": "npm:^0.6.2" "@types/semver": "npm:^7.5.8" "@types/underscore": "npm:^1.13.0" @@ -7004,12 +7004,12 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@npm:26.3.0-0, @sofie-automation/server-core-integration@workspace:server-core-integration": +"@sofie-automation/server-core-integration@npm:26.3.0-1, @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:26.3.0-0" + "@sofie-automation/shared-lib": "npm:26.3.0-1" "@types/koa": "npm:^3.0.0" "@types/koa__router": "npm:^12.0.4" ejson: "npm:^2.2.3" @@ -7021,7 +7021,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@npm:26.3.0-0, @sofie-automation/shared-lib@workspace:shared-lib": +"@sofie-automation/shared-lib@npm:26.3.0-1, @sofie-automation/shared-lib@workspace:shared-lib": version: 0.0.0-use.local resolution: "@sofie-automation/shared-lib@workspace:shared-lib" dependencies: @@ -7053,10 +7053,10 @@ __metadata: "@jstarpl/react-contextmenu": "npm:^2.15.1" "@nrk/core-icons": "npm:^9.6.0" "@popperjs/core": "npm:^2.11.8" - "@sofie-automation/blueprints-integration": "npm:26.3.0-0" - "@sofie-automation/corelib": "npm:26.3.0-0" - "@sofie-automation/meteor-lib": "npm:26.3.0-0" - "@sofie-automation/shared-lib": "npm:26.3.0-0" + "@sofie-automation/blueprints-integration": "npm:26.3.0-1" + "@sofie-automation/corelib": "npm:26.3.0-1" + "@sofie-automation/meteor-lib": "npm:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-1" "@sofie-automation/sorensen": "npm:^1.5.11" "@testing-library/dom": "npm:^10.4.0" "@testing-library/jest-dom": "npm:^6.6.3" @@ -19479,11 +19479,11 @@ 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:26.3.0-0" - "@sofie-automation/corelib": "npm:26.3.0-0" - "@sofie-automation/live-status-gateway-api": "npm:26.3.0-0" - "@sofie-automation/server-core-integration": "npm:26.3.0-0" - "@sofie-automation/shared-lib": "npm:26.3.0-0" + "@sofie-automation/blueprints-integration": "npm:26.3.0-1" + "@sofie-automation/corelib": "npm:26.3.0-1" + "@sofie-automation/live-status-gateway-api": "npm:26.3.0-1" + "@sofie-automation/server-core-integration": "npm:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-1" debug: "npm:^4.4.0" fast-clone: "npm:^1.5.13" influx: "npm:^5.9.7" @@ -21552,8 +21552,8 @@ 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:26.3.0-0" - "@sofie-automation/shared-lib": "npm:26.3.0-0" + "@sofie-automation/server-core-integration": "npm:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-1" tslib: "npm:^2.8.1" type-fest: "npm:^4.33.0" underscore: "npm:^1.13.7" @@ -23919,8 +23919,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": "npm:26.3.0-0" - "@sofie-automation/shared-lib": "npm:26.3.0-0" + "@sofie-automation/server-core-integration": "npm:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-1" debug: "npm:^4.4.0" influx: "npm:^5.9.7" timeline-state-resolver: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" From 9502cc5bc0b445cd7682becfb5d9d943c3c99b58 Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 11 Feb 2026 13:26:43 +0100 Subject: [PATCH 086/291] fix: Infinites timetravel (#1611) --- packages/job-worker/src/__mocks__/context.ts | 16 +- .../__snapshots__/timeline.test.ts.snap | 72 +- .../src/playout/__tests__/timeline.test.ts | 2406 ++++++++++------- .../__snapshots__/rundown.test.ts.snap | 24 +- .../timeline/__tests__/rundown.test.ts | 36 +- .../src/playout/timeline/generate.ts | 7 +- .../src/playout/timeline/rundown.ts | 14 +- 7 files changed, 1521 insertions(+), 1054 deletions(-) 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/playout/__tests__/__snapshots__/timeline.test.ts.snap b/packages/job-worker/src/playout/__tests__/__snapshots__/timeline.test.ts.snap index e0de2f4a7ea..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://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__/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/timeline/__tests__/__snapshots__/rundown.test.ts.snap b/packages/job-worker/src/playout/timeline/__tests__/__snapshots__/rundown.test.ts.snap index 5790c8a5eae..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 @@ -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__/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..2ce72fd0a6b 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -378,7 +378,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) diff --git a/packages/job-worker/src/playout/timeline/rundown.ts b/packages/job-worker/src/playout/timeline/rundown.ts index a473ed066fc..e4ad4ea7b08 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 @@ -360,10 +364,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 From 62659c60f81e41d69cf071b4b1ee9a06a8e50fa7 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 11 Feb 2026 12:51:24 +0000 Subject: [PATCH 087/291] Add lint:fix command to package.json To enable single command lint with fixes that is easy to remember / lookup --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 9c1fed97bae..f7c0f8237cc 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", From 22916fd9bb1a87f5d48cc8a312c97babbf7f4668 Mon Sep 17 00:00:00 2001 From: Krzysztof Zegzula Date: Wed, 11 Feb 2026 15:38:07 +0100 Subject: [PATCH 088/291] feat(PrompterView): customizable shuttle webhid buttons (#1610) * feat(EAV-372): customizable shuttle webhid buttons an array of keyIndex:actionId can be provided in shuttleWebHid_buttonMap * feat(EAV-372): execute actions also on shuttle button release * chore(EAV-372): improve log message --- meteor/server/api/userActions.ts | 6 +-- packages/corelib/src/worker/studio.ts | 4 +- .../docs/user-guide/features/prompter.md | 15 ++++++++ .../job-worker/src/playout/adlibAction.ts | 36 ++++++++++-------- packages/meteor-lib/src/api/userActions.ts | 4 +- .../src/client/ui/Prompter/PrompterView.tsx | 15 ++++++++ .../customizable-shuttle-webhid-device.ts | 38 +++++++++++++++++++ .../client/ui/Prompter/controller/manager.ts | 4 +- .../controller/shuttle-webhid-device.ts | 7 +++- 9 files changed, 104 insertions(+), 25 deletions(-) create mode 100644 packages/webui/src/client/ui/Prompter/controller/customizable-shuttle-webhid-device.ts diff --git a/meteor/server/api/userActions.ts b/meteor/server/api/userActions.ts index 96b3a66a6c6..83eae98dd4f 100644 --- a/meteor/server/api/userActions.ts +++ b/meteor/server/api/userActions.ts @@ -399,9 +399,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 +411,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/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 6eb045fc5e0..6f8dda2b17b 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -266,9 +266,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 } } diff --git a/packages/documentation/docs/user-guide/features/prompter.md b/packages/documentation/docs/user-guide/features/prompter.md index aba0e34ec04..7440cbb5334 100644 --- a/packages/documentation/docs/user-guide/features/prompter.md +++ b/packages/documentation/docs/user-guide/features/prompter.md @@ -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_\) diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index e2dc816487f..e4d14925f7f 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, 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 }, - }), - ]) - 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, diff --git a/packages/meteor-lib/src/api/userActions.ts b/packages/meteor-lib/src/api/userActions.ts index fd1a07347ef..345589da062 100644 --- a/packages/meteor-lib/src/api/userActions.ts +++ b/packages/meteor-lib/src/api/userActions.ts @@ -121,9 +121,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/webui/src/client/ui/Prompter/PrompterView.tsx b/packages/webui/src/client/ui/Prompter/PrompterView.tsx index 5af6906c27b..417880960c3 100644 --- a/packages/webui/src/client/ui/Prompter/PrompterView.tsx +++ b/packages/webui/src/client/ui/Prompter/PrompterView.tsx @@ -66,6 +66,7 @@ interface PrompterConfig { xbox_speedMap?: number[] xbox_reverseSpeedMap?: number[] xbox_triggerDeadZone?: number + shuttleWebHid_buttonMap?: string[] marker?: 'center' | 'top' | 'bottom' | 'hide' showMarker: boolean showScroll: boolean @@ -192,6 +193,7 @@ export class PrompterViewContent extends React.Component + MeteorCall.userAction.executeAction(e, ts, playlist._id, null, actionId, null, triggerMode) + ) + } + private onWindowScroll = () => { this.triggerCheckCurrentTakeMarkers() } 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 From 8c10a53de71fc9ea54cda525224ae51cffe54b80 Mon Sep 17 00:00:00 2001 From: Krzysztof Zegzula Date: Wed, 11 Feb 2026 15:39:18 +0100 Subject: [PATCH 089/291] feat: allow blueprints to specify preview and thumbnail containter ids in `applyConfig` (#1613) * feat(EAV-663): allow blueprints to specify preview and thumbnail containter ids in `applyConfig` * refactor(EAV-663): rename `packageContainerIds` to `packageContainerSettings` * refactor(EAV-663): move migration step to version folder * chore(EAV-667): remove `console.log` and lint * fix(EAV-663): add `packageContainerSettings` defaults in case blueprints don't provide them anymore If blueprints don't provide packageContainerSettings anymore, reset the defaults to a default, so that stale defaults previously returned by the blueprints are not kept forever --- meteor/__mocks__/defaultCollectionObjects.ts | 6 +- meteor/server/__tests__/cronjobs.test.ts | 6 +- meteor/server/api/rest/v1/typeConversion.ts | 6 +- meteor/server/api/studio/api.ts | 6 +- meteor/server/migration/0_1_0.ts | 6 +- meteor/server/migration/X_X_X.ts | 8 +- .../migration/__tests__/migrations.test.ts | 136 +++++++----------- meteor/server/migration/databaseMigration.ts | 14 +- ...erIdsToObjectWithOverridesMigrationStep.ts | 54 +++++++ ...ToObjectWithOverridesMigrationStep.test.ts | 73 ++++++++++ .../expectedPackages/generate.ts | 20 +-- .../expectedPackages/publication.ts | 16 ++- .../__tests__/checkPieceContentStatus.test.ts | 6 +- .../checkPieceContentStatus.ts | 14 +- .../pieceContentStatusUI/common.ts | 9 +- .../blueprints-integration/src/api/studio.ts | 7 +- packages/corelib/src/dataModel/Studio.ts | 10 +- .../src/__mocks__/defaultCollectionObjects.ts | 6 +- packages/job-worker/src/playout/upgrade.ts | 6 + .../src/collections/ExpectedPackages.ts | 8 +- .../src/core/model/PackageContainer.ts | 5 + .../src/__mocks__/defaultCollectionObjects.ts | 6 +- .../PackageContainerPickers.tsx | 113 +++++++++++---- 23 files changed, 366 insertions(+), 175 deletions(-) create mode 100644 meteor/server/migration/steps/X_X_X/ContainerIdsToObjectWithOverridesMigrationStep.ts create mode 100644 meteor/server/migration/steps/X_X_X/__tests__/ContainerIdsToObjectWithOverridesMigrationStep.test.ts diff --git a/meteor/__mocks__/defaultCollectionObjects.ts b/meteor/__mocks__/defaultCollectionObjects.ts index fa8a8934edb..bf9e8fd75ed 100644 --- a/meteor/__mocks__/defaultCollectionObjects.ts +++ b/meteor/__mocks__/defaultCollectionObjects.ts @@ -116,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/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index c61e36bdcb7..a8e972a2081 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -592,8 +592,10 @@ describe('cronjobs', () => { routeSetsWithOverrides: newObjectWithOverrides({}), routeSetExclusivityGroupsWithOverrides: newObjectWithOverrides({}), packageContainersWithOverrides: newObjectWithOverrides({}), - previewContainerIds: [], - thumbnailContainerIds: [], + packageContainerSettingsWithOverrides: newObjectWithOverrides({ + previewContainerIds: [], + thumbnailContainerIds: [], + }), peripheralDeviceSettings: { deviceSettings: newObjectWithOverrides({}), ingestDevices: newObjectWithOverrides({}), diff --git a/meteor/server/api/rest/v1/typeConversion.ts b/meteor/server/api/rest/v1/typeConversion.ts index c1737f613e2..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({}), 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/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/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 30a74d769e1..5aa78a3370c 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -10,6 +10,7 @@ import { } 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 { ContainerIdsToObjectWithOverridesMigrationStep } from './steps/X_X_X/ContainerIdsToObjectWithOverridesMigrationStep' /* * ************************************************************************************** @@ -36,7 +37,9 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ ['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 `There are ${collectionsToDrop.length} obsolete collections to be removed: ${collectionsToDrop + .map((c) => c.name) + .join(', ')}` } return false @@ -195,4 +198,7 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ } }, }, + // Add your migration here + + new ContainerIdsToObjectWithOverridesMigrationStep(), ]) diff --git a/meteor/server/migration/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index 5252a280818..e41ead05a1c 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -1,18 +1,17 @@ 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, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' 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 @@ -107,35 +106,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 +121,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 +136,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 +240,48 @@ 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, + userInput(migrationStatus) + ) + + 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/databaseMigration.ts b/meteor/server/migration/databaseMigration.ts index eed1dc39482..f2d54d9ef20 100644 --- a/meteor/server/migration/databaseMigration.ts +++ b/meteor/server/migration/databaseMigration.ts @@ -82,10 +82,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) } } } @@ -135,9 +133,9 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise { + 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/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/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 c4df7cf748b..d4400907338 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,10 +211,8 @@ export type PieceContentStatusPiece = Pick< */ previousPieceInstanceId?: PieceInstanceId } -export interface PieceContentStatusStudio extends Pick< - DBStudio, - '_id' | 'previewContainerIds' | 'thumbnailContainerIds' -> { +export interface PieceContentStatusStudio { + _id: StudioId /** Mappings between the physical devices / outputs and logical ones */ mappings: MappingsExt /** Route sets with overrides */ @@ -224,6 +222,8 @@ export interface PieceContentStatusStudio extends Pick< */ packageContainers: Record + packageContainerSettings: StudioPackageContainerSettings + settings: IStudioSettings } @@ -709,7 +709,7 @@ async function checkPieceContentExpectedPackageStatus( } if (!thumbnailUrl) { - const sideEffect = getSideEffect(expectedPackage, studio) + const sideEffect = getSideEffect(expectedPackage, studio.packageContainerSettings) thumbnailUrl = await getAssetUrlFromPackageContainerStatus( studio.packageContainers, @@ -721,7 +721,7 @@ async function checkPieceContentExpectedPackageStatus( } if (!previewUrl) { - const sideEffect = getSideEffect(expectedPackage, studio) + const sideEffect = getSideEffect(expectedPackage, studio.packageContainerSettings) previewUrl = await getAssetUrlFromPackageContainerStatus( studio.packageContainers, 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 /** Package Containers */ packageContainers?: Record + /** Which Package Containers are used for media previews/thumbnails in GUI */ + packageContainerSettings?: StudioPackageContainerSettings studioSettings?: IStudioSettings } 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/job-worker/src/__mocks__/defaultCollectionObjects.ts b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts index 88869a4da84..9f194ce196b 100644 --- a/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/job-worker/src/__mocks__/defaultCollectionObjects.ts @@ -111,8 +111,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/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/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/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/webui/src/__mocks__/defaultCollectionObjects.ts b/packages/webui/src/__mocks__/defaultCollectionObjects.ts index 7434f499fbf..f27e2de36ac 100644 --- a/packages/webui/src/__mocks__/defaultCollectionObjects.ts +++ b/packages/webui/src/__mocks__/defaultCollectionObjects.ts @@ -111,8 +111,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/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) => ( + -
-
+ )} +
) } From af8a7d19accdc74d3cc5909b1afefd3efeeb755c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 11 Feb 2026 16:51:15 +0000 Subject: [PATCH 090/291] fix: missed projection values when executing adlib action (#1648) --- .../job-worker/src/playout/adlibAction.ts | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index fa85af74050..e96210ae93e 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -35,6 +35,9 @@ import type { INoteBase } from '@sofie-automation/corelib/dist/dataModel/Notes' import { NotificationsModelHelper } from '../notifications/NotificationsModelHelper.js' import type { INotificationsModel } from '../notifications/NotificationsModel.js' import { PersistentPlayoutStateStore } from '../blueprints/context/services/PersistantStateStore.js' +import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' +import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' +import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' /** * Execute an AdLib Action @@ -78,17 +81,39 @@ export async function executeAdlibActionAndSaveModel( const [adLibAction, baselineAdLibAction, bucketAdLibAction] = await Promise.all([ context.directCollections.AdLibActions.findOne(data.actionDocId as AdLibActionId, { - projection: { _id: 1, privateData: 1 }, - }), + projection: { + _id: 1, + privateData: 1, + publicData: 1, + invalid: 1, + rundownId: 1, + }, + }) as Promise | undefined>, context.directCollections.RundownBaselineAdLibActions.findOne( data.actionDocId as RundownBaselineAdLibActionId, { - projection: { _id: 1, privateData: 1 }, + projection: { + _id: 1, + privateData: 1, + publicData: 1, + invalid: 1, + rundownId: 1, + }, } - ), + ) as Promise< + Pick | undefined + >, context.directCollections.BucketAdLibActions.findOne(data.actionDocId as BucketAdLibActionId, { - projection: { _id: 1, privateData: 1 }, - }), + projection: { + _id: 1, + privateData: 1, + publicData: 1, + invalid: 1, + bucketId: 1, + }, + }) as Promise< + Pick | undefined + >, ]) const adLibActionDoc = adLibAction ?? baselineAdLibAction ?? bucketAdLibAction From d1e0ba1dddab7b6e9ef22d5119a1e4d1d62f4a20 Mon Sep 17 00:00:00 2001 From: ianshade Date: Thu, 12 Feb 2026 00:38:16 +0100 Subject: [PATCH 091/291] chore(EAV-737): update `onRundownActivate` and `onRundownDeactivate` comments --- packages/blueprints-integration/src/api/showStyle.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 43182638f38..8c390d876d2 100644 --- a/packages/blueprints-integration/src/api/showStyle.ts +++ b/packages/blueprints-integration/src/api/showStyle.ts @@ -199,12 +199,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 */ From 261899c6eacc687a5bb55514ed2235af146641d2 Mon Sep 17 00:00:00 2001 From: Krzysztof Zegzula Date: Thu, 12 Feb 2026 00:49:00 +0100 Subject: [PATCH 092/291] fix(PrompterView): prevent jumping/scrolling to undesired positions (#1615) * fix(EAV-693): prevent jumping when script is continued as an infinite into another part * fix(EAV-693): freeze prompter content during scrollTo this prevents from restoring scroll anchors during pending animations, which led to the content stopping at an undesired position * refactor(EAV-693): improve context typing --- .../src/client/ui/Prompter/PrompterView.tsx | 172 ++++++++++++++---- .../webui/src/client/ui/Prompter/prompter.ts | 13 +- 2 files changed, 149 insertions(+), 36 deletions(-) diff --git a/packages/webui/src/client/ui/Prompter/PrompterView.tsx b/packages/webui/src/client/ui/Prompter/PrompterView.tsx index 417880960c3..dd10e81a2ba 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' @@ -39,6 +39,9 @@ import { MeteorCall } from '../../lib/meteorApi.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 @@ -120,7 +123,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 @@ -263,6 +283,9 @@ 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 @@ -385,14 +412,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][] { @@ -657,12 +684,14 @@ export function PrompterView(props: Readonly): JSX.Element { ) return ( - + + + ) } @@ -679,6 +708,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 @@ -737,6 +767,9 @@ const PrompterContent = withTranslation()( Translated & IPrompterTrackedProps>, {} > { + static contextType = PrompterStoreContext + declare context: PrompterStoreRef + private _debounceUpdate: NodeJS.Timeout | undefined constructor(props: Translated & IPrompterTrackedProps>) { @@ -765,10 +798,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. */ @@ -790,29 +822,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, + }) } } @@ -838,7 +884,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() @@ -874,7 +932,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(', ')}` ) @@ -959,6 +1017,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() } @@ -1006,15 +1073,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'}
@@ -1022,32 +1089,67 @@ 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) { + let text = 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 || ''} + {text}
) } diff --git a/packages/webui/src/client/ui/Prompter/prompter.ts b/packages/webui/src/client/ui/Prompter/prompter.ts index f577563c68f..47a7c2114cf 100644 --- a/packages/webui/src/client/ui/Prompter/prompter.ts +++ b/packages/webui/src/client/ui/Prompter/prompter.ts @@ -56,6 +56,8 @@ export interface PrompterDataPart { export interface PrompterDataPiece { id: PieceId text: string + continuationOf?: PieceId + startPartId?: PartId | null } export interface PrompterData { title: string @@ -264,7 +266,16 @@ 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, + continuationOf: piece._id, + startPartId: piece.startPartId, + }) + continue + } piecesIncluded.push(piece._id) partData.pieces.push({ From fc72f023ec1bc9a2466a4ddd3d012bbe189e985c Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 8 Jan 2026 02:23:39 +0100 Subject: [PATCH 093/291] SOFIE-295 | add informative REST results for TAKE failures --- meteor/server/api/rest/v1/playlists.ts | 6 +-- meteor/server/lib/rest/v1/playlists.ts | 4 +- packages/corelib/src/worker/studio.ts | 6 ++- packages/job-worker/src/playout/take.ts | 43 +++++++++++++--- .../src/api/__tests__/client.test.ts | 18 +++++++ packages/meteor-lib/src/api/client.ts | 9 +++- packages/meteor-lib/src/api/userActions.ts | 8 ++- .../openapi/api/definitions/playlists.yaml | 51 ++++++++++++++++++- 8 files changed, 127 insertions(+), 18 deletions(-) diff --git a/meteor/server/api/rest/v1/playlists.ts b/meteor/server/api/rest/v1/playlists.ts index 3af165bfcd5..cdd3e69174b 100644 --- a/meteor/server/api/rest/v1/playlists.ts +++ b/meteor/server/api/rest/v1/playlists.ts @@ -32,7 +32,7 @@ import { } 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' @@ -559,7 +559,7 @@ class PlaylistsServerAPI implements PlaylistsRestAPI { event: string, rundownPlaylistId: RundownPlaylistId, fromPartInstanceId: PartInstanceId | undefined - ): Promise> { + ): Promise> { triggerWriteAccess() const playlist = await this.findPlaylist(rundownPlaylistId) @@ -923,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/lib/rest/v1/playlists.ts b/meteor/server/lib/rest/v1/playlists.ts index 227f161ba67..778ff2b04d2 100644 --- a/meteor/server/lib/rest/v1/playlists.ts +++ b/meteor/server/lib/rest/v1/playlists.ts @@ -11,7 +11,7 @@ import { 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' /* ************************************************************************* @@ -238,7 +238,7 @@ export interface PlaylistsRestAPI { event: string, rundownPlaylistId: RundownPlaylistId, fromPartInstanceId: PartInstanceId | undefined - ): Promise> + ): Promise> /** * Clears the specified SourceLayers. * diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index 6f8dda2b17b..ab14afd4333 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -380,6 +380,10 @@ export interface CleanupOrphanedExpectedPackageReferencesProps { rundownId: RundownId } +export interface TakeNextPartResult { + nextTakeTime: number +} + /** * Set of valid functions, of form: * `id: (data) => return` @@ -404,7 +408,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 diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index c9a2dd32b97..dd103379fba 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) } } diff --git a/packages/meteor-lib/src/api/__tests__/client.test.ts b/packages/meteor-lib/src/api/__tests__/client.test.ts index 969ed62ec53..0a5f35574c0 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 nextAllowedTakeTime from error args', () => { + const error = ClientAPI.responseError( + UserError.create( + UserErrorMessage.TakeRateLimit, + { + duration: 1000, + nextAllowedTakeTime: 1234567890, + }, + 429 + ) + ) + expect(error.nextAllowedTakeTime).toBe(1234567890) + expect(error.errorCode).toBe(429) + }) + it('Does not include nextAllowedTakeTime when not in args', () => { + const error = ClientAPI.responseError(UserError.create(UserErrorMessage.InactiveRundown)) + expect(error.nextAllowedTakeTime).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..69bf618797c 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 + /** For blocked TAKE operations, the next allowed take time (Unix timestamp ms) */ + nextAllowedTakeTime?: number } /** @@ -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 nextAllowedTakeTime = userError.userMessage.args?.nextAllowedTakeTime as number | undefined + return { + error: UserError.serialize(userError), + errorCode: userError.errorCode, + ...(nextAllowedTakeTime !== undefined && { nextAllowedTakeTime }), + } } export interface ClientResponseSuccess { /** On success, return success code (by default, use 200) */ diff --git a/packages/meteor-lib/src/api/userActions.ts b/packages/meteor-lib/src/api/userActions.ts index 345589da062..abb46308f9d 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,7 +38,7 @@ export interface NewUserActionAPI { eventTime: Time, rundownPlaylistId: RundownPlaylistId, fromPartInstanceId: PartInstanceId | null - ): Promise> + ): Promise> setNext( userEvent: string, eventTime: Time, diff --git a/packages/openapi/api/definitions/playlists.yaml b/packages/openapi/api/definitions/playlists.yaml index 74ed9cadc03..ef43141571b 100644 --- a/packages/openapi/api/definitions/playlists.yaml +++ b/packages/openapi/api/definitions/playlists.yaml @@ -589,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: @@ -605,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 + nextAllowedTakeTime: + type: number + description: Unix timestamp (ms) of when the next take will be allowed. + example: 1707024000000 + 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) + nextAllowedTakeTime: + type: number + description: Unix timestamp (ms) of when the next take will be allowed. + example: 1707024000000 500: $ref: '#/components/responses/internalServerError' From eacf9aa8f3abde0914fb17ae947af8bdaa22b83d Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 12 Feb 2026 04:13:37 +0100 Subject: [PATCH 094/291] SOFIE-295 | make the api error more generic --- meteor/server/api/rest/v1/index.ts | 6 +++++- .../meteor-lib/src/api/__tests__/client.test.ts | 8 ++++---- packages/meteor-lib/src/api/client.ts | 8 ++++---- packages/openapi/api/definitions/playlists.yaml | 16 ++++++++-------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/meteor/server/api/rest/v1/index.ts b/meteor/server/api/rest/v1/index.ts index 1f12912170d..ab2908aab57 100644 --- a/meteor/server/api/rest/v1/index.ts +++ b/meteor/server/api/rest/v1/index.ts @@ -117,6 +117,7 @@ interface APIRequestError { status: number message: string details?: string[] + additionalInfo?: Record } function sofieAPIRequest( @@ -133,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) @@ -144,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 }) @@ -176,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 } diff --git a/packages/meteor-lib/src/api/__tests__/client.test.ts b/packages/meteor-lib/src/api/__tests__/client.test.ts index 0a5f35574c0..0312d360cb0 100644 --- a/packages/meteor-lib/src/api/__tests__/client.test.ts +++ b/packages/meteor-lib/src/api/__tests__/client.test.ts @@ -50,7 +50,7 @@ describe('ClientAPI', () => { }) } }) - it('Extracts nextAllowedTakeTime from error args', () => { + it('Extracts additionalInfo from error args', () => { const error = ClientAPI.responseError( UserError.create( UserErrorMessage.TakeRateLimit, @@ -61,12 +61,12 @@ describe('ClientAPI', () => { 429 ) ) - expect(error.nextAllowedTakeTime).toBe(1234567890) + expect(error.additionalInfo).toEqual({ duration: 1000, nextAllowedTakeTime: 1234567890 }) expect(error.errorCode).toBe(429) }) - it('Does not include nextAllowedTakeTime when not in args', () => { + it('Does not include additionalInfo when no args', () => { const error = ClientAPI.responseError(UserError.create(UserErrorMessage.InactiveRundown)) - expect(error.nextAllowedTakeTime).toBeUndefined() + expect(error.additionalInfo).toBeUndefined() }) describe('isClientResponseSuccess', () => { it('Correctly recognizes a responseSuccess object', () => { diff --git a/packages/meteor-lib/src/api/client.ts b/packages/meteor-lib/src/api/client.ts index 69bf618797c..1fcfa54f7a2 100644 --- a/packages/meteor-lib/src/api/client.ts +++ b/packages/meteor-lib/src/api/client.ts @@ -50,8 +50,8 @@ export namespace ClientAPI { errorCode: number /** On error, provide a human-readable error message */ error: SerializedUserError - /** For blocked TAKE operations, the next allowed take time (Unix timestamp ms) */ - nextAllowedTakeTime?: number + /** Optional additional information about the error, forwarded from UserError args */ + additionalInfo?: Record } /** @@ -61,11 +61,11 @@ export namespace ClientAPI { * @returns A `ClientResponseError` object containing the error and the resolved error code. */ export function responseError(userError: UserError): ClientResponseError { - const nextAllowedTakeTime = userError.userMessage.args?.nextAllowedTakeTime as number | undefined + const args = userError.userMessage.args return { error: UserError.serialize(userError), errorCode: userError.errorCode, - ...(nextAllowedTakeTime !== undefined && { nextAllowedTakeTime }), + ...(args !== undefined && Object.keys(args).length > 0 && { additionalInfo: args }), } } export interface ClientResponseSuccess { diff --git a/packages/openapi/api/definitions/playlists.yaml b/packages/openapi/api/definitions/playlists.yaml index ef43141571b..cdb34f65a26 100644 --- a/packages/openapi/api/definitions/playlists.yaml +++ b/packages/openapi/api/definitions/playlists.yaml @@ -633,10 +633,10 @@ resources: message: type: string example: Cannot take during a transition - nextAllowedTakeTime: - type: number - description: Unix timestamp (ms) of when the next take will be allowed. - example: 1707024000000 + 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: @@ -650,10 +650,10 @@ resources: message: type: string example: Ignoring TAKES that are too quick after eachother (1000 ms) - nextAllowedTakeTime: - type: number - description: Unix timestamp (ms) of when the next take will be allowed. - example: 1707024000000 + additionalInfo: + type: object + description: Additional error details, e.g. includes nextAllowedTakeTime (Unix timestamp ms) for blocked takes. + additionalProperties: true 500: $ref: '#/components/responses/internalServerError' From 49a020a699441f1d4add8e2e86c4fb7022a1e2b2 Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Thu, 12 Feb 2026 17:50:25 +0000 Subject: [PATCH 095/291] Fix objectWithOverrides to handle arrays --- .../__tests__/objectWithOverrides.spec.ts | 4 ++ .../src/settings/objectWithOverrides.ts | 41 +++++++++++-------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts b/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts index c4ec6781621..3bc0bed71ab 100644 --- a/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts +++ b/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts @@ -159,6 +159,7 @@ describe('applyAndValidateOverrides', () => { valC: 5, valD: 'xyz', }, + valE: [{ valF: 27, valG: 'hij' }], } const inputObjWithOverrides: ObjectWithOverrides = { @@ -172,6 +173,7 @@ describe('applyAndValidateOverrides', () => { valC: 6, valD: 'uvw', }, + valE: [{ valF: 32, valG: 'klm' }], } const res = updateOverrides(inputObjWithOverrides, updateObj) @@ -185,11 +187,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)) { From 66294b75fc854dd5799a44620bf557d34453a7f9 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 12 Feb 2026 16:18:56 +0000 Subject: [PATCH 096/291] Add docs for 1.52.0 and 26.03.0 By copying from release52 branch and 26.3.0 release tags --- .../version-1.52.0/about-sofie.md | 22 + .../for-developers/api-documentation.md | 8 + .../for-developers/api-stability.md | 26 ++ .../for-developers/contribution-guidelines.md | 108 +++++ .../for-developers/data-model.md | 132 ++++++ .../device-integrations/_category_.json | 4 + .../device-integrations/intro.md | 18 + .../options-and-mappings.md | 11 + .../device-integrations/tsr-actions.md | 11 + .../device-integrations/tsr-api.md | 28 ++ .../device-integrations/tsr-types.md | 7 + .../for-blueprint-developers/_category_.json | 4 + .../_part-timings-demo.jsx | 173 +++++++ .../for-blueprint-developers/ab-playback.md | 236 ++++++++++ .../for-blueprint-developers/hold.md | 52 +++ .../for-blueprint-developers/intro.md | 20 + .../for-blueprint-developers/lookahead.md | 96 ++++ .../manipulating-ingest-data.md | 139 ++++++ .../part-and-piece-timings.mdx | 141 ++++++ .../sync-ingest-changes.md | 23 + .../timeline-datastore.md | 85 ++++ .../version-1.52.0/for-developers/intro.md | 15 + .../for-developers/json-config-schema.md | 218 +++++++++ .../for-developers/libraries.md | 56 +++ .../for-developers/mos-plugins.md | 185 ++++++++ .../for-developers/npm-package-publishing.md | 23 + .../for-developers/publications.md | 43 ++ .../for-developers/url-query-parameters.md | 25 + .../worker-threads-and-locks.md | 61 +++ .../user-guide/concepts-and-architecture.md | 192 ++++++++ .../user-guide/configuration/_category_.json | 4 + .../user-guide/configuration/settings-view.md | 181 ++++++++ .../configuration/sofie-core-settings.md | 110 +++++ .../version-1.52.0/user-guide/faq.md | 16 + .../user-guide/features/_category_.json | 4 + .../user-guide/features/access-levels.md | 64 +++ .../version-1.52.0/user-guide/features/api.md | 19 + .../user-guide/features/language.md | 23 + .../user-guide/features/prompter.md | 199 ++++++++ .../user-guide/features/sofie-views.mdx | 333 +++++++++++++ .../user-guide/features/system-health.md | 27 ++ .../user-guide/further-reading.md | 59 +++ .../user-guide/installation/_category_.json | 4 + .../installation/initial-sofie-core-setup.md | 23 + .../installing-a-gateway/_category_.json | 4 + .../installing-a-gateway/intro.md | 25 + .../installing-a-gateway/playout-gateway.md | 6 + .../_category_.json | 4 + .../inews-gateway.md | 12 + ...g-sofie-with-google-spreadsheet-support.md | 46 ++ .../intro.md | 17 + .../mos-gateway.md | 9 + .../installation/installing-blueprints.md | 46 ++ .../README.md | 35 ++ .../_category_.json | 4 + .../casparcg-server-installation.md | 224 +++++++++ .../ffmpeg-installation.md | 35 ++ .../vision-mixers.md | 14 + .../installation/installing-input-gateway.md | 45 ++ .../installing-package-manager.md | 210 +++++++++ .../installing-sofie-server-core.md | 172 +++++++ .../user-guide/installation/intro.md | 37 ++ .../user-guide/installation/media-manager.md | 20 + .../user-guide/installation/rundown-editor.md | 18 + .../version-1.52.0/user-guide/intro.md | 41 ++ .../user-guide/supported-devices.md | 119 +++++ .../version-26.03.0/about-sofie.md | 20 + .../for-developers/api-documentation.md | 8 + .../for-developers/api-stability.md | 26 ++ .../for-developers/contribution-guidelines.md | 118 +++++ .../for-developers/data-model.md | 130 ++++++ .../device-integrations/_category_.json | 4 + .../device-integrations/intro.md | 18 + .../options-and-mappings.md | 11 + .../shared-hardware-control.md | 68 +++ .../device-integrations/tsr-actions.md | 11 + .../device-integrations/tsr-api.md | 28 ++ .../device-integrations/tsr-plugins.md | 124 +++++ .../device-integrations/tsr-types.md | 7 + .../for-blueprint-developers/_category_.json | 4 + .../_part-timings-demo.jsx | 173 +++++++ .../for-blueprint-developers/ab-playback.md | 236 ++++++++++ .../for-blueprint-developers/hold.md | 52 +++ .../for-blueprint-developers/intro.md | 37 ++ .../for-blueprint-developers/lookahead.md | 96 ++++ .../manipulating-ingest-data.md | 139 ++++++ .../for-blueprint-developers/mos-statuses.md | 53 +++ .../part-and-piece-timings.mdx | 141 ++++++ .../sync-ingest-changes.md | 23 + .../timeline-datastore.md | 85 ++++ .../version-26.03.0/for-developers/intro.md | 15 + .../for-developers/json-config-schema.md | 218 +++++++++ .../for-developers/libraries.md | 55 +++ .../for-developers/mos-plugins.md | 185 ++++++++ .../for-developers/npm-package-publishing.md | 23 + .../for-developers/publications.md | 43 ++ .../for-developers/url-query-parameters.md | 25 + .../worker-threads-and-locks.md | 61 +++ .../user-guide/concepts-and-architecture.md | 191 ++++++++ .../user-guide/configuration/_category_.json | 4 + .../user-guide/configuration/settings-view.md | 202 ++++++++ .../configuration/sofie-core-settings.md | 110 +++++ .../version-26.03.0/user-guide/faq.md | 16 + .../user-guide/features/_category_.json | 4 + .../user-guide/features/access-levels.md | 64 +++ .../user-guide/features/api.md | 19 + .../user-guide/features/intro.md | 18 + .../user-guide/features/language.md | 25 + .../user-guide/features/prompter.md | 245 ++++++++++ .../features/sofie-views-and-screens.mdx | 439 ++++++++++++++++++ .../user-guide/features/system-health.md | 27 ++ .../user-guide/further-reading.md | 59 +++ .../user-guide/installation/_category_.json | 4 + .../installation/initial-sofie-core-setup.md | 23 + .../installing-a-gateway/_category_.json | 4 + .../installing-a-gateway/input-gateway.md | 53 +++ .../installing-a-gateway/intro.md | 41 ++ .../installing-a-gateway/playout-gateway.md | 9 + .../_category_.json | 4 + .../google-spreadsheet.md | 52 +++ .../inews-gateway.md | 8 + .../intro.md | 21 + .../mos-gateway.md | 19 + .../installation/installing-blueprints.md | 46 ++ .../README.md | 35 ++ .../_category_.json | 4 + .../casparcg-server-installation.md | 224 +++++++++ .../ffmpeg-installation.md | 35 ++ .../vision-mixers.md | 13 + .../installing-package-manager.md | 205 ++++++++ .../installing-sofie-server-core.md | 23 + .../user-guide/installation/intro.md | 25 + .../user-guide/installation/quick-install.md | 172 +++++++ .../user-guide/installation/rundown-editor.md | 18 + .../version-26.03.0/user-guide/intro.md | 41 ++ .../user-guide/supported-devices.md | 118 +++++ .../version-1.52.0-sidebars.json | 14 + .../version-26.03.0-sidebars.json | 14 + packages/documentation/versions.json | 2 + 139 files changed, 9258 insertions(+) create mode 100644 packages/documentation/versioned_docs/version-1.52.0/about-sofie.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/api-documentation.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/api-stability.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/contribution-guidelines.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/data-model.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/options-and-mappings.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-actions.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-api.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-types.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/_part-timings-demo.jsx create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/ab-playback.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/hold.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/lookahead.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/manipulating-ingest-data.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/part-and-piece-timings.mdx create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/sync-ingest-changes.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/timeline-datastore.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/json-config-schema.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/libraries.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/mos-plugins.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/npm-package-publishing.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/publications.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/url-query-parameters.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/worker-threads-and-locks.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/concepts-and-architecture.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/settings-view.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/sofie-core-settings.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/faq.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/access-levels.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/api.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/language.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/prompter.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/sofie-views.mdx create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/system-health.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/further-reading.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/initial-sofie-core-setup.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/playout-gateway.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md create mode 100644 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 create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-blueprints.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/README.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/vision-mixers.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-input-gateway.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-package-manager.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-sofie-server-core.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/media-manager.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/rundown-editor.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/supported-devices.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/about-sofie.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/api-documentation.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/api-stability.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/contribution-guidelines.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/data-model.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/options-and-mappings.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/shared-hardware-control.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-actions.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-api.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-plugins.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-types.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/_part-timings-demo.jsx create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/ab-playback.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/hold.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/lookahead.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/manipulating-ingest-data.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/mos-statuses.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/part-and-piece-timings.mdx create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/sync-ingest-changes.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/timeline-datastore.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/json-config-schema.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/libraries.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/mos-plugins.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/npm-package-publishing.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/publications.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/url-query-parameters.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/worker-threads-and-locks.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/concepts-and-architecture.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/settings-view.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/sofie-core-settings.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/faq.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/access-levels.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/api.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/language.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/prompter.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/sofie-views-and-screens.mdx create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/system-health.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/further-reading.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/initial-sofie-core-setup.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/input-gateway.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/playout-gateway.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/google-spreadsheet.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-blueprints.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/README.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/vision-mixers.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-package-manager.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-sofie-server-core.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/quick-install.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/rundown-editor.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/supported-devices.md create mode 100644 packages/documentation/versioned_sidebars/version-1.52.0-sidebars.json create mode 100644 packages/documentation/versioned_sidebars/version-26.03.0-sidebars.json 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..27479bf97ca --- /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 soley 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 aquires 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 refered 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..dbf53b3a49a --- /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 responsibilites 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.html) +- [TSR Integration API](./tsr-api.html) +- [TSR Types package](./tsr-types.html) +- [TSR Actions](./tsr-actions.html) + +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.html). + +:::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..1bb182f1553 --- /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.html) 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..7c2d6449699 --- /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 noticable 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..9a5c7a73823 --- /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 propogates 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..2b21205a3c9 --- /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 pieceGoup 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..0d34a7c9359 --- /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 propogate 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 propogate 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 propogation 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..e739ee0addd --- /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 conjuction 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..7557ca3e293 --- /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 separetely, 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..7432f88abdb --- /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 purpuse 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 programatically 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 udate in the future. + +:::note Example of linking from a (potentially proxied) subfolder +**Case:** `https://mysofie.com/myplugin/index.html` wants to acccess `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..9eebbf157e3 --- /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 aquire a `RundownLock` while inside of a `PlaylistLock`. This is to avoid deadlocks, as it is very +common to aquire 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..917222182b6 --- /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..b6ced3ae8cb --- /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 neccessary 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 sequnece 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 activites 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 wizzard-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..ed2ecc806a1 --- /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 speficied 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..807e5840bc7 --- /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 heirarchical, 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 agressive 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..9e445a263d1 --- /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 definied 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..6940a84cb74 --- /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 scolling. Left-click to toggle, right-click to rewind | +| `?controlmode=smoothscroll` | Scrolling the mouse wheel starts continous 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 outputing 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"_ | Increse `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 Swith 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..d4e0cebd4b5 --- /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 dissapear 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 mached 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..a0fd8d66a2c --- /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 Varibles..." 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/versioned_docs/version-1.52.0/user-guide/installation/installing-input-gateway.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-input-gateway.md new file mode 100644 index 00000000000..d17f66d247e --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-input-gateway.md @@ -0,0 +1,45 @@ +# 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), 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. + +### 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-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..5b2016babe8 --- /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 auxilliaries + +## 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..6682723f991 --- /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 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. + +## 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 resuling 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", From 8f0e2cc7ad2c17e28c26a168588aa264337fc5e7 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 12 Feb 2026 16:23:56 +0000 Subject: [PATCH 097/291] Remove config overriding latest branch So we default to 26.03, but have a "next" that users can click manually. --- packages/documentation/docusaurus.config.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/documentation/docusaurus.config.js b/packages/documentation/docusaurus.config.js index 386e01ebf45..34cf510e12a 100644 --- a/packages/documentation/docusaurus.config.js +++ b/packages/documentation/docusaurus.config.js @@ -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, From f21337bb3de7698d41ce8028d822744acb8388ef Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 12 Feb 2026 16:42:33 +0000 Subject: [PATCH 098/291] Fix broken links in v1.52.0 docs --- .../for-developers/device-integrations/intro.md | 10 +++++----- .../device-integrations/options-and-mappings.md | 2 +- .../user-guide/concepts-and-architecture.md | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) 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 index dbf53b3a49a..a3da6b440cd 100644 --- 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 @@ -4,14 +4,14 @@ Device integrations in Sofie are part of the Timeline State Resolver (TSR) libra 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.html) -- [TSR Integration API](./tsr-api.html) -- [TSR Types package](./tsr-types.html) -- [TSR Actions](./tsr-actions.html) +- [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.html). +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 index 1bb182f1553..343b3821e59 100644 --- 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 @@ -1,6 +1,6 @@ # 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.html) in the `$schemas` folder of your integration. +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 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 index 917222182b6..ef9008f40ca 100644 --- 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 @@ -27,7 +27,7 @@ To be able to facilitate various workflows and to Here's a short explanation abo - 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). +- 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) From 4cc230a1d782136d9a22e0ee0379510fa5d884e4 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Fri, 13 Feb 2026 11:35:34 +0000 Subject: [PATCH 099/291] Fix typos in new docs --- .../device-integrations/tsr-plugins.md | 2 +- .../docs/user-guide/features/prompter.md | 26 +++++++++---------- .../src/components/GitHubReleases.jsx | 2 +- .../for-developers/data-model.md | 6 ++--- .../device-integrations/intro.md | 2 +- .../for-blueprint-developers/lookahead.md | 2 +- .../manipulating-ingest-data.md | 2 +- .../part-and-piece-timings.mdx | 2 +- .../sync-ingest-changes.md | 6 ++--- .../timeline-datastore.md | 2 +- .../for-developers/json-config-schema.md | 2 +- .../for-developers/mos-plugins.md | 8 +++--- .../worker-threads-and-locks.md | 4 +-- .../user-guide/configuration/settings-view.md | 8 +++--- .../configuration/sofie-core-settings.md | 2 +- .../user-guide/features/access-levels.md | 4 +-- .../version-1.52.0/user-guide/features/api.md | 2 +- .../user-guide/features/prompter.md | 10 +++---- .../user-guide/features/sofie-views.mdx | 4 +-- .../ffmpeg-installation.md | 2 +- .../user-guide/supported-devices.md | 2 +- .../device-integrations/tsr-plugins.md | 2 +- 22 files changed, 51 insertions(+), 51 deletions(-) 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..b6cd77ceeb4 100644 --- a/packages/documentation/docs/for-developers/device-integrations/tsr-plugins.md +++ b/packages/documentation/docs/for-developers/device-integrations/tsr-plugins.md @@ -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/user-guide/features/prompter.md b/packages/documentation/docs/user-guide/features/prompter.md index 7440cbb5334..3fe09809329 100644 --- a/packages/documentation/docs/user-guide/features/prompter.md +++ b/packages/documentation/docs/user-guide/features/prompter.md @@ -46,7 +46,7 @@ The prompter can be controlled by different types of controllers. The control mo | `?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=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\) @@ -175,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` @@ -241,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/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.52.0/for-developers/data-model.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/data-model.md index 27479bf97ca..f835ecbb4f4 100644 --- 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 @@ -18,7 +18,7 @@ In every case, any layout changes and any scheduled cleanup are performed by the 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 soley 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. +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: @@ -82,7 +82,7 @@ Some of these collections are used by Package Manager to initiate work, while ot 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 aquires 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. +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. @@ -127,6 +127,6 @@ Our solution to some of this early on was to not regenerate certain Parts when r 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 refered to a Part which no longer existed, or whose content was very different to how it was played. +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/intro.md b/packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/intro.md index a3da6b440cd..928514cc1fa 100644 --- 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 @@ -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 responsibilites 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/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 index 7c2d6449699..f1d10c34381 100644 --- 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 @@ -38,7 +38,7 @@ export enum LookaheadMode { } ``` -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 noticable cost. +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. 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 index 9a5c7a73823..3b01e885cba 100644 --- 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 @@ -7,7 +7,7 @@ At times it can be useful to manipulate this data before it gets passed into the 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 propogates nrcs changes is: +The basic implementation of this method which simply propagates nrcs changes is: ```ts function processIngestData( 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 index 2b21205a3c9..8c2b6e8e694 100644 --- 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 @@ -131,7 +131,7 @@ const inTransition = { Pieces with an infinite lifespan (ie, not `lifespan: PieceLifespan.WithinPart`) get handled differently to other pieces. -Only one pieceGoup is created for an infinite Piece which is present in multiple of the current, next and previous Parts. +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 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 index 0d34a7c9359..7c609400d29 100644 --- 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 @@ -2,11 +2,11 @@ title: Sync Ingest Changes --- -Since PartInstances and PieceInstances were added to Sofie, the default behaviour in Sofie is to not propogate any ingest changes from a Part onto its PartInstances. +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 propogate 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. +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 propogation 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. +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. 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 index e739ee0addd..ae18c75c05f 100644 --- 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 @@ -1,6 +1,6 @@ # Timeline Datastore -The timeline datastore is a key-value store that can be used in conjuction 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. +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 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 index 7557ca3e293..6567cbc6761 100644 --- 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 @@ -130,7 +130,7 @@ This is not available in all places we use this schema. For example, Mappings ar ## Examples -Below is an example of a simple schema for a gateway configuration. The subdevices are handled separetely, with their own schema. +Below is an example of a simple schema for a gateway configuration. The subdevices are handled separately, with their own schema. ```json { 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 index 7432f88abdb..1c414442719 100644 --- 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 @@ -5,7 +5,7 @@ sidebar_position: 20 # iFrames MOS-plugins -**The usage of MOS-plugins allow micro frontends to be injected into Sofie for the purpuse of adding content to the production without turning away from the Sofie UI.** +**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. @@ -27,7 +27,7 @@ The user can create one or more Buckets. From the plugin they can drag-and-drop ## 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 programatically create an ncsItem without the drag-and-drop interaction. This is a viable solution which avoids cross-origin drag-and-drop problems. +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 @@ -61,10 +61,10 @@ As you can tell from the table, you need to exactly match both the protocol, dom _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 udate in the future. +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 acccess `https://mysofie.com/myplugin/static/images/logo.png`. +**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`. 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 index 9eebbf157e3..8018a060822 100644 --- 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 @@ -56,6 +56,6 @@ This lock gives ownership of a specific `Rundown`. It is required to be able to during other times where the `Rundown` is modified or is expected to not change. :::caution -It is not allowed to aquire a `RundownLock` while inside of a `PlaylistLock`. This is to avoid deadlocks, as it is very -common to aquire a `PlaylistLock` inside of a `RundownLock` +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/configuration/settings-view.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/settings-view.md index b6ced3ae8cb..0a570ecbcd7 100644 --- 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 @@ -62,7 +62,7 @@ The Studio uses a studio-blueprint, which handles things like mapping up an inco ### 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 neccessary data into it. +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 @@ -143,11 +143,11 @@ The triggers are designed to be either client-specific or issued by a peripheral 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 sequnece 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. +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 activites 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-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) @@ -155,7 +155,7 @@ If you would like to set up combination Triggers, using Device Triggers on an In 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 wizzard-like fashion through the available *filter* options on a given *action*. +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) 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 index ed2ecc806a1..a6d00aa139c 100644 --- 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 @@ -97,7 +97,7 @@ There are various settings you can set for an installation. See the list below: | `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 speficied NTP server time. | `null` | +| `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 | 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 index 807e5840bc7..b0d765c86bb 100644 --- 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 @@ -6,14 +6,14 @@ sidebar_position: 3 ## Permissions -There are a few different access levels that users can be assigned. They are not heirarchical, you will often need to enable multiple for each user. +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 agressive in what is shown in the rundown view | +| **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. | 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 index 9e445a263d1..a6ee88bcddd 100644 --- 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 @@ -6,7 +6,7 @@ sidebar_position: 10 ## Sofie User Actions REST API -Starting with version 1.50.0, there is a semantically-versioned HTTP REST API definied 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. +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` 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 index 6940a84cb74..d3b40372db7 100644 --- 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 @@ -54,8 +54,8 @@ 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 scolling. Left-click to toggle, right-click to rewind | -| `?controlmode=smoothscroll` | Scrolling the mouse wheel starts continous scrolling. Small speed adjustments can then be made by nudging the scroll wheel. Stop the scrolling by making a "larger scroll" on the wheel. | +| `?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. @@ -72,7 +72,7 @@ Keyboard control is intended to be used when having a "keyboard"-device, such as #### 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 outputing keystrokes will work in this mode. +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 | | :----------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | @@ -139,7 +139,7 @@ Any movement within forward range will map to the `pedal_speedMap` with interpol | _"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"_ | Increse `pedal_rangeNeutralMin` | +| _"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: @@ -173,7 +173,7 @@ The Joycons can operate in 3 modes, the L-stick, the R-stick or both L+R sticks - `joycon_rangeNeutralMax` has to be greater than `joycon_rangeNeutralMin` - `joycon_rangeFwdMax` has to be greater than `joycon_rangeNeutralMax` -![Nintendo Swith Joycons](/img/docs/main/features/nintendo-switch-joycons.jpg) +![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. 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 index d4e0cebd4b5..4ce3b9ba014 100644 --- 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 @@ -60,7 +60,7 @@ In the top-right corner of the Segment, there's a button controlling the display 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 dissapear from the view. +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. @@ -128,7 +128,7 @@ The _Rundown View_ and the _Detached Shelf View_ UI can have multiple concurrent 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 mached 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: +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` 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 index a0fd8d66a2c..9833fb45a43 100644 --- 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 @@ -11,7 +11,7 @@ Some parts of Sofie (specifically the Package Manager) require that [`FFmpeg`](h ![Start Menu screenshot](/img/docs/edit_system_environment_variables.jpg) -4. In the System Properties menu, click the "Environment Varibles..." button at the bottom of the "Advanced" tab. +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) 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 index 5b2016babe8..0bee545156d 100644 --- 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 @@ -15,7 +15,7 @@ We support almost all features of these devices except fairlight audio, camera c - Full control of keyers - Full control of DVE's - Control of media pools -- Control of auxilliaries +- Control of auxiliaries ## CasparCG Server 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 index 6682723f991..b6cd77ceeb4 100644 --- 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 @@ -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` From d117426d2c1d53474990cca8109d34660cee1d6f Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Wed, 17 Dec 2025 17:22:50 +0100 Subject: [PATCH 100/291] feat(core): RundownPlaylist filters in Action Triggers Add UI for RundownPlaylist filters for AdLibs --- .../api/deviceTriggers/StudioObserver.ts | 4 +- .../deviceTriggers/reactiveContentCache.ts | 11 +- .../api/deviceTriggers/triggersContext.ts | 7 +- .../blueprints-integration/src/triggers.ts | 7 + .../meteor-lib/src/triggers/actionFactory.ts | 5 +- .../triggers/actionFilterChainCompilers.ts | 101 ++++++++- .../client/lib/triggers/TriggersHandler.tsx | 9 +- .../actionEditors/ActionEditor.tsx | 30 ++- .../filterPreviews/AdLibFilter.tsx | 14 +- .../filterPreviews/FilterEditor.tsx | 13 +- .../filterPreviews/RundownPlaylistFilter.tsx | 195 ++++++++++++++++-- .../filterPreviews/SwitchFilterType.tsx | 47 +++++ .../filterPreviews/ViewFilter.tsx | 12 +- 13 files changed, 425 insertions(+), 30 deletions(-) create mode 100644 packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/filterPreviews/SwitchFilterType.tsx 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 79eee1de0e7..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' @@ -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/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/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..6401a8c1b38 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,38 @@ 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) { + adLibPieceTypeFilter.skip = true + adLibActionTypeFilter.skip = true + clearAdLibs.length = 0 + stickyAdLibs.length = 0 + } + } + } + { let skip = adLibPieceTypeFilter.skip const currentNextOverride: MongoQuery = {} 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/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx index c7c5d3bdfdd..86b1c86f360 100644 --- a/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx +++ b/packages/webui/src/client/ui/Settings/components/triggeredActions/actionEditors/ActionEditor.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useState } from 'react' import _ from 'underscore' import { + FilterType, IAdLibFilterLink, IGUIContextFilterLink, IRundownPlaylistFilterLink, @@ -39,9 +40,8 @@ function isFinal( ): boolean { if (action.action === PlayoutActions.adlib) { return chainLink?.object === 'adLib' && (chainLink?.field === 'pick' || chainLink?.field === 'pickEnd') - } else { - return chainLink?.object === 'view' } + return false } type ChainLink = IRundownPlaylistFilterLink | IGUIContextFilterLink | IAdLibFilterLink @@ -75,6 +75,15 @@ export const ActionEditor: React.FC = 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..67a7bd112a9 --- /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.')}

From 28680cfdc98a61eb54590c81943767745d3f3fdf Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Sun, 7 Dec 2025 19:18:27 +0000 Subject: [PATCH 101/291] Add --db option to run.js To manage database switching in meteor. If given it will symlink a new database folder. --- scripts/lib.js | 5 +++++ scripts/run.mjs | 51 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/scripts/lib.js b/scripts/lib.js index 58376c572aa..171b98afcf9 100644 --- a/scripts/lib.js +++ b/scripts/lib.js @@ -1,9 +1,14 @@ const args = process.argv.slice(2); +// 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, }; module.exports = { diff --git a/scripts/run.mjs b/scripts/run.mjs index a2a65ff3c66..b1a1d1e9fd9 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,56 @@ function hr() { return "─".repeat(process.stdout.columns ?? 40); } +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 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 as 'default' + 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, just remove current + fs.rmSync(dbLink, { recursive: true, force: true }); + } + } + } + + // 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 + + // Switch database if requested + if (config.dbName) { + switchDatabase(config.dbName); + } // The main watching execution console.log(hr()); From f5fd62a33710c2fe52759c434a9e5f677429fab5 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Sun, 7 Dec 2025 19:19:10 +0000 Subject: [PATCH 102/291] Add --help option to run.mjs via lib.js --- scripts/lib.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/scripts/lib.js b/scripts/lib.js index 171b98afcf9..affe6c932f2 100644 --- a/scripts/lib.js +++ b/scripts/lib.js @@ -1,5 +1,32 @@ 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] + +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 + +Examples: + yarn dev # Run in normal dev mode + 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 +`); + process.exit(0); +} + // Parse --db=name option const dbArg = args.find(arg => arg.startsWith('--db=')); const dbName = dbArg ? dbArg.split('=')[1] : null; From 42da439efea972bee50982f37f7640a12e2b9b40 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Sun, 7 Dec 2025 19:28:51 +0000 Subject: [PATCH 103/291] Mention yarn start in help --- scripts/lib.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/lib.js b/scripts/lib.js index affe6c932f2..a4743b3c03c 100644 --- a/scripts/lib.js +++ b/scripts/lib.js @@ -6,6 +6,10 @@ if (args.indexOf("--help") >= 0 || args.indexOf("-h") >= 0) { 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 @@ -18,11 +22,13 @@ Options: Run without --db to use the currently active database Examples: - yarn dev # Run in normal dev mode + yarn start # Install, build, then run in dev mode + yarn dev # Run in normal dev mode (requires prior build) 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); } From 101b78f61690f79c0bd9b218bfa2f88840924686 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Mon, 9 Feb 2026 16:08:56 +0000 Subject: [PATCH 104/291] Add --db-list option to list available databases --- scripts/lib.js | 3 +++ scripts/run.mjs | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/scripts/lib.js b/scripts/lib.js index a4743b3c03c..ea9fad5917d 100644 --- a/scripts/lib.js +++ b/scripts/lib.js @@ -20,10 +20,12 @@ Options: 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 @@ -42,6 +44,7 @@ const config = { 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 b1a1d1e9fd9..158493a3647 100644 --- a/scripts/run.mjs +++ b/scripts/run.mjs @@ -72,6 +72,52 @@ 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.statSync(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'); @@ -118,6 +164,12 @@ function switchDatabase(dbName) { try { // 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); From d98a5117428ffb95e11b1a187a44c14edd536c44 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Fri, 13 Feb 2026 15:02:48 +0000 Subject: [PATCH 105/291] Apply suggestions from Coderabbit --- scripts/run.mjs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/scripts/run.mjs b/scripts/run.mjs index 158493a3647..a7020c5e1f1 100644 --- a/scripts/run.mjs +++ b/scripts/run.mjs @@ -99,7 +99,7 @@ function listDatabases() { // List all db.* directories const files = fs.readdirSync(meteorLocalDir); const dbDirs = files - .filter(file => file.startsWith('db.') && fs.statSync(path.join(meteorLocalDir, file)).isDirectory()) + .filter(file => file.startsWith('db.') && fs.lstatSync(path.join(meteorLocalDir, file)).isDirectory()) .map(file => file.substring(3)); console.log('\nAvailable databases:'); @@ -125,10 +125,13 @@ function switchDatabase(dbName) { // Check if we're already using this database if (fs.existsSync(dbLink)) { - const currentTarget = fs.readlinkSync(dbLink); - if (currentTarget === `db.${dbName}`) { - console.log(`✓ Already using database: ${dbName}`); - return; + const stats = fs.lstatSync(dbLink); + if (stats.isSymbolicLink()) { + const currentTarget = fs.readlinkSync(dbLink); + if (currentTarget === `db.${dbName}`) { + console.log(`✓ Already using database: ${dbName}`); + return; + } } } @@ -144,14 +147,23 @@ function switchDatabase(dbName) { if (stats.isSymbolicLink()) { fs.unlinkSync(dbLink); } else { - // It's a real directory - back it up as 'default' + // 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, just remove current - fs.rmSync(dbLink, { recursive: true, force: true }); + // 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); } } } From 098224b1791aff551901fad77a6e524b76aa38cc Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Sun, 15 Feb 2026 14:54:27 +0000 Subject: [PATCH 106/291] fix: Remove unused imports from merge --- packages/job-worker/src/playout/adlibAction.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/job-worker/src/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index 38670f959c4..e4d14925f7f 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -35,9 +35,6 @@ import type { INoteBase } from '@sofie-automation/corelib/dist/dataModel/Notes' import { NotificationsModelHelper } from '../notifications/NotificationsModelHelper.js' import type { INotificationsModel } from '../notifications/NotificationsModel.js' import { PersistentPlayoutStateStore } from '../blueprints/context/services/PersistantStateStore.js' -import { AdLibAction } from '@sofie-automation/corelib/dist/dataModel/AdlibAction' -import { RundownBaselineAdLibAction } from '@sofie-automation/corelib/dist/dataModel/RundownBaselineAdLibAction' -import { BucketAdLibAction } from '@sofie-automation/corelib/dist/dataModel/BucketAdLibAction' /** * Execute an AdLib Action From 4d0de2f30e1e153a61d3eb6ab5eec99e74081a14 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 06:52:18 +0000 Subject: [PATCH 107/291] chore(deps): bump aquasecurity/trivy-action from 0.33.1 to 0.34.0 Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.33.1 to 0.34.0. - [Release notes](https://github.com/aquasecurity/trivy-action/releases) - [Commits](https://github.com/aquasecurity/trivy-action/compare/0.33.1...0.34.0) --- updated-dependencies: - dependency-name: aquasecurity/trivy-action dependency-version: 0.34.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/node.yaml | 4 ++-- .github/workflows/trivy.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 17342c73945..022bf49a870 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -274,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.34.0 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: @@ -446,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.34.0 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index d12492f71cc..65b5a9cab56 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Run Trivy vulnerability scanner (json) - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.34.0 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: @@ -30,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.34.0 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: @@ -48,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.34.0 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: From f336ec269d85738f8da772d38435b2618da782c5 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 16 Feb 2026 08:59:10 +0000 Subject: [PATCH 108/291] fix: reduce duplication when using useOverrideOpHelper for simple objects (#1619) --- .../Settings/BlueprintConfigSchema/index.tsx | 42 ++++--------- .../Studio/Devices/IngestSubDevices.tsx | 3 +- .../Studio/Devices/InputSubDevices.tsx | 3 +- .../Studio/Devices/PlayoutSubDevices.tsx | 3 +- .../src/client/ui/Settings/Studio/Generic.tsx | 41 +++---------- .../client/ui/Settings/SystemManagement.tsx | 42 +------------ .../ui/Settings/util/OverrideOpHelper.tsx | 59 ++++++++++++++++++- 7 files changed, 83 insertions(+), 110 deletions(-) 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/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/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/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, + } +} From 0353093a143b39ed34d70b70f7667749c1876422 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 16 Feb 2026 10:12:18 +0000 Subject: [PATCH 109/291] feat: single eslint config (#1629) --- .github/workflows/node.yaml | 41 +++------- .github/workflows/publish-libs.yml | 23 +++--- .../blueprints-integration/eslint.config.mjs | 3 - packages/blueprints-integration/package.json | 11 +-- packages/corelib/eslint.config.mjs | 3 - packages/corelib/package.json | 11 +-- packages/{webui => }/eslint.config.mjs | 75 +++++++++++++------ packages/job-worker/eslint.config.mjs | 3 - packages/job-worker/package.json | 11 +-- .../src/blueprints/__tests__/lib.ts | 2 + .../live-status-gateway-api/eslint.config.mjs | 3 - packages/live-status-gateway-api/package.json | 11 +-- .../live-status-gateway/eslint.config.mjs | 15 ---- packages/live-status-gateway/package.json | 11 +-- .../live-status-gateway/tsconfig.build.json | 6 +- packages/meteor-lib/eslint.config.mjs | 3 - packages/meteor-lib/package.json | 11 +-- packages/mos-gateway/eslint.config.mjs | 3 - packages/mos-gateway/package.json | 11 +-- .../src/integrationTests/index.spec.ts | 6 +- packages/mos-gateway/tsconfig.build.json | 6 +- packages/openapi/eslint.config.mjs | 17 ----- packages/openapi/package.json | 13 +--- packages/package.json | 11 ++- packages/playout-gateway/eslint.config.mjs | 3 - packages/playout-gateway/package.json | 11 +-- packages/playout-gateway/tsconfig.build.json | 6 +- .../server-core-integration/eslint.config.mjs | 3 - packages/server-core-integration/package.json | 11 +-- packages/shared-lib/eslint.config.mjs | 3 - packages/shared-lib/package.json | 11 +-- packages/tsconfig.build.json | 1 + packages/tsconfig.json | 1 + packages/tsconfig.test.json | 25 +++++++ packages/webui/package.json | 3 +- packages/yarn.lock | 24 +++--- 36 files changed, 153 insertions(+), 259 deletions(-) delete mode 100644 packages/blueprints-integration/eslint.config.mjs delete mode 100644 packages/corelib/eslint.config.mjs rename packages/{webui => }/eslint.config.mjs (56%) delete mode 100644 packages/job-worker/eslint.config.mjs delete mode 100644 packages/live-status-gateway-api/eslint.config.mjs delete mode 100644 packages/live-status-gateway/eslint.config.mjs delete mode 100644 packages/meteor-lib/eslint.config.mjs delete mode 100644 packages/mos-gateway/eslint.config.mjs delete mode 100644 packages/openapi/eslint.config.mjs delete mode 100644 packages/playout-gateway/eslint.config.mjs delete mode 100644 packages/server-core-integration/eslint.config.mjs delete mode 100644 packages/shared-lib/eslint.config.mjs create mode 100644 packages/tsconfig.test.json diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 022bf49a870..174a7f5c8dd 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -467,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: @@ -498,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@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 diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index 2652a0b9aa1..fff3d3fb39a 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 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/package.json b/packages/blueprints-integration/package.json index 1da929ce401..5bdbca51474 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -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", @@ -40,14 +39,6 @@ "tslib": "^2.8.1", "type-fest": "^4.41.0" }, - "lint-staged": { - "*.{css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx,js,jsx}": [ - "yarn lint:raw" - ] - }, "publishConfig": { "access": "public" }, 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/package.json b/packages/corelib/package.json index 0edc415df3b..01fe56e7000 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -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", @@ -54,13 +53,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/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/package.json b/packages/job-worker/package.json index 21a2bd9a64d..e524321c79f 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -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", @@ -53,14 +52,6 @@ "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": "^30.2.0", 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/live-status-gateway-api/eslint.config.mjs b/packages/live-status-gateway-api/eslint.config.mjs deleted file mode 100644 index 7b51e4e0352..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({ ignores: ['server'] }) diff --git a/packages/live-status-gateway-api/package.json b/packages/live-status-gateway-api/package.json index 62eb35dffa3..ef2c8edac40 100644 --- a/packages/live-status-gateway-api/package.json +++ b/packages/live-status-gateway-api/package.json @@ -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", @@ -54,13 +53,5 @@ "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/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/package.json b/packages/live-status-gateway/package.json index bc59a813fc7..c684e397136 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -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", @@ -62,13 +61,5 @@ }, "devDependencies": { "type-fest": "^4.41.0" - }, - "lint-staged": { - "*.{css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx,js,jsx,yaml}": [ - "yarn lint:raw" - ] } } 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/package.json b/packages/meteor-lib/package.json index b9b9fe015b5..55b1c386c7f 100644 --- a/packages/meteor-lib/package.json +++ b/packages/meteor-lib/package.json @@ -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", @@ -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/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/package.json b/packages/mos-gateway/package.json index edd6286042d..3e9da8abdc7 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -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", @@ -70,13 +69,5 @@ "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/tsconfig.build.json b/packages/mos-gateway/tsconfig.build.json index 67f2cc0cfe1..16b30b4ad02 100644 --- a/packages/mos-gateway/tsconfig.build.json +++ b/packages/mos-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", @@ -12,7 +12,9 @@ "types": ["node"], "resolveJsonModule": true, "skipLibCheck": true, - "esModuleInterop": true + "esModuleInterop": true, + "declaration": true, + "composite": true }, "references": [ // 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/package.json b/packages/openapi/package.json index 62e703dee87..a038b2af813 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -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", @@ -41,18 +39,9 @@ "devDependencies": { "@openapitools/openapi-generator-cli": "^2.28.0", "eslint": "^9.39.2", - "eslint-plugin-yml": "^3.0.0", "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/package.json b/packages/package.json index 18b88cde000..ea5396abb36 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 .", @@ -56,6 +56,7 @@ "copyfiles": "^2.4.1", "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", @@ -88,5 +89,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/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/package.json b/packages/playout-gateway/package.json index 74ef628f243..7d205484e76 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -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", @@ -61,13 +60,5 @@ "underscore": "^1.13.7", "winston": "^3.19.0" }, - "lint-staged": { - "*.{css,json,md,scss}": [ - "yarn run -T prettier" - ], - "*.{ts,tsx,js,jsx}": [ - "yarn lint:raw" - ] - }, "packageManager": "yarn@4.12.0" } diff --git a/packages/playout-gateway/tsconfig.build.json b/packages/playout-gateway/tsconfig.build.json index 14692f9baf2..36c632a33a0 100644 --- a/packages/playout-gateway/tsconfig.build.json +++ b/packages/playout-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", @@ -11,7 +11,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/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/package.json b/packages/server-core-integration/package.json index 5a09436ff53..cb4a835fa04 100644 --- a/packages/server-core-integration/package.json +++ b/packages/server-core-integration/package.json @@ -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", @@ -81,14 +80,6 @@ "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/package.json b/packages/shared-lib/package.json index 7c6e641df74..8638db37deb 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -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", @@ -43,14 +42,6 @@ "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/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/package.json b/packages/webui/package.json index b6bdfc68520..5d146a29837 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -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", diff --git a/packages/yarn.lock b/packages/yarn.lock index 76fe59939d2..ed694390ef1 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -3743,7 +3743,7 @@ __metadata: languageName: node linkType: hard -"@eslint/core@npm:^1.0.1": +"@eslint/core@npm:^1.0.1, @eslint/core@npm:^1.1.0": version: 1.1.0 resolution: "@eslint/core@npm:1.1.0" dependencies: @@ -3793,13 +3793,13 @@ __metadata: languageName: node linkType: hard -"@eslint/plugin-kit@npm:^0.5.1": - version: 0.5.1 - resolution: "@eslint/plugin-kit@npm:0.5.1" +"@eslint/plugin-kit@npm:^0.6.0": + version: 0.6.0 + resolution: "@eslint/plugin-kit@npm:0.6.0" dependencies: - "@eslint/core": "npm:^1.0.1" + "@eslint/core": "npm:^1.1.0" levn: "npm:^0.4.1" - checksum: 10/ea68c0a01279daf3b0f5aa65d82f7d65690b9a1aa6e2c7998dfc431039d22dcf0bdacee644ce77eb8dcec8e61128f2cbc6d57c7fe5b108e8cd875d47e3f79cda + checksum: 10/9c4e2901248ce092674b939fce9104a81d16222ed23c1b6057a5aaea8ec2fa802bcc9fbaf667e54a4206aca21f70c8b943ee14e6010530a46a4fcb64c41537b1 languageName: node linkType: hard @@ -7232,7 +7232,6 @@ __metadata: dependencies: "@openapitools/openapi-generator-cli": "npm:^2.28.0" eslint: "npm:^9.39.2" - eslint-plugin-yml: "npm:^3.0.0" js-yaml: "npm:^4.1.1" tslib: "npm:^2.8.1" wget-improved: "npm:^3.4.0" @@ -14810,12 +14809,12 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"eslint-plugin-yml@npm:^3.0.0": - version: 3.0.0 - resolution: "eslint-plugin-yml@npm:3.0.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.5.1" + "@eslint/plugin-kit": "npm:^0.6.0" debug: "npm:^4.3.2" diff-sequences: "npm:^29.0.0" escape-string-regexp: "npm:5.0.0" @@ -14823,7 +14822,7 @@ asn1@evs-broadcast/node-asn1: yaml-eslint-parser: "npm:^2.0.0" peerDependencies: eslint: ">=9.38.0" - checksum: 10/501d78791d45e7c0e722e727bc51e782fe80cb5762b0d1f3b72b27fabcb8610bd8dfbe3c2ba1dc03108cdb08811047152ba3564ba49d030bbe27b39cf3f0b9dd + checksum: 10/8be6d3353c21b7e276b76430fcff1ab83f2fcca491e58b4ba399738c3ce7196e82f55c98f88dc73061aa462cc6f868008dc294445524f2a2cb58457c6ddf0489 languageName: node linkType: hard @@ -23320,6 +23319,7 @@ asn1@evs-broadcast/node-asn1: copyfiles: "npm:^2.4.1" 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" From 70690cc7e3a0d1a10d2f27a60d96677c88f0b472 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 16 Feb 2026 10:13:15 +0000 Subject: [PATCH 110/291] chore: replace nrk-coreicons with fontawesome (#1634) --- packages/webui/package.json | 1 - packages/webui/src/client/lib/ModalDialog.tsx | 4 ++-- .../lib/notifications/NotificationCenterPanel.tsx | 11 +++-------- .../src/client/lib/ui/containers/modals/Modal.tsx | 4 ++-- .../ui/RundownView/RundownHeader/RundownHeader.tsx | 4 ++-- .../client/ui/UserEditOperations/PropertiesPanel.tsx | 4 ++-- packages/yarn.lock | 8 -------- 7 files changed, 11 insertions(+), 25 deletions(-) diff --git a/packages/webui/package.json b/packages/webui/package.json index 5d146a29837..69e18fc9e23 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -36,7 +36,6 @@ "@fortawesome/free-solid-svg-icons": "^7.1.0", "@fortawesome/react-fontawesome": "^3.1.1", "@jstarpl/react-contextmenu": "^2.15.3", - "@nrk/core-icons": "^9.6.0", "@popperjs/core": "^2.11.8", "@sofie-automation/blueprints-integration": "26.3.0-1", "@sofie-automation/corelib": "26.3.0-1", diff --git a/packages/webui/src/client/lib/ModalDialog.tsx b/packages/webui/src/client/lib/ModalDialog.tsx index 5eb9da50662..cf603ab4719 100644 --- a/packages/webui/src/client/lib/ModalDialog.tsx +++ b/packages/webui/src/client/lib/ModalDialog.tsx @@ -1,5 +1,5 @@ import React, { useLayoutEffect, useRef } from 'react' -import * as CoreIcons from '@nrk/core-icons/jsx' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import Escape from './Escape.js' // @ts-expect-error type linking issue import FocusBounder from 'react-focus-bounder' @@ -164,7 +164,7 @@ export function ModalDialog({ onKeyUp={emulateClick} aria-label={t('Dismiss')} > - + 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/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/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index ffbf52f145a..00466fab680 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useContext, useEffect, 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' @@ -219,7 +219,7 @@ export function RundownHeader({
- +
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/yarn.lock b/packages/yarn.lock index ed694390ef1..4a56f7336df 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -5789,13 +5789,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" @@ -7285,7 +7278,6 @@ __metadata: "@fortawesome/free-solid-svg-icons": "npm:^7.1.0" "@fortawesome/react-fontawesome": "npm:^3.1.1" "@jstarpl/react-contextmenu": "npm:^2.15.3" - "@nrk/core-icons": "npm:^9.6.0" "@popperjs/core": "npm:^2.11.8" "@sofie-automation/blueprints-integration": "npm:26.3.0-1" "@sofie-automation/corelib": "npm:26.3.0-1" From dc3f171b42fb841ff37d80e4e0198bd0780606f1 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 16 Feb 2026 10:32:39 +0000 Subject: [PATCH 111/291] feat: allow defining OnAir only timeline objects (#1642) --- .../blueprints-integration/src/timeline.ts | 1 + .../lookahead/__tests__/findForLayer.test.ts | 213 ++++++++- .../lookahead/__tests__/findObjects.test.ts | 275 +++++++++++- .../lookahead/__tests__/lookahead.test.ts | 78 +++- .../src/playout/lookahead/findForLayer.ts | 15 +- .../src/playout/lookahead/findObjects.ts | 17 +- .../job-worker/src/playout/lookahead/index.ts | 11 +- .../playout/timeline/__tests__/lib.test.ts | 406 ++++++++++++++++++ .../job-worker/src/playout/timeline/lib.ts | 66 ++- .../job-worker/src/playout/timeline/part.ts | 6 +- .../job-worker/src/playout/timeline/piece.ts | 29 +- .../src/playout/timeline/rundown.ts | 22 +- .../shared-lib/src/core/model/Timeline.ts | 10 + 13 files changed, 1070 insertions(+), 79 deletions(-) create mode 100644 packages/job-worker/src/playout/timeline/__tests__/lib.test.ts 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/job-worker/src/playout/lookahead/__tests__/findForLayer.test.ts b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer.test.ts index 643d95b66e8..d4b50abb2c5 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/findForLayer.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/findForLayer.test.ts @@ -12,11 +12,13 @@ type TfindLookaheadObjectsForPart = jest.MockedFunction []) // Default mock +const DEFAULT_PLAYOUT_STATE = { isInHold: false, isRehearsal: false } + describe('findLookaheadForLayer', () => { const context = setupDefaultJobEnvironment() test('no data', () => { - const res = findLookaheadForLayer(context, null, [], undefined, [], 'abc', 1, 1) + const res = findLookaheadForLayer(context, null, [], undefined, [], 'abc', 1, 1, DEFAULT_PLAYOUT_STATE) expect(res.timed).toHaveLength(0) expect(res.future).toHaveLength(0) }) @@ -38,7 +40,8 @@ describe('findLookaheadForLayer', () => { usesInTransition: false, pieces: partInstanceInfo.allPieces, }, - partInstanceInfo.part._id + partInstanceInfo.part._id, + expect.objectContaining({ isInHold: expect.any(Boolean), isRehearsal: expect.any(Boolean) }) ) } @@ -88,7 +91,17 @@ describe('findLookaheadForLayer', () => { .mockReturnValueOnce(['t4', 't5'] as any) // Run it - const res = findLookaheadForLayer(context, null, partInstancesInfo, undefined, [], layer, 1, 1) + const res = findLookaheadForLayer( + context, + null, + partInstancesInfo, + undefined, + [], + layer, + 1, + 1, + DEFAULT_PLAYOUT_STATE + ) expect(res.timed).toEqual(['t0', 't1', 't2', 't3']) expect(res.future).toEqual(['t4', 't5']) @@ -105,7 +118,17 @@ describe('findLookaheadForLayer', () => { onTimeline: true, } as any findLookaheadObjectsForPartMock.mockReset().mockReturnValue([]) - findLookaheadForLayer(context, null, partInstancesInfo, previousPartInfo, [], layer, 1, 1) + findLookaheadForLayer( + context, + null, + partInstancesInfo, + previousPartInfo, + [], + layer, + 1, + 1, + DEFAULT_PLAYOUT_STATE + ) expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(3) expectInstancesToMatch(1, layer, partInstancesInfo[0], previousPartInfo) @@ -117,7 +140,17 @@ describe('findLookaheadForLayer', () => { .mockReturnValueOnce(['t2', 't3'] as any) .mockReturnValueOnce(['t4', 't5'] as any) - const res2 = findLookaheadForLayer(context, null, partInstancesInfo, undefined, [], layer, 1, 0) + const res2 = findLookaheadForLayer( + context, + null, + partInstancesInfo, + undefined, + [], + layer, + 1, + 0, + DEFAULT_PLAYOUT_STATE + ) expect(res2.timed).toEqual(['t0', 't1', 't2', 't3']) expect(res2.future).toHaveLength(0) expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2) @@ -138,7 +171,8 @@ describe('findLookaheadForLayer', () => { layer, previousPart, partInfo, - null + null, + expect.objectContaining({ isInHold: expect.any(Boolean), isRehearsal: expect.any(Boolean) }) ) } @@ -167,13 +201,33 @@ describe('findLookaheadForLayer', () => { .mockReturnValueOnce(['t8', 't9'] as any) // Cant search far enough - const res = findLookaheadForLayer(context, null, [], undefined, orderedParts, layer, 1, 1) + const res = findLookaheadForLayer( + context, + null, + [], + undefined, + orderedParts, + layer, + 1, + 1, + DEFAULT_PLAYOUT_STATE + ) 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) + const res2 = findLookaheadForLayer( + context, + null, + [], + undefined, + orderedParts, + layer, + 1, + 4, + DEFAULT_PLAYOUT_STATE + ) expect(res2.timed).toHaveLength(0) expect(res2.future).toEqual(['t0', 't1']) expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(1) @@ -181,7 +235,17 @@ describe('findLookaheadForLayer', () => { // Find the target of 0 findLookaheadObjectsForPartMock.mockReset().mockReturnValue([]) - const res3 = findLookaheadForLayer(context, null, [], undefined, orderedParts, layer, 0, 4) + const res3 = findLookaheadForLayer( + context, + null, + [], + undefined, + orderedParts, + layer, + 0, + 4, + DEFAULT_PLAYOUT_STATE + ) expect(res3.timed).toHaveLength(0) expect(res3.future).toHaveLength(0) expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(0) @@ -196,7 +260,17 @@ describe('findLookaheadForLayer', () => { .mockReturnValueOnce(['t6', 't7'] as any) .mockReturnValueOnce(['t8', 't9'] as any) - const res4 = findLookaheadForLayer(context, null, [], undefined, orderedParts, layer, 100, 5) + const res4 = findLookaheadForLayer( + context, + null, + [], + undefined, + orderedParts, + layer, + 100, + 5, + DEFAULT_PLAYOUT_STATE + ) expect(res4.timed).toHaveLength(0) expect(res4.future).toEqual(['t0', 't1', 't2', 't3', 't4', 't5']) expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(3) @@ -204,4 +278,123 @@ describe('findLookaheadForLayer', () => { expectPartToMatch(2, layer, orderedParts[2], orderedParts[0].part) expectPartToMatch(3, layer, orderedParts[3], orderedParts[2].part) }) + + test('playoutState propagates to findLookaheadObjectsForPart for partInstances', () => { + const layer = getRandomString() + const partInstancesInfo: PartInstanceAndPieceInstances[] = [ + { + part: { _id: '1', part: '1p' }, + allPieces: [createFakePiece('1')], + onTimeline: true, + nowInPart: 2000, + calculatedTimings: { inTransitionStart: null }, + }, + { + part: { _id: '2', part: '2p' }, + allPieces: [createFakePiece('2')], + onTimeline: false, + nowInPart: 0, + calculatedTimings: { inTransitionStart: null }, + }, + ] as any + + findLookaheadObjectsForPartMock.mockReset().mockReturnValue([]) + + // Test with isInHold: true + findLookaheadForLayer(context, null, partInstancesInfo, undefined, [], layer, 1, 1, { + isInHold: true, + isRehearsal: false, + }) + + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2) + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + 1, + context, + null, + layer, + undefined, + expect.any(Object), + partInstancesInfo[0].part._id, + { isInHold: true, isRehearsal: false } + ) + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + 2, + context, + null, + layer, + partInstancesInfo[0].part.part, + expect.any(Object), + partInstancesInfo[1].part._id, + { isInHold: true, isRehearsal: false } + ) + + // Test with isRehearsal: true + findLookaheadObjectsForPartMock.mockReset().mockReturnValue([]) + findLookaheadForLayer(context, null, partInstancesInfo, undefined, [], layer, 1, 1, { + isInHold: false, + isRehearsal: true, + }) + + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2) + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + 1, + context, + null, + layer, + undefined, + expect.any(Object), + partInstancesInfo[0].part._id, + { isInHold: false, isRehearsal: true } + ) + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + 2, + context, + null, + layer, + partInstancesInfo[0].part.part, + expect.any(Object), + partInstancesInfo[1].part._id, + { isInHold: false, isRehearsal: true } + ) + }) + + test('playoutState propagates to findLookaheadObjectsForPart for parts', () => { + const layer = getRandomString() + const orderedParts: PartAndPieces[] = [{ _id: 'p1' }, { _id: 'p2' }].map((p) => ({ + part: p as any, + usesInTransition: true, + pieces: [{ _id: p._id + '_p1' } as any], + })) + + findLookaheadObjectsForPartMock.mockReset().mockReturnValue([]) + + // Test with both flags set - orderedParts (future parts) always get isInHold: false with includeWhenNotInHoldObjects: true + findLookaheadForLayer(context, null, [], undefined, orderedParts, layer, 100, 5, { + isInHold: true, + isRehearsal: true, + }) + + expect(findLookaheadObjectsForPartMock).toHaveBeenCalledTimes(2) + // All future parts get modified playoutState (isInHold forced to false, includeWhenNotInHoldObjects added) + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + 1, + context, + null, + layer, + undefined, + orderedParts[0], + null, + { isInHold: false, isRehearsal: true, includeWhenNotInHoldObjects: true } + ) + expect(findLookaheadObjectsForPartMock).toHaveBeenNthCalledWith( + 2, + context, + null, + layer, + orderedParts[0].part, + orderedParts[1], + null, + { isInHold: false, isRehearsal: true, includeWhenNotInHoldObjects: true } + ) + }) }) 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..ab996b2af81 100644 --- a/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts +++ b/packages/job-worker/src/playout/lookahead/__tests__/lookahead.test.ts @@ -11,7 +11,7 @@ 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 @@ -26,6 +26,8 @@ import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objec 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 @@ -142,7 +144,8 @@ describe('Lookahead', () => { orderedPartsFollowingPlayhead, 'PRELOAD', 1, - LOOKAHEAD_DEFAULT_SEARCH_DISTANCE + LOOKAHEAD_DEFAULT_SEARCH_DISTANCE, + DEFAULT_PLAYOUT_STATE ) expect(findLookaheadForLayerMock).toHaveBeenNthCalledWith( 2, @@ -153,7 +156,8 @@ describe('Lookahead', () => { orderedPartsFollowingPlayhead, 'WHEN_CLEAR', 1, - LOOKAHEAD_DEFAULT_SEARCH_DISTANCE + LOOKAHEAD_DEFAULT_SEARCH_DISTANCE, + DEFAULT_PLAYOUT_STATE ) findLookaheadForLayerMock.mockClear() } @@ -341,6 +345,74 @@ describe('Lookahead', () => { await expectLookaheadForLayerMock(playlistId, [expectedCurrent, expectedNext], 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, + null, + [], + undefined, + fakeParts, + 'PRELOAD', + 1, + LOOKAHEAD_DEFAULT_SEARCH_DISTANCE, + { isInHold: false, isRehearsal: true } + ) + + 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, + null, + [], + undefined, + fakeParts, + 'PRELOAD', + 1, + LOOKAHEAD_DEFAULT_SEARCH_DISTANCE, + { isInHold: true, isRehearsal: false } + ) + + 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, + null, + [], + undefined, + fakeParts, + 'PRELOAD', + 1, + LOOKAHEAD_DEFAULT_SEARCH_DISTANCE, + { isInHold: true, isRehearsal: true } + ) + }) + // eslint-disable-next-line jest/no-commented-out-tests // test('Pieces', () => { // const fakeParts = partIds.map((p) => ({ _id: p })) as Part[] diff --git a/packages/job-worker/src/playout/lookahead/findForLayer.ts b/packages/job-worker/src/playout/lookahead/findForLayer.ts index e09297c0776..b2beaa93a1e 100644 --- a/packages/job-worker/src/playout/lookahead/findForLayer.ts +++ b/packages/job-worker/src/playout/lookahead/findForLayer.ts @@ -5,6 +5,7 @@ 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 @@ -19,7 +20,8 @@ export function findLookaheadForLayer( orderedPartInfos: Array, layer: string, lookaheadTargetFutureObjects: number, - lookaheadMaxSearchDistance: number + lookaheadMaxSearchDistance: number, + playoutState: TimelinePlayoutState ): LookaheadResult { const span = context.startSpan(`findLookaheadForlayer.${layer}`) const res: LookaheadResult = { @@ -49,7 +51,8 @@ export function findLookaheadForLayer( layer, previousPart, partInfo, - partInstanceInfo.part._id + partInstanceInfo.part._id, + playoutState ) if (partInstanceInfo.onTimeline) { @@ -75,7 +78,13 @@ export function findLookaheadForLayer( layer, previousPart, partInfo, - null + 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 diff --git a/packages/job-worker/src/playout/lookahead/findObjects.ts b/packages/job-worker/src/playout/lookahead/findObjects.ts index d96035a74f5..4a1a4860790 100644 --- a/packages/job-worker/src/playout/lookahead/findObjects.ts +++ b/packages/job-worker/src/playout/lookahead/findObjects.ts @@ -13,6 +13,7 @@ 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' function getBestPieceInstanceId(piece: ReadonlyDeep): string { if (!piece.isTemporary || piece.partInstanceId) { @@ -51,12 +52,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) @@ -92,7 +98,8 @@ export function findLookaheadObjectsForPart( layer: string, previousPart: ReadonlyDeep | undefined, partInfo: PartAndPieces, - partInstanceId: PartInstanceId | null + partInstanceId: PartInstanceId | null, + playoutState: TimelinePlayoutState ): Array { // Sanity check, if no part to search, then abort if (!partInfo || partInfo.pieces.length === 0) { @@ -103,7 +110,7 @@ export function findLookaheadObjectsForPart( for (const rawPiece of partInfo.pieces) { if (shouldIgnorePiece(partInfo, rawPiece)) continue - const obj = getObjectMapForPiece(rawPiece).get(layer) + const obj = getObjectMapForPiece(playoutState, rawPiece).get(layer) if (obj) { allObjs.push( literal({ @@ -144,7 +151,7 @@ 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) => { @@ -160,7 +167,7 @@ export function findLookaheadObjectsForPart( } // Note: This is assuming that there is only one use of a layer in each piece. - const obj = getObjectMapForPiece(piece).get(layer) + const obj = getObjectMapForPiece(playoutState, piece).get(layer) if (obj) { const patchedContent = tryActivateKeyframesForObject(obj, !!transitionPiece, classesFromPreviousPart) diff --git a/packages/job-worker/src/playout/lookahead/index.ts b/packages/job-worker/src/playout/lookahead/index.ts index 64ac5a23372..98a3a9e97ee 100644 --- a/packages/job-worker/src/playout/lookahead/index.ts +++ b/packages/job-worker/src/playout/lookahead/index.ts @@ -21,9 +21,10 @@ 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' const LOOKAHEAD_OBJ_PRIORITY = 0.1 @@ -177,6 +178,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) @@ -196,7 +202,8 @@ export async function getLookeaheadObjects( orderedPartInfos, layerId, lookaheadTargetObjects, - lookaheadMaxSearchDistance + lookaheadMaxSearchDistance, + playoutState ) timelineObjs.push(...processResult(lookaheadObjs, mapping.lookahead)) 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/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/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 e4ad4ea7b08..d2848356d6c 100644 --- a/packages/job-worker/src/playout/timeline/rundown.ts +++ b/packages/job-worker/src/playout/timeline/rundown.ts @@ -193,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, + } ) ) @@ -321,8 +324,11 @@ function generateCurrentInfinitePieceObjects( pieceEnable, 0, groupClasses, - isInHold, - isOriginOfInfinite + { + isRehearsal: !!activePlaylist.rehearsal, + isInHold: isInHold, + includeWhenNotInHoldObjects: isOriginOfInfinite, + } ), ] } @@ -493,7 +499,10 @@ function generatePreviousPartInstanceObjects( previousPartGroup, previousPartInfo, currentPartInstanceTimings, - activePlaylist.holdState === RundownHoldState.ACTIVE + { + isRehearsal: !!activePlaylist.rehearsal, + isInHold: activePlaylist.holdState === RundownHoldState.ACTIVE, + } ), ] } else { @@ -536,7 +545,10 @@ function generateNextPartInstanceObjects( nextPartGroup, nextPartInfo, null, - false + { + isRehearsal: !!activePlaylist.rehearsal, + isInHold: false, + } ), ] } diff --git a/packages/shared-lib/src/core/model/Timeline.ts b/packages/shared-lib/src/core/model/Timeline.ts index 2a5c72247d2..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 */ From f6ab86bd48a8cd9cada176fea517d690387824bd Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 8 Jan 2026 12:44:34 +0000 Subject: [PATCH 112/291] chore: Change SCSS @import to @use --- packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss index e7182b8e321..aea1c526671 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); @@ -115,7 +115,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 +404,7 @@ } & > * { - @include item-type-colors(); + @include itemTypeColors.item-type-colors(); } .video-preview__label { From d0acf274539b4d5dbd935b44d3091165006f3acf Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 8 Jan 2026 12:45:08 +0000 Subject: [PATCH 113/291] fix: silence SCSS deprecation warnings from Bootstrap and other libraries --- packages/webui/vite.config.mts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/webui/vite.config.mts b/packages/webui/vite.config.mts index 17c1524bd55..298d8e6b259 100644 --- a/packages/webui/vite.config.mts +++ b/packages/webui/vite.config.mts @@ -59,6 +59,16 @@ export default defineConfig(({ command }) => ({ }, }, + css: { + preprocessorOptions: { + scss: { + // Silence deprecation warnings from Bootstrap and other libraries + // These are caused by Bootstrap 5.x not yet fully supporting Dart Sass 2.x + silenceDeprecations: ['import', 'global-builtin', 'color-functions', 'mixed-decls'], + }, + }, + }, + define: { __APP_VERSION__: JSON.stringify(process.env.npm_package_version), }, From 0cc05742d37e0fc97d76501374fe372c88755aef Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 8 Jan 2026 12:45:47 +0000 Subject: [PATCH 114/291] refactor: Reorganize SCSS structure to improve readability --- .../client/ui/PreviewPopUp/PreviewPopUp.scss | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss index aea1c526671..030b495f906 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss @@ -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,21 @@ 'YTFI' 738, 'YTLC' 548, 'YTUC' 712; + + &--large { + width: 482px; + padding-bottom: 10px; + --preview-max-dimension: 480; + } + + &--small { + width: 322px; + --preview-max-dimension: 320; + } + + &--hidden { + visibility: none; + } } .preview-popUp__preview { From 5f37a3aa94a2bb215d13a6e224c1d3b68417de89 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Mon, 26 Jan 2026 12:48:33 +0000 Subject: [PATCH 115/291] Remove broken PreviewPopUp css hidden feature The CSS was wrong and it wasn't used. We unmount the component when we don't want to see it, which is probably a much better choice for this component. --- packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss | 4 ---- packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.tsx | 4 +--- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss index 030b495f906..eac04fbac9c 100644 --- a/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss +++ b/packages/webui/src/client/ui/PreviewPopUp/PreviewPopUp.scss @@ -43,10 +43,6 @@ width: 322px; --preview-max-dimension: 320; } - - &--hidden { - visibility: none; - } } .preview-popUp__preview { 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} From 3c26cf924a0fdbb52347f1cfc9f2f8d17abec5f4 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Tue, 17 Feb 2026 01:27:31 +0000 Subject: [PATCH 116/291] refactor: use quietDeps instead of silenceDeprecations As suggested by Julusian in PR review, quietDeps is better because: - It hides ALL warnings from dependencies (not just specific types) - It still shows warnings from our own code - This prevents us from accidentally adding deprecated features to our code without being warned --- packages/webui/vite.config.mts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/webui/vite.config.mts b/packages/webui/vite.config.mts index 298d8e6b259..d7a38968a76 100644 --- a/packages/webui/vite.config.mts +++ b/packages/webui/vite.config.mts @@ -62,9 +62,10 @@ export default defineConfig(({ command }) => ({ css: { preprocessorOptions: { scss: { - // Silence deprecation warnings from Bootstrap and other libraries - // These are caused by Bootstrap 5.x not yet fully supporting Dart Sass 2.x - silenceDeprecations: ['import', 'global-builtin', 'color-functions', 'mixed-decls'], + // 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, }, }, }, From 469af0b7f718f444e246c083f9547e6f9ee72026 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 10 Feb 2026 17:28:35 +0000 Subject: [PATCH 117/291] feat: remove deprecated system blueprint migrations --- .../__tests__/migrationContext.test.ts | 194 ------------------ .../server/api/blueprints/migrationContext.ts | 123 ----------- meteor/server/migration/databaseMigration.ts | 124 +---------- .../blueprints-integration/src/api/system.ts | 6 - .../blueprints-integration/src/migrations.ts | 28 +-- packages/meteor-lib/src/api/migration.ts | 3 - 6 files changed, 4 insertions(+), 474 deletions(-) delete mode 100644 meteor/server/api/blueprints/__tests__/migrationContext.test.ts delete mode 100644 meteor/server/api/blueprints/migrationContext.ts 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/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/migration/databaseMigration.ts b/meteor/server/migration/databaseMigration.ts index f2d54d9ef20..16f7e3f9e91 100644 --- a/meteor/server/migration/databaseMigration.ts +++ b/meteor/server/migration/databaseMigration.ts @@ -1,19 +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, @@ -33,11 +27,9 @@ import { GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-lib/dist/collec import { clone, 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' @@ -146,78 +138,12 @@ 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.bind(step), - canBeRunAutomatically: step.canBeRunAutomatically, - migrate: step.migrate?.bind(step), - 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 @@ -283,9 +209,6 @@ 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, @@ -485,9 +393,6 @@ 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) } else throw new Meteor.Error(500, `Unknown step.chunk.sourceType "${step.chunk.sourceType}"`) } @@ -499,9 +404,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 @@ -569,22 +471,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}"`) } } @@ -644,11 +530,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/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/migrations.ts b/packages/blueprints-integration/src/migrations.ts index 19824af1d9a..bb1a7230c34 100644 --- a/packages/blueprints-integration/src/migrations.ts +++ b/packages/blueprints-integration/src/migrations.ts @@ -1,5 +1,3 @@ -import { IBlueprintTriggeredActions } from './triggers.js' - export interface MigrationStepInput { stepId?: string // automatically filled in later label: string @@ -19,32 +17,13 @@ export interface MigrationStepInputFilteredResult { } export type ValidateFunctionCore = (afterMigration: boolean) => Promise -export type ValidateFunctionSystem = ( - context: MigrationContextSystem, - afterMigration: boolean -) => Promise -export type ValidateFunction = ValidateFunctionSystem | ValidateFunctionCore +export type ValidateFunction = ValidateFunctionCore export type MigrateFunctionCore = (input: MigrationStepInputFilteredResult) => Promise -export type MigrateFunctionSystem = ( - context: MigrationContextSystem, - input: MigrationStepInputFilteredResult -) => Promise -export type MigrateFunction = MigrateFunctionSystem | MigrateFunctionCore +export type MigrateFunction = 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 type InputFunction = InputFunctionCore export interface MigrationStepBase< TValidate extends ValidateFunction, @@ -89,4 +68,3 @@ export interface MigrationStep< } export type MigrationStepCore = MigrationStep -export type MigrationStepSystem = MigrationStep diff --git a/packages/meteor-lib/src/api/migration.ts b/packages/meteor-lib/src/api/migration.ts index 80f64856679..3533d92f44a 100644 --- a/packages/meteor-lib/src/api/migration.ts +++ b/packages/meteor-lib/src/api/migration.ts @@ -121,9 +121,6 @@ export interface RunMigrationResult { } export enum MigrationStepType { CORE = 'core', - SYSTEM = 'system', - STUDIO = 'studio', - SHOWSTYLE = 'showstyle', } export interface MigrationChunk { sourceType: MigrationStepType From 877306a8d45d2956e597d44008d1c033d993992c Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 10 Feb 2026 17:35:06 +0000 Subject: [PATCH 118/291] feat: remove support for input during migrations --- meteor/server/api/rest/v1/system.ts | 31 ++----- meteor/server/coreSystem/index.ts | 2 +- meteor/server/lib/rest/v1/system.ts | 6 +- .../migration/__tests__/migrations.test.ts | 23 +---- meteor/server/migration/api.ts | 11 +-- meteor/server/migration/databaseMigration.ts | 76 ++-------------- meteor/server/migration/lib.ts | 2 +- .../src/api/showStyle.ts | 1 - packages/blueprints-integration/src/index.ts | 1 - .../blueprints-integration/src/migrations.ts | 70 --------------- packages/meteor-lib/src/api/migration.ts | 3 - packages/meteor-lib/src/migrations.ts | 40 +++++++++ .../src/client/ui/Settings/Migration.tsx | 89 +------------------ 13 files changed, 60 insertions(+), 295 deletions(-) delete mode 100644 packages/blueprints-integration/src/migrations.ts create mode 100644 packages/meteor-lib/src/migrations.ts 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/coreSystem/index.ts b/meteor/server/coreSystem/index.ts index 5ad804e3fc6..99548ba4321 100644 --- a/meteor/server/coreSystem/index.ts +++ b/meteor/server/coreSystem/index.ts @@ -86,7 +86,7 @@ async function initializeCoreSystem() { const migration = await prepareMigration(true) if (migration.migrationNeeded && migration.manualStepCount === 0 && 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/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/__tests__/migrations.test.ts b/meteor/server/migration/__tests__/migrations.test.ts index e41ead05a1c..84ee75c1472 100644 --- a/meteor/server/migration/__tests__/migrations.test.ts +++ b/meteor/server/migration/__tests__/migrations.test.ts @@ -4,9 +4,8 @@ import { ICoreSystem, GENESIS_SYSTEM_VERSION } from '@sofie-automation/meteor-li 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 { MigrationStepCore, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' +import { MigrationStepCore } from '@sofie-automation/blueprints-integration' import { DBStudio } from '@sofie-automation/corelib/dist/dataModel/Studio' import { MeteorCall } from '../../api/methods' import { wrapDefaultObject } from '@sofie-automation/corelib/dist/settings/objectWithOverrides' @@ -36,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) @@ -76,8 +60,7 @@ describe('Migrations', () => { const migrationResult0: RunMigrationResult = await MeteorCall.migration.runMigration( migrationStatus0.migration.chunks, - migrationStatus0.migration.hash, - userInput(migrationStatus0) + migrationStatus0.migration.hash ) expect(migrationResult0).toMatchObject({ 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/databaseMigration.ts b/meteor/server/migration/databaseMigration.ts index 16f7e3f9e91..419c2ff681d 100644 --- a/meteor/server/migration/databaseMigration.ts +++ b/meteor/server/migration/databaseMigration.ts @@ -1,18 +1,13 @@ import { Meteor } from 'meteor/meteor' import * as semver from 'semver' import { - InputFunctionCore, MigrateFunctionCore, MigrationStep, - MigrationStepInput, - MigrationStepInputFilteredResult, - MigrationStepInputResult, ValidateFunctionCore, ValidateFunction, MigrateFunction, - InputFunction, MigrationStepCore, -} from '@sofie-automation/blueprints-integration' +} from '@sofie-automation/meteor-lib/dist/migrations' import _ from 'underscore' import { GetMigrationStatusResult, @@ -24,7 +19,7 @@ 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 { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' @@ -57,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 @@ -92,7 +87,6 @@ export interface PreparedMigration { automaticStepCount: number manualStepCount: number ignoredStepCount: number - manualInputs: MigrationStepInput[] partialMigration: boolean } export async function prepareMigration(returnAllChunks?: boolean): Promise { @@ -128,7 +122,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 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++ } @@ -288,7 +251,6 @@ export async function prepareMigration(returnAllChunks?: boolean): Promise, hash: string, - inputResults: Array, isFirstOfPartialMigrations = true, chunksLeft = 20 ): Promise { @@ -310,19 +271,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) => { @@ -373,18 +324,8 @@ export async function runMigration( `Migration: ${migration.automaticStepCount} automatic and ${migration.manualStepCount} manual steps (${migration.ignoredStepCount} ignored).` ) - logger.debug(inputResults) - 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) { @@ -392,7 +333,7 @@ export async function runMigration( if (step.chunk.sourceType === MigrationStepType.CORE) { const migration = step.migrate as MigrateFunctionCore - await migration(stepInput) + await migration() } else throw new Meteor.Error(500, `Unknown step.chunk.sourceType "${step.chunk.sourceType}"`) } @@ -431,13 +372,7 @@ export async function runMigration( const s = await getMigrationStatus() if (s.migration.automaticStepCount > 0 || s.migration.manualStepCount > 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 } @@ -491,7 +426,6 @@ export async function getMigrationStatus(): Promise { migration: { canDoAutomaticMigration: migration.manualStepCount === 0, - manualInputs: migration.manualInputs, hash: migration.hash, chunks: migration.chunks, 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/packages/blueprints-integration/src/api/showStyle.ts b/packages/blueprints-integration/src/api/showStyle.ts index 5afe7317069..0ea002a9840 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, 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/migrations.ts b/packages/blueprints-integration/src/migrations.ts deleted file mode 100644 index bb1a7230c34..00000000000 --- a/packages/blueprints-integration/src/migrations.ts +++ /dev/null @@ -1,70 +0,0 @@ -export interface MigrationStepInput { - stepId?: string // automatically filled in later - label: string - description?: string - inputType: 'text' | 'multiline' | 'int' | 'checkbox' | 'dropdown' | 'toggle' | null // EditAttribute types, null = dont display edit field - attribute: string | null - defaultValue?: any - dropdownOptions?: string[] -} -export interface MigrationStepInputResult { - stepId: string - attribute: string - value: any -} -export interface MigrationStepInputFilteredResult { - [attribute: string]: any -} - -export type ValidateFunctionCore = (afterMigration: boolean) => Promise -export type ValidateFunction = ValidateFunctionCore - -export type MigrateFunctionCore = (input: MigrationStepInputFilteredResult) => Promise -export type MigrateFunction = MigrateFunctionCore - -export type InputFunctionCore = () => MigrationStepInput[] -export type InputFunction = InputFunctionCore - -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 diff --git a/packages/meteor-lib/src/api/migration.ts b/packages/meteor-lib/src/api/migration.ts index 3533d92f44a..325cba8dc97 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 @@ -104,7 +102,6 @@ export interface GetMigrationStatusResult { migration: { canDoAutomaticMigration: boolean - manualInputs: Array hash: string automaticStepCount: number manualStepCount: number diff --git a/packages/meteor-lib/src/migrations.ts b/packages/meteor-lib/src/migrations.ts new file mode 100644 index 00000000000..f075186cbbf --- /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 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 */ + 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/webui/src/client/ui/Settings/Migration.tsx b/packages/webui/src/client/ui/Settings/Migration.tsx index 16e40a3a056..158dee793ad 100644 --- a/packages/webui/src/client/ui/Settings/Migration.tsx +++ b/packages/webui/src/client/ui/Settings/Migration.tsx @@ -10,9 +10,7 @@ import { RunMigrationResult, MigrationChunk, } from '@sofie-automation/meteor-lib/dist/api/migration' -import { MigrationStepInput, MigrationStepInputResult } from '@sofie-automation/blueprints-integration' import _ from 'underscore' -import { EditAttribute } from '../../lib/EditAttribute.js' import { MeteorCall } from '../../lib/meteorApi.js' import { checkForOldDataAndCleanUp } from './SystemManagement.js' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' @@ -27,7 +25,6 @@ interface IState { migration?: { canDoAutomaticMigration: boolean - manualInputs: Array hash: string chunks: Array automaticStepCount: number @@ -40,12 +37,6 @@ interface IState { partialMigration: boolean haveRunMigration: boolean - - inputValues: { - [stepId: string]: { - [attribute: string]: any - } - } } interface ITrackedProps {} export const MigrationView = translateWithTracker((_props: IProps) => { @@ -64,8 +55,6 @@ export const MigrationView = translateWithTracker migrationCompleted: false, partialMigration: false, haveRunMigration: false, - - inputValues: {}, } } componentDidMount(): void { @@ -98,24 +87,9 @@ export const MigrationView = translateWithTracker .then((r: GetMigrationStatusResult) => { if (this.cancelRequests) return - const inputValues = this.state.inputValues - _.each(r.migration.manualInputs, (manualInput: MigrationStepInput) => { - if (manualInput.stepId && manualInput.inputType && manualInput.attribute) { - const stepId = manualInput.stepId - - if (!inputValues[stepId]) inputValues[stepId] = {} - - const value = inputValues[stepId][manualInput.attribute] - if (_.isUndefined(value)) { - inputValues[stepId][manualInput.attribute] = manualInput.defaultValue - } - } - }) - this.setState({ migrationNeeded: r.migrationNeeded, migration: r.migration, - inputValues: inputValues, }) }) .catch((err) => { @@ -124,29 +98,12 @@ export const MigrationView = translateWithTracker }) } runMigration() { - const inputResults: Array = [] - if (this.state.migration) { - _.each(this.state.migration.manualInputs, (manualInput) => { - if (manualInput.stepId && manualInput.attribute) { - let value: any - const step = this.state.inputValues[manualInput.stepId] - if (step) { - value = step[manualInput.attribute] - } - inputResults.push({ - stepId: manualInput.stepId, - attribute: manualInput.attribute, - value: value, - }) - } - }) this.setErrorMessage('') MeteorCall.migration .runMigration( this.state.migration.chunks, - this.state.migration.hash, // hash - inputResults // inputResults + this.state.migration.hash // hash ) .then((r: RunMigrationResult) => { if (this.cancelRequests) return @@ -212,49 +169,6 @@ export const MigrationView = translateWithTracker checkForOldData() { checkForOldDataAndCleanUp(this.props.t, 3) } - renderManualSteps() { - if (this.state.migration) { - let rank = 0 - return _.map(this.state.migration.manualInputs, (manualInput: MigrationStepInput) => { - if (manualInput.stepId) { - const stepId = manualInput.stepId - let value - if (manualInput.attribute) { - value = (this.state.inputValues[stepId] || {})[manualInput.attribute] - } - return ( -
-

{manualInput.label}

-
{manualInput.description}
-
- {manualInput.inputType && manualInput.attribute ? ( - { - if (manualInput.attribute) { - const inputValues = this.state.inputValues - if (!inputValues[stepId]) inputValues[stepId] = {} - inputValues[stepId][manualInput.attribute] = newValue - - this.setState({ - inputValues: inputValues, - }) - } - }} - /> - ) : null} -
-
- ) - } else { - return null - } - }) - } - } render(): JSX.Element { const { t } = this.props @@ -367,7 +281,6 @@ export const MigrationView = translateWithTracker

{t('The migration procedure needs some help from you in order to complete, see below:')}

-
{this.renderManualSteps()}
- - - -
- - ) - } - } -) + }, + [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..f80a5f6361a 100644 --- a/packages/webui/src/client/ui/Settings/SnapshotsView.tsx +++ b/packages/webui/src/client/ui/Settings/SnapshotsView.tsx @@ -1,14 +1,11 @@ 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 { NotificationCenter, Notification, NoticeLevel } from '../../lib/notifications/notifications.js' import { UploadButton } from '../../lib/uploadButton.js' @@ -19,32 +16,21 @@ 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' -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() + + const [removeSnapshots, setRemoveSnapshots] = React.useState(false) + const toggleRemoveView = React.useCallback(() => setRemoveSnapshots((old) => !old), []) -export default function SnapshotsView(props: Readonly): JSX.Element { - // // Subscribe to data: + const [editSnapshotId, setEditSnapshotId] = React.useState(null) + + // Subscribe to data: useSubscription(MeteorPubSub.snapshots) useSubscription(CorelibPubSub.studios, null) @@ -63,309 +49,138 @@ 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')}

- -
- -
- - {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} + {studios.map((studio) => { + return ( +
+ +
+ ) + })}
+ ) : null} +
-

{t('Restore from Snapshot File')}

+

{t('Restore from Snapshot File')}

-

- - {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')} - -

+

+ + {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 Stored Snapshots')}

-
- - - - - - - - {this.state.removeSnapshots ? : null} - - {_.map(this.props.snapshots, (snapshot) => { - return ( - - - - - + {removeSnapshots ? ( + + ) : null} + + ) + })} + +
TypeNameComment
- - {snapshot.type} - - {snapshot.name} - - - {this.state.editSnapshotId === snapshot._id ? ( -
- +

{t('Restore from Stored Snapshots')}

+
+ + + + + + + + {removeSnapshots ? : null} + + {snapshots.map((snapshot) => { + return ( + + + + + - {this.state.removeSnapshots ? ( - - ) : null} - - ) - })} - -
{t('Type')}{t('Name')}{t('Comment')}
+ + {snapshot.type} + + {snapshot.name} + + + {editSnapshotId === snapshot._id ? ( +
+ - -
- ) : ( - { - e.preventDefault() - this.editSnapshot(snapshot._id) - }} - > - {multilineText(snapshot.comment)} - - )} -
- -
- -
+ +
+ ) : ( + { + e.preventDefault() + setEditSnapshotId(snapshot._id) + }} + > + {multilineText(snapshot.comment)} + + )} +
+ +
+ - ) - } - } -) +
+
+ ) +} function SnapshotImportButton({ restoreVariant, @@ -441,3 +256,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/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) => ( -} +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} -// -// -// -// -// ) -// })} -// -// ) -// } -// } -// ) From a847a82723ed6f81fbf45c47c42517b6e80a0b31 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 18 Feb 2026 12:51:06 +0000 Subject: [PATCH 131/291] chore: fix failing import --- packages/webui/src/client/ui/Settings/BlueprintSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/ui/Settings/BlueprintSettings.tsx b/packages/webui/src/client/ui/Settings/BlueprintSettings.tsx index 7d38233e0fc..45d8bafe264 100644 --- a/packages/webui/src/client/ui/Settings/BlueprintSettings.tsx +++ b/packages/webui/src/client/ui/Settings/BlueprintSettings.tsx @@ -22,7 +22,7 @@ import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyE 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.js' +import { assertNever } from '@sofie-automation/corelib/dist/lib' interface IProps { blueprintId: BlueprintId From f6227499aa9875109c8647ed8aeab1a5f5d7af70 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Tue, 17 Feb 2026 20:15:57 +0000 Subject: [PATCH 132/291] Merge pull request #1650 from Sofie-Automation/rjmunro/update-docs Update docs for release52 and release53/26.03 --- .../device-integrations/tsr-plugins.md | 2 +- .../docs/user-guide/features/prompter.md | 26 +- packages/documentation/docusaurus.config.js | 9 - .../src/components/GitHubReleases.jsx | 2 +- .../version-1.52.0/about-sofie.md | 22 + .../for-developers/api-documentation.md | 8 + .../for-developers/api-stability.md | 26 ++ .../for-developers/contribution-guidelines.md | 108 +++++ .../for-developers/data-model.md | 132 ++++++ .../device-integrations/_category_.json | 4 + .../device-integrations/intro.md | 18 + .../options-and-mappings.md | 11 + .../device-integrations/tsr-actions.md | 11 + .../device-integrations/tsr-api.md | 28 ++ .../device-integrations/tsr-types.md | 7 + .../for-blueprint-developers/_category_.json | 4 + .../_part-timings-demo.jsx | 173 +++++++ .../for-blueprint-developers/ab-playback.md | 236 ++++++++++ .../for-blueprint-developers/hold.md | 52 +++ .../for-blueprint-developers/intro.md | 20 + .../for-blueprint-developers/lookahead.md | 96 ++++ .../manipulating-ingest-data.md | 139 ++++++ .../part-and-piece-timings.mdx | 141 ++++++ .../sync-ingest-changes.md | 23 + .../timeline-datastore.md | 85 ++++ .../version-1.52.0/for-developers/intro.md | 15 + .../for-developers/json-config-schema.md | 218 +++++++++ .../for-developers/libraries.md | 56 +++ .../for-developers/mos-plugins.md | 185 ++++++++ .../for-developers/npm-package-publishing.md | 23 + .../for-developers/publications.md | 43 ++ .../for-developers/url-query-parameters.md | 25 + .../worker-threads-and-locks.md | 61 +++ .../user-guide/concepts-and-architecture.md | 192 ++++++++ .../user-guide/configuration/_category_.json | 4 + .../user-guide/configuration/settings-view.md | 181 ++++++++ .../configuration/sofie-core-settings.md | 110 +++++ .../version-1.52.0/user-guide/faq.md | 16 + .../user-guide/features/_category_.json | 4 + .../user-guide/features/access-levels.md | 64 +++ .../version-1.52.0/user-guide/features/api.md | 19 + .../user-guide/features/language.md | 23 + .../user-guide/features/prompter.md | 199 ++++++++ .../user-guide/features/sofie-views.mdx | 333 +++++++++++++ .../user-guide/features/system-health.md | 27 ++ .../user-guide/further-reading.md | 59 +++ .../user-guide/installation/_category_.json | 4 + .../installation/initial-sofie-core-setup.md | 23 + .../installing-a-gateway/_category_.json | 4 + .../installing-a-gateway/intro.md | 25 + .../installing-a-gateway/playout-gateway.md | 6 + .../_category_.json | 4 + .../inews-gateway.md | 12 + ...g-sofie-with-google-spreadsheet-support.md | 46 ++ .../intro.md | 17 + .../mos-gateway.md | 9 + .../installation/installing-blueprints.md | 46 ++ .../README.md | 35 ++ .../_category_.json | 4 + .../casparcg-server-installation.md | 224 +++++++++ .../ffmpeg-installation.md | 35 ++ .../vision-mixers.md | 14 + .../installation/installing-input-gateway.md | 45 ++ .../installing-package-manager.md | 210 +++++++++ .../installing-sofie-server-core.md | 172 +++++++ .../user-guide/installation/intro.md | 37 ++ .../user-guide/installation/media-manager.md | 20 + .../user-guide/installation/rundown-editor.md | 18 + .../version-1.52.0/user-guide/intro.md | 41 ++ .../user-guide/supported-devices.md | 119 +++++ .../version-26.03.0/about-sofie.md | 20 + .../for-developers/api-documentation.md | 8 + .../for-developers/api-stability.md | 26 ++ .../for-developers/contribution-guidelines.md | 118 +++++ .../for-developers/data-model.md | 130 ++++++ .../device-integrations/_category_.json | 4 + .../device-integrations/intro.md | 18 + .../options-and-mappings.md | 11 + .../shared-hardware-control.md | 68 +++ .../device-integrations/tsr-actions.md | 11 + .../device-integrations/tsr-api.md | 28 ++ .../device-integrations/tsr-plugins.md | 124 +++++ .../device-integrations/tsr-types.md | 7 + .../for-blueprint-developers/_category_.json | 4 + .../_part-timings-demo.jsx | 173 +++++++ .../for-blueprint-developers/ab-playback.md | 236 ++++++++++ .../for-blueprint-developers/hold.md | 52 +++ .../for-blueprint-developers/intro.md | 37 ++ .../for-blueprint-developers/lookahead.md | 96 ++++ .../manipulating-ingest-data.md | 139 ++++++ .../for-blueprint-developers/mos-statuses.md | 53 +++ .../part-and-piece-timings.mdx | 141 ++++++ .../sync-ingest-changes.md | 23 + .../timeline-datastore.md | 85 ++++ .../version-26.03.0/for-developers/intro.md | 15 + .../for-developers/json-config-schema.md | 218 +++++++++ .../for-developers/libraries.md | 55 +++ .../for-developers/mos-plugins.md | 185 ++++++++ .../for-developers/npm-package-publishing.md | 23 + .../for-developers/publications.md | 43 ++ .../for-developers/url-query-parameters.md | 25 + .../worker-threads-and-locks.md | 61 +++ .../user-guide/concepts-and-architecture.md | 191 ++++++++ .../user-guide/configuration/_category_.json | 4 + .../user-guide/configuration/settings-view.md | 202 ++++++++ .../configuration/sofie-core-settings.md | 110 +++++ .../version-26.03.0/user-guide/faq.md | 16 + .../user-guide/features/_category_.json | 4 + .../user-guide/features/access-levels.md | 64 +++ .../user-guide/features/api.md | 19 + .../user-guide/features/intro.md | 18 + .../user-guide/features/language.md | 25 + .../user-guide/features/prompter.md | 245 ++++++++++ .../features/sofie-views-and-screens.mdx | 439 ++++++++++++++++++ .../user-guide/features/system-health.md | 27 ++ .../user-guide/further-reading.md | 59 +++ .../user-guide/installation/_category_.json | 4 + .../installation/initial-sofie-core-setup.md | 23 + .../installing-a-gateway/_category_.json | 4 + .../installing-a-gateway/input-gateway.md | 53 +++ .../installing-a-gateway/intro.md | 41 ++ .../installing-a-gateway/playout-gateway.md | 9 + .../_category_.json | 4 + .../google-spreadsheet.md | 52 +++ .../inews-gateway.md | 8 + .../intro.md | 21 + .../mos-gateway.md | 19 + .../installation/installing-blueprints.md | 46 ++ .../README.md | 35 ++ .../_category_.json | 4 + .../casparcg-server-installation.md | 224 +++++++++ .../ffmpeg-installation.md | 35 ++ .../vision-mixers.md | 13 + .../installing-package-manager.md | 205 ++++++++ .../installing-sofie-server-core.md | 23 + .../user-guide/installation/intro.md | 25 + .../user-guide/installation/quick-install.md | 172 +++++++ .../user-guide/installation/rundown-editor.md | 18 + .../version-26.03.0/user-guide/intro.md | 41 ++ .../user-guide/supported-devices.md | 118 +++++ .../version-1.52.0-sidebars.json | 14 + .../version-26.03.0-sidebars.json | 14 + packages/documentation/versions.json | 2 + 143 files changed, 9273 insertions(+), 24 deletions(-) create mode 100644 packages/documentation/versioned_docs/version-1.52.0/about-sofie.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/api-documentation.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/api-stability.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/contribution-guidelines.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/data-model.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/options-and-mappings.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-actions.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-api.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/device-integrations/tsr-types.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/_part-timings-demo.jsx create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/ab-playback.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/hold.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/lookahead.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/manipulating-ingest-data.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/part-and-piece-timings.mdx create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/sync-ingest-changes.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/for-blueprint-developers/timeline-datastore.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/json-config-schema.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/libraries.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/mos-plugins.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/npm-package-publishing.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/publications.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/url-query-parameters.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/for-developers/worker-threads-and-locks.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/concepts-and-architecture.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/settings-view.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/configuration/sofie-core-settings.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/faq.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/access-levels.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/api.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/language.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/prompter.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/sofie-views.mdx create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/features/system-health.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/further-reading.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/initial-sofie-core-setup.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/playout-gateway.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md create mode 100644 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 create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-blueprints.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/README.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/_category_.json create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-connections-and-additional-hardware/vision-mixers.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-input-gateway.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-package-manager.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-sofie-server-core.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/media-manager.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/rundown-editor.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/intro.md create mode 100644 packages/documentation/versioned_docs/version-1.52.0/user-guide/supported-devices.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/about-sofie.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/api-documentation.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/api-stability.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/contribution-guidelines.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/data-model.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/options-and-mappings.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/shared-hardware-control.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-actions.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-api.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-plugins.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/device-integrations/tsr-types.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/_part-timings-demo.jsx create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/ab-playback.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/hold.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/lookahead.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/manipulating-ingest-data.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/mos-statuses.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/part-and-piece-timings.mdx create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/sync-ingest-changes.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/for-blueprint-developers/timeline-datastore.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/json-config-schema.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/libraries.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/mos-plugins.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/npm-package-publishing.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/publications.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/url-query-parameters.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/for-developers/worker-threads-and-locks.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/concepts-and-architecture.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/settings-view.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/configuration/sofie-core-settings.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/faq.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/access-levels.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/api.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/language.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/prompter.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/sofie-views-and-screens.mdx create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/features/system-health.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/further-reading.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/initial-sofie-core-setup.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/input-gateway.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/playout-gateway.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/google-spreadsheet.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/inews-gateway.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-a-gateway/rundown-or-newsroom-system-connection/mos-gateway.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-blueprints.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/README.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/_category_.json create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/casparcg-server-installation.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/ffmpeg-installation.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-connections-and-additional-hardware/vision-mixers.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-package-manager.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/installing-sofie-server-core.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/quick-install.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/installation/rundown-editor.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/intro.md create mode 100644 packages/documentation/versioned_docs/version-26.03.0/user-guide/supported-devices.md create mode 100644 packages/documentation/versioned_sidebars/version-1.52.0-sidebars.json create mode 100644 packages/documentation/versioned_sidebars/version-26.03.0-sidebars.json 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..b6cd77ceeb4 100644 --- a/packages/documentation/docs/for-developers/device-integrations/tsr-plugins.md +++ b/packages/documentation/docs/for-developers/device-integrations/tsr-plugins.md @@ -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/user-guide/features/prompter.md b/packages/documentation/docs/user-guide/features/prompter.md index aba0e34ec04..7490dafc88c 100644 --- a/packages/documentation/docs/user-guide/features/prompter.md +++ b/packages/documentation/docs/user-guide/features/prompter.md @@ -46,7 +46,7 @@ The prompter can be controlled by different types of controllers. The control mo | `?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=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\) @@ -160,16 +160,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 +226,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/docusaurus.config.js b/packages/documentation/docusaurus.config.js index 386e01ebf45..34cf510e12a 100644 --- a/packages/documentation/docusaurus.config.js +++ b/packages/documentation/docusaurus.config.js @@ -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/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.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/versioned_docs/version-1.52.0/user-guide/installation/installing-input-gateway.md b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-input-gateway.md new file mode 100644 index 00000000000..d17f66d247e --- /dev/null +++ b/packages/documentation/versioned_docs/version-1.52.0/user-guide/installation/installing-input-gateway.md @@ -0,0 +1,45 @@ +# 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), 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. + +### 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-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..b6cd77ceeb4 --- /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 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. + +## 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", From 91cd1aba52d5ba345b6f388302e415bcf93ffbcb Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:37:06 +0000 Subject: [PATCH 133/291] chore: Bump TSR version --- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 90 +++++++++++++++++---------- 3 files changed, 60 insertions(+), 34 deletions(-) diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 3f96eeccfd1..914b19da1ef 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -56,7 +56,7 @@ "@sofie-automation/shared-lib": "26.3.0-1", "debug": "^4.4.0", "influx": "^5.9.7", - "timeline-state-resolver": "10.0.0-nightly-release53-20251217-143607-df590aa96.0", + "timeline-state-resolver": "10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0", "tslib": "^2.8.1", "underscore": "^1.13.7", "winston": "^3.17.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index dc6c91f23b0..3da5de11391 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -38,7 +38,7 @@ "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-release-26-03-20260212-104218-c1745408c.0", "tslib": "^2.8.1", "type-fest": "^4.33.0" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 417781df9b8..f71bde5c2c5 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -7027,7 +7027,7 @@ __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-release-26-03-20260212-104218-c1745408c.0" tslib: "npm:^2.8.1" type-fest: "npm:^4.33.0" languageName: unknown @@ -10032,16 +10032,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.2.1": + version: 1.2.1 + resolution: "atem-state@npm:1.2.1" 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/be74a217e6310a4cadb8883b8bfe76c3df0bebbea30194deef63995c777cec72cd9017383faec7f760ce7033398ed9817ad1a3576d20075cfab56f761ecf2611 languageName: node linkType: hard @@ -19109,7 +19109,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"kairos-lib@npm:^0.2.3": +"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": version: 0.2.3 resolution: "kairos-lib@npm:0.2.3" dependencies: @@ -23923,7 +23933,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": "npm:26.3.0-1" debug: "npm:^4.4.0" influx: "npm:^5.9.7" - timeline-state-resolver: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" + timeline-state-resolver: "npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0" tslib: "npm:^2.8.1" underscore: "npm:^1.13.7" winston: "npm:^3.17.0" @@ -28622,43 +28632,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-release-26-03-20260212-104218-c1745408c.0": + version: 10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0 + resolution: "timeline-state-resolver-api@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.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-release-26-03-20260212-104218-c1745408c.0 + checksum: 10/0e49376cd52fa073f2927e23aafce081e8c863a8a34c9ef178225a9735fc56e3fecb01ad25ddfe3607ddef142d416154644d3d871be768f65255ec6244eba3d1 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-release-26-03-20260212-104218-c1745408c.0": + version: 10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0 + resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0" dependencies: + kairos-lib: "npm:0.2.3" tslib: "npm:^2.8.1" - checksum: 10/6f17030e9f10568757b3ebaae40a54ce5220ce5c2f37d9b5d8a5d286704e4779c80fbfddae500e1952a6efdf6e76b67bc18d0151211c84eb194ae3b52f78c7bf + checksum: 10/33dba430305d68ab78bddf697643c9da3b33d3a428e08e10efbd94afa6b6cd91b8a52eb7231846e58a622359f7b3d6314663ba6db440c96bff9a305d06692db1 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-release-26-03-20260212-104218-c1745408c.0": + version: 10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0 + resolution: "timeline-state-resolver@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.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.2.1" 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 +28679,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-release-26-03-20260212-104218-c1745408c.0" + timeline-state-resolver-types: "npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.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/fe76fdd9a89872c7bf1147e04300aa117f524e761f15830dd88b4e2fc32ad4ac905afa33f49fb9761b204d121f247bf048014c4c349cae02282c5457c172e33e languageName: node linkType: hard @@ -29941,13 +29952,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 @@ -31024,7 +31035,7 @@ 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": +"ws@npm:^8.11.0, ws@npm:^8.13.0, ws@npm:^8.18.0": version: 8.18.3 resolution: "ws@npm:8.18.3" peerDependencies: @@ -31039,6 +31050,21 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard +"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" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 10/26e4901e93abaf73af9f26a93707c95b4845e91a7a347ec8c569e6e9be7f9df066f6c2b817b2d685544e208207898a750b78461e6e8d810c11a370771450c31b + languageName: node + linkType: hard + "wsl-utils@npm:^0.1.0": version: 0.1.0 resolution: "wsl-utils@npm:0.1.0" From cd3cd9d0f0f00ce3265875ad5b1c923eee19194d Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:52:47 +0000 Subject: [PATCH 134/291] chore(release): 26.3.0-2 --- meteor/CHANGELOG.md | 8 +++ meteor/package.json | 2 +- meteor/yarn.lock | 31 +++++----- packages/blueprints-integration/CHANGELOG.md | 8 +++ packages/blueprints-integration/package.json | 4 +- packages/corelib/package.json | 6 +- packages/documentation/package.json | 2 +- packages/job-worker/package.json | 8 +-- packages/lerna.json | 2 +- packages/live-status-gateway-api/package.json | 2 +- packages/live-status-gateway/package.json | 12 ++-- packages/meteor-lib/package.json | 8 +-- packages/mos-gateway/CHANGELOG.md | 11 ++++ packages/mos-gateway/package.json | 6 +- packages/openapi/package.json | 2 +- packages/playout-gateway/CHANGELOG.md | 11 ++++ packages/playout-gateway/package.json | 6 +- packages/server-core-integration/CHANGELOG.md | 8 +++ packages/server-core-integration/package.json | 4 +- packages/shared-lib/package.json | 2 +- packages/webui/package.json | 10 ++-- packages/yarn.lock | 58 +++++++++---------- 22 files changed, 129 insertions(+), 82 deletions(-) diff --git a/meteor/CHANGELOG.md b/meteor/CHANGELOG.md index 6ad55b67d9d..8e1e72f9f1e 100644 --- a/meteor/CHANGELOG.md +++ b/meteor/CHANGELOG.md @@ -2,6 +2,14 @@ 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) diff --git a/meteor/package.json b/meteor/package.json index 532841b7b48..b2334ff4ce5 100644 --- a/meteor/package.json +++ b/meteor/package.json @@ -1,6 +1,6 @@ { "name": "automation-core", - "version": "26.3.0-1", + "version": "26.3.0-2", "private": true, "engines": { "node": ">=22.20.0" diff --git a/meteor/yarn.lock b/meteor/yarn.lock index c5122ba281a..ec26acae30c 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -1158,7 +1158,7 @@ __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:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-2" tslib: "npm:^2.8.1" type-fest: "npm:^4.33.0" languageName: node @@ -1194,8 +1194,8 @@ __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:26.3.0-1" - "@sofie-automation/shared-lib": "npm:26.3.0-1" + "@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" @@ -1227,9 +1227,9 @@ __metadata: 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:26.3.0-1" - "@sofie-automation/corelib": "npm:26.3.0-1" - "@sofie-automation/shared-lib": "npm:26.3.0-1" + "@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" deepmerge: "npm:^4.3.1" elastic-apm-node: "npm:^4.11.0" @@ -1249,9 +1249,9 @@ __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:26.3.0-1" - "@sofie-automation/corelib": "npm:26.3.0-1" - "@sofie-automation/shared-lib": "npm:26.3.0-1" + "@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" @@ -1268,7 +1268,7 @@ __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-release-26-03-20260212-104218-c1745408c.0" tslib: "npm:^2.8.1" type-fest: "npm:^4.33.0" languageName: node @@ -6712,7 +6712,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: @@ -10043,12 +10043,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-release-26-03-20260212-104218-c1745408c.0": + version: 10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0 + resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0" dependencies: + kairos-lib: "npm:0.2.3" tslib: "npm:^2.8.1" - checksum: 10/6f17030e9f10568757b3ebaae40a54ce5220ce5c2f37d9b5d8a5d286704e4779c80fbfddae500e1952a6efdf6e76b67bc18d0151211c84eb194ae3b52f78c7bf + checksum: 10/33dba430305d68ab78bddf697643c9da3b33d3a428e08e10efbd94afa6b6cd91b8a52eb7231846e58a622359f7b3d6314663ba6db440c96bff9a305d06692db1 languageName: node linkType: hard diff --git a/packages/blueprints-integration/CHANGELOG.md b/packages/blueprints-integration/CHANGELOG.md index d122dcf11ba..375875e55b1 100644 --- a/packages/blueprints-integration/CHANGELOG.md +++ b/packages/blueprints-integration/CHANGELOG.md @@ -3,6 +3,14 @@ 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 diff --git a/packages/blueprints-integration/package.json b/packages/blueprints-integration/package.json index 7c8050b9ce9..53bd5a17388 100644 --- a/packages/blueprints-integration/package.json +++ b/packages/blueprints-integration/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/blueprints-integration", - "version": "26.3.0-1", + "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", @@ -36,7 +36,7 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/shared-lib": "26.3.0-1", + "@sofie-automation/shared-lib": "26.3.0-2", "tslib": "^2.8.1", "type-fest": "^4.33.0" }, diff --git a/packages/corelib/package.json b/packages/corelib/package.json index ac4b5ec3e6f..a5c54eb82aa 100644 --- a/packages/corelib/package.json +++ b/packages/corelib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/corelib", - "version": "26.3.0-1", + "version": "26.3.0-2", "private": true, "description": "Internal library for some types shared by core and workers", "main": "dist/index.js", @@ -37,8 +37,8 @@ "/LICENSE" ], "dependencies": { - "@sofie-automation/blueprints-integration": "26.3.0-1", - "@sofie-automation/shared-lib": "26.3.0-1", + "@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", diff --git a/packages/documentation/package.json b/packages/documentation/package.json index 09781a84b9a..5bcc19685cd 100644 --- a/packages/documentation/package.json +++ b/packages/documentation/package.json @@ -1,6 +1,6 @@ { "name": "sofie-documentation", - "version": "26.3.0-1", + "version": "26.3.0-2", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/packages/job-worker/package.json b/packages/job-worker/package.json index d9b664fdb3c..38abe98ecb2 100644 --- a/packages/job-worker/package.json +++ b/packages/job-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/job-worker", - "version": "26.3.0-1", + "version": "26.3.0-2", "description": "Worker for things", "main": "dist/index.js", "license": "MIT", @@ -37,9 +37,9 @@ ], "dependencies": { "@slack/webhook": "^7.0.4", - "@sofie-automation/blueprints-integration": "26.3.0-1", - "@sofie-automation/corelib": "26.3.0-1", - "@sofie-automation/shared-lib": "26.3.0-1", + "@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", "deepmerge": "^4.3.1", "elastic-apm-node": "^4.11.0", diff --git a/packages/lerna.json b/packages/lerna.json index 2dbe1e61dd9..2d2b10ddfd6 100644 --- a/packages/lerna.json +++ b/packages/lerna.json @@ -1,5 +1,5 @@ { - "version": "26.3.0-1", + "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/package.json b/packages/live-status-gateway-api/package.json index fb415751779..09f32a30437 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": "26.3.0-1", + "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", diff --git a/packages/live-status-gateway/package.json b/packages/live-status-gateway/package.json index 28ed0561b37..81ca091e5ed 100644 --- a/packages/live-status-gateway/package.json +++ b/packages/live-status-gateway/package.json @@ -1,6 +1,6 @@ { "name": "live-status-gateway", - "version": "26.3.0-1", + "version": "26.3.0-2", "private": true, "description": "Provides state from Sofie over sockets", "license": "MIT", @@ -47,11 +47,11 @@ "production" ], "dependencies": { - "@sofie-automation/blueprints-integration": "26.3.0-1", - "@sofie-automation/corelib": "26.3.0-1", - "@sofie-automation/live-status-gateway-api": "26.3.0-1", - "@sofie-automation/server-core-integration": "26.3.0-1", - "@sofie-automation/shared-lib": "26.3.0-1", + "@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.0", "fast-clone": "^1.5.13", "influx": "^5.9.7", diff --git a/packages/meteor-lib/package.json b/packages/meteor-lib/package.json index 585a075a611..d017769960d 100644 --- a/packages/meteor-lib/package.json +++ b/packages/meteor-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/meteor-lib", - "version": "26.3.0-1", + "version": "26.3.0-2", "private": true, "description": "Temporary internal library for some types shared by meteor and webui", "main": "dist/index.js", @@ -38,9 +38,9 @@ ], "dependencies": { "@mos-connection/helper": "^5.0.0-alpha.0", - "@sofie-automation/blueprints-integration": "26.3.0-1", - "@sofie-automation/corelib": "26.3.0-1", - "@sofie-automation/shared-lib": "26.3.0-1", + "@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", diff --git a/packages/mos-gateway/CHANGELOG.md b/packages/mos-gateway/CHANGELOG.md index bd182b08f8c..b902f9a9de3 100644 --- a/packages/mos-gateway/CHANGELOG.md +++ b/packages/mos-gateway/CHANGELOG.md @@ -3,6 +3,17 @@ 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 diff --git a/packages/mos-gateway/package.json b/packages/mos-gateway/package.json index c7f9ae55e7a..c3c4a5eb90a 100644 --- a/packages/mos-gateway/package.json +++ b/packages/mos-gateway/package.json @@ -1,6 +1,6 @@ { "name": "mos-gateway", - "version": "26.3.0-1", + "version": "26.3.0-2", "private": true, "description": "MOS-Gateway for the Sofie project", "license": "MIT", @@ -62,8 +62,8 @@ ], "dependencies": { "@mos-connection/connector": "^5.0.0-alpha.0", - "@sofie-automation/server-core-integration": "26.3.0-1", - "@sofie-automation/shared-lib": "26.3.0-1", + "@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", "underscore": "^1.13.7", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 64254a33e29..8909f8cc4f4 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/openapi", - "version": "26.3.0-1", + "version": "26.3.0-2", "license": "MIT", "repository": { "type": "git", diff --git a/packages/playout-gateway/CHANGELOG.md b/packages/playout-gateway/CHANGELOG.md index 1379dc8e350..fda39b3db18 100644 --- a/packages/playout-gateway/CHANGELOG.md +++ b/packages/playout-gateway/CHANGELOG.md @@ -3,6 +3,17 @@ 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 diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index 914b19da1ef..d2686983f85 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -1,6 +1,6 @@ { "name": "playout-gateway", - "version": "26.3.0-1", + "version": "26.3.0-2", "private": true, "description": "Connect to Core, play stuff", "license": "MIT", @@ -52,8 +52,8 @@ "production" ], "dependencies": { - "@sofie-automation/server-core-integration": "26.3.0-1", - "@sofie-automation/shared-lib": "26.3.0-1", + "@sofie-automation/server-core-integration": "26.3.0-2", + "@sofie-automation/shared-lib": "26.3.0-2", "debug": "^4.4.0", "influx": "^5.9.7", "timeline-state-resolver": "10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0", diff --git a/packages/server-core-integration/CHANGELOG.md b/packages/server-core-integration/CHANGELOG.md index baf3103f06a..86e74dfbdcd 100644 --- a/packages/server-core-integration/CHANGELOG.md +++ b/packages/server-core-integration/CHANGELOG.md @@ -3,6 +3,14 @@ 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 diff --git a/packages/server-core-integration/package.json b/packages/server-core-integration/package.json index 8dc210a789d..0636f3a34fc 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": "26.3.0-1", + "version": "26.3.0-2", "description": "Library for connecting to Core", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -73,7 +73,7 @@ }, "dependencies": { "@koa/router": "^14.0.0", - "@sofie-automation/shared-lib": "26.3.0-1", + "@sofie-automation/shared-lib": "26.3.0-2", "ejson": "^2.2.3", "faye-websocket": "^0.11.4", "got": "^11.8.6", diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index 3da5de11391..648b36289ae 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "@sofie-automation/shared-lib", - "version": "26.3.0-1", + "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", diff --git a/packages/webui/package.json b/packages/webui/package.json index 01e702a6f08..1ac72ea1dba 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -1,7 +1,7 @@ { "name": "@sofie-automation/webui", "private": true, - "version": "26.3.0-1", + "version": "26.3.0-2", "type": "module", "license": "MIT", "repository": { @@ -39,10 +39,10 @@ "@jstarpl/react-contextmenu": "^2.15.1", "@nrk/core-icons": "^9.6.0", "@popperjs/core": "^2.11.8", - "@sofie-automation/blueprints-integration": "26.3.0-1", - "@sofie-automation/corelib": "26.3.0-1", - "@sofie-automation/meteor-lib": "26.3.0-1", - "@sofie-automation/shared-lib": "26.3.0-1", + "@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", diff --git a/packages/yarn.lock b/packages/yarn.lock index f71bde5c2c5..24b0055fe38 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -6862,11 +6862,11 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/blueprints-integration@npm:26.3.0-1, @sofie-automation/blueprints-integration@workspace:blueprints-integration": +"@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:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-2" tslib: "npm:^2.8.1" type-fest: "npm:^4.33.0" languageName: unknown @@ -6898,12 +6898,12 @@ __metadata: languageName: node linkType: hard -"@sofie-automation/corelib@npm:26.3.0-1, @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:26.3.0-1" - "@sofie-automation/shared-lib": "npm:26.3.0-1" + "@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" @@ -6935,9 +6935,9 @@ __metadata: resolution: "@sofie-automation/job-worker@workspace:job-worker" dependencies: "@slack/webhook": "npm:^7.0.4" - "@sofie-automation/blueprints-integration": "npm:26.3.0-1" - "@sofie-automation/corelib": "npm:26.3.0-1" - "@sofie-automation/shared-lib": "npm:26.3.0-1" + "@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" deepmerge: "npm:^4.3.1" elastic-apm-node: "npm:^4.11.0" @@ -6955,7 +6955,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/live-status-gateway-api@npm:26.3.0-1, @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: @@ -6970,14 +6970,14 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/meteor-lib@npm:26.3.0-1, @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:26.3.0-1" - "@sofie-automation/corelib": "npm:26.3.0-1" - "@sofie-automation/shared-lib": "npm:26.3.0-1" + "@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/underscore": "npm:^1.13.0" @@ -7004,12 +7004,12 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/server-core-integration@npm:26.3.0-1, @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:26.3.0-1" + "@sofie-automation/shared-lib": "npm:26.3.0-2" "@types/koa": "npm:^3.0.0" "@types/koa__router": "npm:^12.0.4" ejson: "npm:^2.2.3" @@ -7021,7 +7021,7 @@ __metadata: languageName: unknown linkType: soft -"@sofie-automation/shared-lib@npm:26.3.0-1, @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: @@ -7053,10 +7053,10 @@ __metadata: "@jstarpl/react-contextmenu": "npm:^2.15.1" "@nrk/core-icons": "npm:^9.6.0" "@popperjs/core": "npm:^2.11.8" - "@sofie-automation/blueprints-integration": "npm:26.3.0-1" - "@sofie-automation/corelib": "npm:26.3.0-1" - "@sofie-automation/meteor-lib": "npm:26.3.0-1" - "@sofie-automation/shared-lib": "npm:26.3.0-1" + "@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" @@ -19489,11 +19489,11 @@ 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:26.3.0-1" - "@sofie-automation/corelib": "npm:26.3.0-1" - "@sofie-automation/live-status-gateway-api": "npm:26.3.0-1" - "@sofie-automation/server-core-integration": "npm:26.3.0-1" - "@sofie-automation/shared-lib": "npm:26.3.0-1" + "@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.0" fast-clone: "npm:^1.5.13" influx: "npm:^5.9.7" @@ -21562,8 +21562,8 @@ 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:26.3.0-1" - "@sofie-automation/shared-lib": "npm:26.3.0-1" + "@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" underscore: "npm:^1.13.7" @@ -23929,8 +23929,8 @@ asn1@evs-broadcast/node-asn1: version: 0.0.0-use.local resolution: "playout-gateway@workspace:playout-gateway" dependencies: - "@sofie-automation/server-core-integration": "npm:26.3.0-1" - "@sofie-automation/shared-lib": "npm:26.3.0-1" + "@sofie-automation/server-core-integration": "npm:26.3.0-2" + "@sofie-automation/shared-lib": "npm:26.3.0-2" debug: "npm:^4.4.0" influx: "npm:^5.9.7" timeline-state-resolver: "npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0" From 6cff2ffb789f7717e731c8b2b2c7298018809c7a Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:57:39 +0000 Subject: [PATCH 135/291] Revert "Merge branch 'release/26.03'" This reverts commit d80cad6b6701e2ba4d9ce380c8d9439d16b824af, reversing changes made to a847a82723ed6f81fbf45c47c42517b6e80a0b31. --- packages/playout-gateway/package.json | 2 +- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 75 ++++++++++++--------------- 3 files changed, 34 insertions(+), 45 deletions(-) diff --git a/packages/playout-gateway/package.json b/packages/playout-gateway/package.json index bbf3ebb4384..7d205484e76 100644 --- a/packages/playout-gateway/package.json +++ b/packages/playout-gateway/package.json @@ -55,7 +55,7 @@ "@sofie-automation/shared-lib": "26.3.0-1", "debug": "^4.4.3", "influx": "^5.12.0", - "timeline-state-resolver": "10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0", + "timeline-state-resolver": "10.0.0-nightly-release53-20251217-143607-df590aa96.0", "tslib": "^2.8.1", "underscore": "^1.13.7", "winston": "^3.19.0" diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index cfaf8945f77..8638db37deb 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -37,7 +37,7 @@ "dependencies": { "@mos-connection/model": "^5.0.0-alpha.0", "kairos-lib": "^0.2.3", - "timeline-state-resolver-types": "10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0", + "timeline-state-resolver-types": "10.0.0-nightly-release53-20251217-143607-df590aa96.0", "tslib": "^2.8.1", "type-fest": "^4.41.0" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 5495eac9b96..4a56f7336df 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -7254,7 +7254,7 @@ __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-release-26-03-20260212-104218-c1745408c.0" + timeline-state-resolver-types: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" tslib: "npm:^2.8.1" type-fest: "npm:^4.41.0" languageName: unknown @@ -10359,16 +10359,16 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"atem-state@npm:1.2.1": - version: 1.2.1 - resolution: "atem-state@npm:1.2.1" +"atem-state@npm:1.2.0": + version: 1.2.0 + resolution: "atem-state@npm:1.2.0" dependencies: deepmerge: "npm:^4.3.1" tslib: "npm:^2.6.2" type-fest: "npm:^3.13.1" peerDependencies: - atem-connection: 3.7 - checksum: 10/be74a217e6310a4cadb8883b8bfe76c3df0bebbea30194deef63995c777cec72cd9017383faec7f760ce7033398ed9817ad1a3576d20075cfab56f761ecf2611 + atem-connection: 3.4 + checksum: 10/9eecbc871e7e1311d05ef2a40ac620480bfef9deb93ef81ca277bd6e34700c17a6ca0a4f27d1369669ef96990745fb58baa538de388149180dbfd2394e197e02 languageName: node linkType: hard @@ -19273,17 +19273,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"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: @@ -24017,7 +24007,7 @@ asn1@evs-broadcast/node-asn1: "@sofie-automation/shared-lib": "npm:26.3.0-1" debug: "npm:^4.4.3" influx: "npm:^5.12.0" - timeline-state-resolver: "npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0" + timeline-state-resolver: "npm:10.0.0-nightly-release53-20251217-143607-df590aa96.0" tslib: "npm:^2.8.1" underscore: "npm:^1.13.7" winston: "npm:^3.19.0" @@ -28886,44 +28876,43 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"timeline-state-resolver-api@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0": - version: 10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0 - resolution: "timeline-state-resolver-api@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0" +"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" dependencies: tslib: "npm:^2.8.1" peerDependencies: - timeline-state-resolver-types: 10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0 - checksum: 10/0e49376cd52fa073f2927e23aafce081e8c863a8a34c9ef178225a9735fc56e3fecb01ad25ddfe3607ddef142d416154644d3d871be768f65255ec6244eba3d1 + timeline-state-resolver-types: 10.0.0-nightly-release53-20251217-143607-df590aa96.0 + checksum: 10/fb174edd6694643c8ca16f851593ab09cd2ea4edbadaa70944f92bf5dad97a18aab1b5a454a353428589cb104f150f379eaba394ae57099843388c12b27c7683 languageName: node linkType: hard -"timeline-state-resolver-types@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0": - version: 10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0 - resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0" +"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" dependencies: - kairos-lib: "npm:0.2.3" tslib: "npm:^2.8.1" - checksum: 10/33dba430305d68ab78bddf697643c9da3b33d3a428e08e10efbd94afa6b6cd91b8a52eb7231846e58a622359f7b3d6314663ba6db440c96bff9a305d06692db1 + checksum: 10/6f17030e9f10568757b3ebaae40a54ce5220ce5c2f37d9b5d8a5d286704e4779c80fbfddae500e1952a6efdf6e76b67bc18d0151211c84eb194ae3b52f78c7bf languageName: node linkType: hard -"timeline-state-resolver@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0": - version: 10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0 - resolution: "timeline-state-resolver@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0" +"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" dependencies: "@tv2media/v-connection": "npm:^7.3.4" atem-connection: "npm:3.7.0" - atem-state: "npm:1.2.1" + atem-state: "npm:1.2.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" @@ -28933,16 +28922,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-release-26-03-20260212-104218-c1745408c.0" - timeline-state-resolver-types: "npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.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" 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.6" - ws: "npm:^8.19.0" + utf-8-validate: "npm:^6.0.5" + ws: "npm:^8.18.3" xml-js: "npm:^1.6.11" - checksum: 10/fe76fdd9a89872c7bf1147e04300aa117f524e761f15830dd88b4e2fc32ad4ac905afa33f49fb9761b204d121f247bf048014c4c349cae02282c5457c172e33e + checksum: 10/82b22c7945946005485c38ad8fcb94314bcba99aaf3aa549ec30326bb33548e46fe3ee5f6d48c06e6b593405458cbf1bbbda8a95793f57507f0158180e441686 languageName: node linkType: hard @@ -30229,13 +30218,13 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"utf-8-validate@npm:^6.0.6": - version: 6.0.6 - resolution: "utf-8-validate@npm:6.0.6" +"utf-8-validate@npm:^6.0.5": + version: 6.0.5 + resolution: "utf-8-validate@npm:6.0.5" dependencies: node-gyp: "npm:latest" node-gyp-build: "npm:^4.3.0" - checksum: 10/c1fa53fe5f0e3b7bf990a8ee41d890b10218b087a4ad401519a1a6353a427172fedc29c9af36b81080ea27b311802ae37b0e857b82aaa976238904398870f465 + checksum: 10/8c96d342064d3f03d7acf616fe727e484825f4f5f7a455059122787306b2df1a4e23c2d27f16bf7ba21293f4ce6ab3e683b893fe7b4c74ac9d43b871c10001a0 languageName: node linkType: hard @@ -31279,7 +31268,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ws@npm:^8.13.0, ws@npm:^8.18.0, ws@npm:^8.19.0": +"ws@npm:^8.13.0, ws@npm:^8.18.0, ws@npm:^8.18.3, ws@npm:^8.19.0": version: 8.19.0 resolution: "ws@npm:8.19.0" peerDependencies: From 91845d1c1ec2ce48b89617eeb01aef2fbc3fbfbb Mon Sep 17 00:00:00 2001 From: Peter C <12292660+PeterC89@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:53:24 +0000 Subject: [PATCH 136/291] chore: Update lockfiles after merge --- packages/shared-lib/package.json | 2 +- packages/yarn.lock | 75 ++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/packages/shared-lib/package.json b/packages/shared-lib/package.json index e1e16128b85..b9c1736ba77 100644 --- a/packages/shared-lib/package.json +++ b/packages/shared-lib/package.json @@ -37,7 +37,7 @@ "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-release-26-03-20260212-104218-c1745408c.0", "tslib": "^2.8.1", "type-fest": "^4.41.0" }, diff --git a/packages/yarn.lock b/packages/yarn.lock index 577d432ae40..368fb36db3a 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -7254,7 +7254,7 @@ __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-release-26-03-20260212-104218-c1745408c.0" tslib: "npm:^2.8.1" type-fest: "npm:^4.41.0" languageName: unknown @@ -10359,16 +10359,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.2.1": + version: 1.2.1 + resolution: "atem-state@npm:1.2.1" 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/be74a217e6310a4cadb8883b8bfe76c3df0bebbea30194deef63995c777cec72cd9017383faec7f760ce7033398ed9817ad1a3576d20075cfab56f761ecf2611 languageName: node linkType: hard @@ -19273,7 +19273,17 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"kairos-lib@npm:^0.2.3": +"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": version: 0.2.3 resolution: "kairos-lib@npm:0.2.3" dependencies: @@ -24007,7 +24017,7 @@ asn1@evs-broadcast/node-asn1: "@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-release53-20251217-143607-df590aa96.0" + timeline-state-resolver: "npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0" tslib: "npm:^2.8.1" underscore: "npm:^1.13.7" winston: "npm:^3.19.0" @@ -28876,43 +28886,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-release-26-03-20260212-104218-c1745408c.0": + version: 10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0 + resolution: "timeline-state-resolver-api@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.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-release-26-03-20260212-104218-c1745408c.0 + checksum: 10/0e49376cd52fa073f2927e23aafce081e8c863a8a34c9ef178225a9735fc56e3fecb01ad25ddfe3607ddef142d416154644d3d871be768f65255ec6244eba3d1 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-release-26-03-20260212-104218-c1745408c.0": + version: 10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0 + resolution: "timeline-state-resolver-types@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0" dependencies: + kairos-lib: "npm:0.2.3" tslib: "npm:^2.8.1" - checksum: 10/6f17030e9f10568757b3ebaae40a54ce5220ce5c2f37d9b5d8a5d286704e4779c80fbfddae500e1952a6efdf6e76b67bc18d0151211c84eb194ae3b52f78c7bf + checksum: 10/33dba430305d68ab78bddf697643c9da3b33d3a428e08e10efbd94afa6b6cd91b8a52eb7231846e58a622359f7b3d6314663ba6db440c96bff9a305d06692db1 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-release-26-03-20260212-104218-c1745408c.0": + version: 10.0.0-nightly-release-26-03-20260212-104218-c1745408c.0 + resolution: "timeline-state-resolver@npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.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.2.1" 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" @@ -28922,16 +28933,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-release-26-03-20260212-104218-c1745408c.0" + timeline-state-resolver-types: "npm:10.0.0-nightly-release-26-03-20260212-104218-c1745408c.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/fe76fdd9a89872c7bf1147e04300aa117f524e761f15830dd88b4e2fc32ad4ac905afa33f49fb9761b204d121f247bf048014c4c349cae02282c5457c172e33e languageName: node linkType: hard @@ -30218,13 +30229,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 @@ -31268,7 +31279,7 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"ws@npm:^8.13.0, ws@npm:^8.18.0, ws@npm:^8.18.3, ws@npm:^8.19.0": +"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: From 26a20c0a658b2fb18241605ea9789cb0fd18fc07 Mon Sep 17 00:00:00 2001 From: Simon Rogers Date: Fri, 20 Feb 2026 17:09:52 +0000 Subject: [PATCH 137/291] Fix: Update for new TSR mapping types --- .../publications/packageManager/expectedPackages/util.ts | 4 ++-- .../pieceContentStatusUI/checkPieceContentStatus.ts | 4 ++-- packages/job-worker/src/playout/lookahead/index.ts | 4 ++-- packages/meteor-lib/src/collections/Studios.ts | 3 ++- packages/webui/src/client/ui/Settings/Studio/Mappings.tsx | 5 +++-- 5 files changed, 11 insertions(+), 9 deletions(-) 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/pieceContentStatusUI/checkPieceContentStatus.ts b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts index f6bf71d0a70..0232aa9ee42 100644 --- a/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts +++ b/meteor/server/publications/pieceContentStatusUI/checkPieceContentStatus.ts @@ -1184,7 +1184,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 +1199,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/packages/job-worker/src/playout/lookahead/index.ts b/packages/job-worker/src/playout/lookahead/index.ts index 64ac5a23372..b2f0906a989 100644 --- a/packages/job-worker/src/playout/lookahead/index.ts +++ b/packages/job-worker/src/playout/lookahead/index.ts @@ -35,7 +35,7 @@ function parseSearchDistance(rawVal: number | undefined): number { } } -function findLargestLookaheadDistance(mappings: Array<[string, MappingExt]>): number { +function findLargestLookaheadDistance(mappings: Array<[string, ReadonlyDeep]>): number { const values = mappings.map(([_id, m]) => parseSearchDistance(m.lookaheadMaxSearchDistance)) return _.max(values) } @@ -79,7 +79,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) { 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/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) { From fe145658e85aeeb1b42c48a79d7dba755dffa766 Mon Sep 17 00:00:00 2001 From: Johan Nyman Date: Wed, 25 Feb 2026 12:31:47 +0100 Subject: [PATCH 138/291] Delete .github/CODEOWNERS Removing CODEOWNERS after decision by TSC --- .github/CODEOWNERS | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/CODEOWNERS 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 From c90fad3e05b36d68441c83da8119744e6d6af245 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 25 Feb 2026 12:15:53 +0000 Subject: [PATCH 139/291] fix: update react lottie library (#1659) * fix: downgrade lottie * fix: switch to lottie-react This library is better maintained and is vastly more popular Funnily, this is the reverse of https://github.com/Sofie-Automation/sofie-core/commit/34b59ba81df7658620200d81d35f90aa3d30590f --- packages/webui/package.json | 3 +- .../webui/src/client/lib/LottieButton.tsx | 8 ++--- .../webui/src/client/lib/ui/icons/looping.tsx | 27 +++++++++++++---- .../RundownView/RundownRightHandControls.tsx | 14 ++++----- packages/yarn.lock | 29 +++++++++---------- 5 files changed, 47 insertions(+), 34 deletions(-) diff --git a/packages/webui/package.json b/packages/webui/package.json index f04806469c1..575bf57f441 100644 --- a/packages/webui/package.json +++ b/packages/webui/package.json @@ -30,7 +30,6 @@ "license-validate": "run -T sofie-licensecheck" }, "dependencies": { - "@crello/react-lottie": "0.0.11", "@fortawesome/fontawesome-free": "^7.1.0", "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.1.0", @@ -52,7 +51,7 @@ "i18next-browser-languagedetector": "^8.2.0", "i18next-http-backend": "^3.0.2", "immutability-helper": "^3.1.1", - "lottie-web": "^5.13.0", + "lottie-react": "^2.4.1", "moment": "^2.30.1", "motion": "^12.31.0", "promise.allsettled": "^1.0.7", 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}
>): 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/ui/RundownView/RundownRightHandControls.tsx b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx index 775d55c326f..cd23aaea9ac 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, } @@ -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/yarn.lock b/packages/yarn.lock index 368fb36db3a..9fb10fa247b 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -2221,18 +2221,6 @@ __metadata: languageName: node linkType: hard -"@crello/react-lottie@npm:0.0.11": - version: 0.0.11 - resolution: "@crello/react-lottie@npm:0.0.11" - dependencies: - lottie-web: "npm:^5.7.3" - peerDependencies: - react: ~16.9.0 - react-dom: ~16.9.0 - checksum: 10/23629b6cdd5b703f936ef4263696feecaadf3b642d6442afae94043c4bc0e4c4a98e249257a277812c9214ebca4363c37466fba1c9c7dee951ade50c31e02737 - 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" @@ -7272,7 +7260,6 @@ __metadata: resolution: "@sofie-automation/webui@workspace:webui" dependencies: "@babel/preset-env": "npm:^7.29.0" - "@crello/react-lottie": "npm:0.0.11" "@fortawesome/fontawesome-free": "npm:^7.1.0" "@fortawesome/fontawesome-svg-core": "npm:^7.1.0" "@fortawesome/free-solid-svg-icons": "npm:^7.1.0" @@ -7312,7 +7299,7 @@ __metadata: i18next-browser-languagedetector: "npm:^8.2.0" i18next-http-backend: "npm:^3.0.2" immutability-helper: "npm:^3.1.1" - lottie-web: "npm:^5.13.0" + lottie-react: "npm:^2.4.1" moment: "npm:^2.30.1" motion: "npm:^12.31.0" promise.allsettled: "npm:^1.0.7" @@ -19899,7 +19886,19 @@ asn1@evs-broadcast/node-asn1: languageName: node linkType: hard -"lottie-web@npm:^5.13.0, lottie-web@npm:^5.7.3": +"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.10.2": version: 5.13.0 resolution: "lottie-web@npm:5.13.0" checksum: 10/ccc65b91ddc569c874de265252ef41cb546798515dd63c5ee366844efd1e10335c080c483ce4305faba0cebd54c4afd6bb918fd0d6f4394dcc284fc0c3944941 From 340898edfb7ebc81975134f8a9be49d9a877ca1a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:18:29 +0000 Subject: [PATCH 140/291] chore(deps): bump aquasecurity/trivy-action from 0.34.0 to 0.34.1 (#1663) Bumps [aquasecurity/trivy-action](https://github.com/aquasecurity/trivy-action) from 0.34.0 to 0.34.1. - [Release notes](https://github.com/aquasecurity/trivy-action/releases) - [Commits](https://github.com/aquasecurity/trivy-action/compare/0.34.0...0.34.1) --- updated-dependencies: - dependency-name: aquasecurity/trivy-action dependency-version: 0.34.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/node.yaml | 4 ++-- .github/workflows/trivy.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/node.yaml b/.github/workflows/node.yaml index 174a7f5c8dd..00926ec1d4f 100644 --- a/.github/workflows/node.yaml +++ b/.github/workflows/node.yaml @@ -274,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.34.0 + uses: aquasecurity/trivy-action@0.34.1 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: @@ -446,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.34.0 + uses: aquasecurity/trivy-action@0.34.1 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 65b5a9cab56..508639c3a6a 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Run Trivy vulnerability scanner (json) - uses: aquasecurity/trivy-action@0.34.0 + uses: aquasecurity/trivy-action@0.34.1 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: @@ -30,7 +30,7 @@ jobs: output: "${{ matrix.image }}-trivy-scan-results.json" - name: Run Trivy vulnerability scanner (table) - uses: aquasecurity/trivy-action@0.34.0 + uses: aquasecurity/trivy-action@0.34.1 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: @@ -48,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.34.0 + uses: aquasecurity/trivy-action@0.34.1 env: TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db with: From fb5dd9f18557cc19d4a3327bfbd704a8af18e53b Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 25 Feb 2026 14:28:00 +0000 Subject: [PATCH 141/291] feat: expose persistent playout state on LSG (#1644) --- meteor/server/migration/X_X_X.ts | 46 +++++++++++++++++- .../src/context/playoutStore.ts | 30 ++++++++++-- .../corelib/src/dataModel/RundownPlaylist.ts | 8 +++- .../context/services/PersistantStateStore.ts | 47 ++++++++++++++----- .../job-worker/src/playout/adlibAction.ts | 9 ++-- .../src/playout/model/PlayoutModel.ts | 10 +++- .../model/implementation/PlayoutModelImpl.ts | 12 +++-- packages/job-worker/src/playout/setNext.ts | 9 ++-- packages/job-worker/src/playout/take.ts | 15 +++--- .../src/playout/timeline/generate.ts | 7 ++- .../activePlaylistEvent.yaml | 2 + .../src/generated/asyncapi.yaml | 3 ++ .../src/generated/schema.ts | 4 ++ .../src/topics/activePlaylistTopic.ts | 9 ++-- .../DirectorScreen/DirectorScreen.tsx | 3 +- .../client/ui/ClockView/PresenterScreen.tsx | 3 +- .../src/client/ui/MediaStatus/MediaStatus.tsx | 3 +- 17 files changed, 170 insertions(+), 50 deletions(-) diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 5aa78a3370c..2c9c5a4260d 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -1,7 +1,7 @@ import { addMigrationSteps } from './databaseMigration' import { CURRENT_SYSTEM_VERSION } from './currentSystemVersion' import { MongoInternals } from 'meteor/mongo' -import { Studios } from '../collections' +import { RundownPlaylists, Studios } from '../collections' import { ExpectedPackages } from '../collections' import * as PackagesPreR53 from '@sofie-automation/corelib/dist/dataModel/Old/ExpectedPackagesR52' import { @@ -198,6 +198,50 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ } }, }, + { + id: `Rename previousPersistentState to privatePlayoutPersistentState`, + canBeRunAutomatically: true, + validate: async () => { + 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 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 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, + }, + }) + } + }, + }, // Add your migration here new ContainerIdsToObjectWithOverridesMigrationStep(), 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/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index e2850bc49bb..d8bc66f809a 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -171,7 +171,13 @@ 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 */ 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/playout/adlibAction.ts b/packages/job-worker/src/playout/adlibAction.ts index e4d14925f7f..66217e8a42a 100644 --- a/packages/job-worker/src/playout/adlibAction.ts +++ b/packages/job-worker/src/playout/adlibAction.ts @@ -247,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, @@ -260,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/model/PlayoutModel.ts b/packages/job-worker/src/playout/model/PlayoutModel.ts index 867f0420cd2..5158e6ae4a8 100644 --- a/packages/job-worker/src/playout/model/PlayoutModel.ts +++ b/packages/job-worker/src/playout/model/PlayoutModel.ts @@ -328,10 +328,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 diff --git a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts index 86e2d089150..d7685b55859 100644 --- a/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts +++ b/packages/job-worker/src/playout/model/implementation/PlayoutModelImpl.ts @@ -671,7 +671,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 @@ -783,8 +784,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 } diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index 8739e289a33..afb4ca2ba7b 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -229,14 +229,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 diff --git a/packages/job-worker/src/playout/take.ts b/packages/job-worker/src/playout/take.ts index c9a2dd32b97..2836fda9e19 100644 --- a/packages/job-worker/src/playout/take.ts +++ b/packages/job-worker/src/playout/take.ts @@ -343,7 +343,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 +354,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 +548,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 +558,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/generate.ts b/packages/job-worker/src/playout/timeline/generate.ts index de535e799d5..aa95c879c62 100644 --- a/packages/job-worker/src/playout/timeline/generate.ts +++ b/packages/job-worker/src/playout/timeline/generate.ts @@ -415,7 +415,8 @@ export 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') @@ -437,9 +438,7 @@ export async function getTimelineRundown( }) }) - if (blueprintPersistentState.hasChanges) { - playoutModel.setBlueprintPersistentState(blueprintPersistentState.getAll()) - } + blueprintPersistentState.saveToModel(playoutModel) } playoutModel.setAbResolvingState( 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..21e7277c149 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,6 +40,8 @@ $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: diff --git a/packages/live-status-gateway-api/src/generated/asyncapi.yaml b/packages/live-status-gateway-api/src/generated/asyncapi.yaml index b314a66bcdb..71d5675007d 100644 --- a/packages/live-status-gateway-api/src/generated/asyncapi.yaml +++ b/packages/live-status-gateway-api/src/generated/asyncapi.yaml @@ -644,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 diff --git a/packages/live-status-gateway-api/src/generated/schema.ts b/packages/live-status-gateway-api/src/generated/schema.ts index 54a4a5e0729..dca47bd84fd 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 */ diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index f1f29c940d4..aa4b0093292 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -34,7 +34,7 @@ import { 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,6 +45,7 @@ const PLAYLIST_KEYS = [ 'name', 'rundownIdsInOrder', 'publicData', + 'publicPlayoutPersistentState', 'currentPartInfo', 'nextPartInfo', 'timing', @@ -101,7 +102,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 +158,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, @@ -171,7 +173,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket : undefined, }, }) - : literal({ + : literal>({ event: 'activePlaylist', id: null, externalId: null, @@ -182,6 +184,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket nextPart: null, quickLoop: undefined, publicData: undefined, + playoutState: undefined, timing: { timingMode: ActivePlaylistTimingMode.NONE, }, diff --git a/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx index eb0612ae310..f8638b616f3 100644 --- a/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx @@ -238,7 +238,8 @@ 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, diff --git a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx index 22235b01d24..95b09a26b0f 100644 --- a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx @@ -184,7 +184,8 @@ export const getPresenterScreenReactive = ( fields: { lastIncorrectPartPlaybackReported: 0, modified: 0, - previousPersistentState: 0, + publicPlayoutPersistentState: 0, + privatePlayoutPersistentState: 0, rundownRanksAreSetInSofie: 0, trackedAbSessions: 0, restoredFromSnapshotId: 0, 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, From 8a89f4e6f0c560fb15a75d9f0b66e2fdb3b21946 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:15:51 +0000 Subject: [PATCH 142/291] chore(deps): bump actions/download-artifact from 7 to 8 (#1668) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '8' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish-libs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index fff3d3fb39a..d89feea2b0b 100644 --- a/.github/workflows/publish-libs.yml +++ b/.github/workflows/publish-libs.yml @@ -231,7 +231,7 @@ jobs: node-version-file: ".node-version" - name: Download release artifact - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: publish-dist From c4426b51ed1b246b5a352d45352b2e8b14b814ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:15:58 +0000 Subject: [PATCH 143/291] chore(deps): bump actions/upload-artifact from 6 to 7 (#1667) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish-libs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-libs.yml b/.github/workflows/publish-libs.yml index d89feea2b0b..110c02e805f 100644 --- a/.github/workflows/publish-libs.yml +++ b/.github/workflows/publish-libs.yml @@ -196,7 +196,7 @@ jobs: yarn install --no-immutable - name: Upload release artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: publish-dist path: | From c355cc8df781f66c869bf749fdd725c3ddd51490 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 2 Mar 2026 10:20:40 +0000 Subject: [PATCH 144/291] feat: formatting in prompter SOFIE-215 (#1658) --- .../blueprints-integration/src/content.ts | 1 + .../blueprints-integration/src/previews.ts | 2 + .../for-blueprint-developers/prompter-text.md | 74 ++ .../webui/src/client/styles/prompter.scss | 46 ++ .../ui/PreviewPopUp/PreviewPopUpContext.tsx | 2 + .../PreviewPopUp/Previews/ScriptPreview.tsx | 17 +- .../ui/Prompter/Formatted/MdDisplay.tsx | 59 ++ .../mdParser/__tests__/mdParser.test.ts | 683 ++++++++++++++++++ .../Prompter/Formatted/mdParser/astNodes.ts | 67 ++ .../Formatted/mdParser/constructs/colour.ts | 58 ++ .../mdParser/constructs/emphasisAndStrong.ts | 61 ++ .../Formatted/mdParser/constructs/escape.ts | 26 + .../mdParser/constructs/paragraph.ts | 34 + .../Formatted/mdParser/constructs/reverse.ts | 35 + .../mdParser/constructs/screenMarker.ts | 24 + .../mdParser/constructs/underlineOrHide.ts | 84 +++ .../ui/Prompter/Formatted/mdParser/index.ts | 128 ++++ .../Formatted/mdParser/parserState.ts | 43 ++ .../src/client/ui/Prompter/PrompterView.tsx | 48 +- .../webui/src/client/ui/Prompter/prompter.ts | 4 + 20 files changed, 1471 insertions(+), 25 deletions(-) create mode 100644 packages/documentation/docs/for-developers/for-blueprint-developers/prompter-text.md create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/MdDisplay.tsx create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/mdParser/__tests__/mdParser.test.ts create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/mdParser/astNodes.ts create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/colour.ts create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/emphasisAndStrong.ts create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/escape.ts create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/paragraph.ts create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/reverse.ts create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/screenMarker.ts create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/mdParser/constructs/underlineOrHide.ts create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/mdParser/index.ts create mode 100644 packages/webui/src/client/ui/Prompter/Formatted/mdParser/parserState.ts 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/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/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/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/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 dd10e81a2ba..0ace8ae3b43 100644 --- a/packages/webui/src/client/ui/Prompter/PrompterView.tsx +++ b/packages/webui/src/client/ui/Prompter/PrompterView.tsx @@ -33,9 +33,10 @@ 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 @@ -975,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) @@ -1127,7 +1130,8 @@ const PrompterContent = withTranslation()( ) for (const line of part.pieces) { - let text = line.text || '' + const isFormatted = line.formattedText !== undefined + let text = (isFormatted ? line.formattedText : line.text) || '' if (line.id === pieceIdToHideScript) { text = '' } @@ -1135,6 +1139,7 @@ const PrompterContent = withTranslation()( // if a continuation is not in a live part, it should not display its text text = '' } + lines.push(
- {text} + {isFormatted ? : text}
) } diff --git a/packages/webui/src/client/ui/Prompter/prompter.ts b/packages/webui/src/client/ui/Prompter/prompter.ts index 47a7c2114cf..e64c90006d3 100644 --- a/packages/webui/src/client/ui/Prompter/prompter.ts +++ b/packages/webui/src/client/ui/Prompter/prompter.ts @@ -56,6 +56,7 @@ export interface PrompterDataPart { export interface PrompterDataPiece { id: PieceId text: string + formattedText: string | undefined continuationOf?: PieceId startPartId?: PartId | null } @@ -271,6 +272,7 @@ export namespace PrompterAPI { partData.pieces.push({ id: protectString(`${partData.id}_${piece._id}_continuation`), text: content.fullScript, + formattedText: content.fullScriptFormatted, continuationOf: piece._id, startPartId: piece.startPartId, }) @@ -281,6 +283,7 @@ export namespace PrompterAPI { partData.pieces.push({ id: piece._id, text: content.fullScript, + formattedText: content.fullScriptFormatted, }) } } @@ -290,6 +293,7 @@ export namespace PrompterAPI { partData.pieces.push({ id: protectString(`part_${partData.id}_empty`), text: '', + formattedText: '', }) } From f1e15aa96f87ff01ad21fa0bb0ea234a39a92363 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 2 Mar 2026 15:45:43 +0000 Subject: [PATCH 145/291] feat: improve readability of snapshots table (#1664) --- meteor/server/__tests__/cronjobs.test.ts | 2 + meteor/server/api/__tests__/cleanup.test.ts | 1 + meteor/server/api/snapshot.ts | 14 +- .../meteor-lib/src/collections/Snapshots.ts | 1 + packages/webui/src/client/lib/lib.tsx | 8 +- .../src/client/ui/Settings/SnapshotsView.tsx | 146 ++++++++++++------ .../client/ui/util/useToggleExpandHelper.tsx | 7 +- 7 files changed, 115 insertions(+), 64 deletions(-) diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index a8e972a2081..6b7cc685142 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: 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/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/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/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/ui/Settings/SnapshotsView.tsx b/packages/webui/src/client/ui/Settings/SnapshotsView.tsx index f80a5f6361a..5a6ff00d9ed 100644 --- a/packages/webui/src/client/ui/Settings/SnapshotsView.tsx +++ b/packages/webui/src/client/ui/Settings/SnapshotsView.tsx @@ -6,7 +6,7 @@ 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 { 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' @@ -21,6 +21,10 @@ 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.js' +import { useState } from 'react' +import { MomentFromNow } from '../../lib/Moment.js' +import { assertNever } from '@sofie-automation/corelib/dist/lib.js' export default function SnapshotsView(): JSX.Element { const { t } = useTranslation() @@ -28,8 +32,6 @@ export default function SnapshotsView(): JSX.Element { const [removeSnapshots, setRemoveSnapshots] = React.useState(false) const toggleRemoveView = React.useCallback(() => setRemoveSnapshots((old) => !old), []) - const [editSnapshotId, setEditSnapshotId] = React.useState(null) - // Subscribe to data: useSubscription(MeteorPubSub.snapshots) useSubscription(CorelibPubSub.studios, null) @@ -114,56 +116,17 @@ export default function SnapshotsView(): JSX.Element { - {t('Type')} {t('Name')} - {t('Comment')} + {t('When')} {removeSnapshots ? : null} - {snapshots.map((snapshot) => { - return ( - - - - - {snapshot.type} - - - {snapshot.name} - - - - {editSnapshotId === snapshot._id ? ( -
- - - -
- ) : ( - { - e.preventDefault() - setEditSnapshotId(snapshot._id) - }} - > - {multilineText(snapshot.comment)} - - )} - - {removeSnapshots ? ( - - - - ) : null} - - ) - })} + {snapshots.map((snapshot) => ( + + ))}
@@ -182,6 +145,89 @@ export default function SnapshotsView(): JSX.Element { ) } +function SnapshotRowItem({ + snapshot, + removeSnapshots, +}: { + snapshot: SnapshotItem + removeSnapshots: boolean +}): JSX.Element { + const [isExpanded, setIsExpanded] = useState(false) + + return ( + + + + + + + {isExpanded ? ( +
+ + + +
+ ) : ( + { + 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, children, 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>({}) From fb99a01adc2c96f34d51ce543dde0026743f769b Mon Sep 17 00:00:00 2001 From: Jan Starzak Date: Mon, 2 Mar 2026 17:26:13 +0100 Subject: [PATCH 146/291] fix(core): RundownPlaylist filters for Action Triggers a bit too persistent --- .../meteor-lib/src/triggers/actionFilterChainCompilers.ts | 5 +---- .../actionEditors/filterPreviews/SwitchFilterType.tsx | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts b/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts index 6401a8c1b38..9f8720c49f0 100644 --- a/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts +++ b/packages/meteor-lib/src/triggers/actionFilterChainCompilers.ts @@ -643,10 +643,7 @@ export function compileAdLibFilter( if (!matchAll) { if (!activationStateMatches || !nameMatches || !studioMatches || !rehearsalMatches) { - adLibPieceTypeFilter.skip = true - adLibActionTypeFilter.skip = true - clearAdLibs.length = 0 - stickyAdLibs.length = 0 + return [] } } } 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 index 67a7bd112a9..3eb9c296f87 100644 --- 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 @@ -20,7 +20,7 @@ export function SwitchFilterType({ {allowedTypes.includes('view') ? ( + ) +} 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..a37d650965a --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -0,0 +1,341 @@ +@import '../../../styles/colorScheme'; + +.rundown-header { + height: 64px; + min-height: 64px; + padding: 0; + width: 100%; + border-bottom: 1px solid #333; + transition: background-color 0.5s; + font-family: 'Roboto Flex', sans-serif; + + .rundown-header__trigger { + height: 100%; + width: 100%; + display: block; + } + + // State-based background colors + &.not-active { + background: $color-header-inactive; + } + + &.active { + background: $color-header-on-air; + + .rundown-header__segment-remaining, + .rundown-header__onair-remaining, + .rundown-header__expected-end { + color: #fff; + } + + .timing__header_t-timers__timer__label { + color: rgba(255, 255, 255, 0.9); + } + + .timing-clock { + &.time-now { + color: #fff; + } + } + + &.rehearsal { + background: $color-header-rehearsal; + } + } + + .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; + } + + .rundown-header__right { + display: flex; + align-items: center; + justify-content: flex-end; + flex: 1; + gap: 1em; + } + + .rundown-header__center { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + + .timing-clock { + color: #40b8fa99; + font-size: 1.4em; + font-variant-numeric: tabular-nums; + letter-spacing: 0.05em; + transition: color 0.2s; + + &.time-now { + font-size: 1.25em; + font-style: italic; + font-weight: 300; + } + } + + .rundown-header__timing-display { + display: flex; + align-items: center; + margin-right: 1.5em; + margin-left: 2em; + + .rundown-header__diff { + display: flex; + align-items: center; + gap: 0.4em; + font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif; + font-variant-numeric: tabular-nums; + white-space: nowrap; + + .rundown-header__diff__label { + font-size: 0.85em; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: #888; + } + + .rundown-header__diff__chip { + font-size: 1.1em; + font-weight: 500; + padding: 0.15em 0.75em; + border-radius: 999px; + font-variant-numeric: tabular-nums; + } + + &.rundown-header__diff--under { + .rundown-header__diff__chip { + background-color: #c8a800; + color: #000; + } + } + + &.rundown-header__diff--over { + .rundown-header__diff__chip { + background-color: #b00; + color: #fff; + } + } + } + } + + .timing__header_t-timers { + position: absolute; + left: 28%; /* Position exactly between the 15% left edge content and the 50% center clock */ + top: 0; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: center; /* Center vertically against the entire header height */ + align-items: flex-end; + + .timing__header_t-timers__timer { + display: flex; + gap: 0.5em; + justify-content: space-between; + align-items: baseline; + white-space: nowrap; + line-height: 1.25; + + .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.4em; + } + + .timing__header_t-timers__timer__sign { + display: inline-block; + width: 0.6em; + text-align: center; + font-weight: 500; + font-size: 1.1em; + 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; + } + + &:only-child { + /* For single timers, lift it vertically by exactly half its height to match the SegBudget top row height */ + transform: translateY(-65%); + } + } + } + + &:hover { + .timing-clock { + color: #40b8fa; + } + } + } + + .rundown-header__right { + display: flex; + align-items: center; + justify-content: flex-end; + flex: 1; + padding-right: 1rem; + } + + .rundown-header__hamburger-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; + } + + .rundown-header__timers { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: center; + gap: 0.1em; + } + + .rundown-header__segment-remaining, + .rundown-header__onair-remaining { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.8em; + color: rgba(255, 255, 255, 0.6); + transition: color 0.2s; + + &__label { + font-size: 0.7em; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + opacity: 0; + transition: opacity 0.2s; + } + + &__value { + font-size: 1.4em; + font-variant-numeric: tabular-nums; + letter-spacing: 0.05em; + + .overtime { + color: $general-late-color; + } + } + } + + // Stacked Plan. End / Est. End in right section + .rundown-header__endtimes { + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.1em; + min-width: 9em; + } + + .rundown-header__expected-end { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.6em; + color: rgba(255, 255, 255, 0.6); + transition: color 0.2s; + + &__label { + font-size: 0.7em; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + } + + &__value { + font-size: 1.4em; + font-variant-numeric: tabular-nums; + letter-spacing: 0.05em; + } + } + + .rundown-header__onair-remaining__label { + background-color: $general-live-color; + color: #fff; + padding: 0.1em 0.6em 0.1em 0.3em; + border-radius: 2px 999px 999px 2px; + font-weight: bold; + 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; + cursor: pointer; + color: #40b8fa; + opacity: 0; + transition: opacity 0.2s; + } + + &:hover { + .rundown-header__hamburger-btn, + .rundown-header__center .timing-clock { + color: #40b8fa; + } + + .rundown-header__segment-remaining, + .rundown-header__onair-remaining, + .rundown-header__expected-end { + color: white; + } + + .rundown-header__segment-remaining__label, + .rundown-header__onair-remaining__label { + opacity: 1; + } + + .rundown-header__close-btn { + opacity: 1; + } + } +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index c0d2b8ae834..3cce7350805 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -1,40 +1,30 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' 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 { VTContent } from '@sofie-automation/blueprints-integration' 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 Moment from 'react-moment' +import { RundownContextMenu, RundownHeaderContextMenuTrigger, RundownHamburgerButton } from './RundownContextMenu' +import { TimeOfDay } from '../RundownTiming/TimeOfDay' +import { CurrentPartOrSegmentRemaining } from '../RundownTiming/CurrentPartOrSegmentRemaining' +import { RundownHeaderTimers } from './RundownHeaderTimers' +import { FreezeFrameIcon } from '../../../lib/ui/icons/freezeFrame' +import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' +import { PieceInstances, PartInstances } from '../../../collections/index' +import { useTiming, TimingTickResolution, TimingDataResolution } from '../RundownTiming/withTiming' +import { getPlaylistTimingDiff } from '../../../lib/rundownTiming' +import { RundownUtils } from '../../../lib/rundown' +import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' +import './RundownHeader.scss' interface IRundownHeaderProps { playlist: DBRundownPlaylist @@ -44,121 +34,18 @@ interface IRundownHeaderProps { studio: UIStudio rundownIds: RundownId[] firstRundown: Rundown | undefined + rundownCount: number onActivate?: (isRehearsal: boolean) => void inActiveRundownView?: boolean layout: RundownLayoutRundownHeader | undefined } -export function RundownHeader({ - playlist, - showStyleBase, - showStyleVariant, - currentRundown, - studio, - rundownIds, - firstRundown, - inActiveRundownView, - layout, -}: IRundownHeaderProps): JSX.Element { +export function RundownHeader({ playlist, studio, firstRundown }: IRundownHeaderProps): JSX.Element { const { t } = useTranslation() - const userPermissions = useContext(UserPermissionsContext) - - const [selectedPiece, setSelectedPiece] = useState(undefined) - const [shouldQueueAdlibs, setShouldQueueAdlibs] = useState(false) - - const operations = useRundownPlaylistOperations() - - 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]) - - const canClearQuickLoop = - !!studio.settings.enableQuickLoop && - !RundownResolver.isLoopLocked(playlist) && - RundownResolver.isAnyLoopMarkerDefined(playlist) - - const rundownTimesInfo = checkRundownTimes(playlist.timing) - - useEffect(() => { - console.debug(`Rundown T-Timers Info: `, JSON.stringify(playlist.tTimers, undefined, 2)) - }, [playlist.tTimers]) - 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')} - - )} -
-
+ - - - noResetOnActivate ? operations.activateRundown(e) : operations.resetAndActivateRundown(e) - } - /> -
-
-
- -
- -
+ +
+
+ + {playlist.currentPartInfo && ( +
+ + {t('Seg. Budg.')} + + + + + + {t('On Air')} + + + + + +
+ )}
- {layout && RundownLayoutsAPI.isDashboardLayout(layout) ? ( - - ) : ( - <> - - - - )} -
-
- - - -
+ +
+ + + +
+ +
+ + + +
- + ) } + +interface IRundownHeaderTimingDisplayProps { + playlist: DBRundownPlaylist +} + +function RundownHeaderTimingDisplay({ playlist }: IRundownHeaderTimingDisplayProps): JSX.Element | null { + const timingDurations = useTiming() + + const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) ?? 0 + const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(overUnderClock), false, false, true, true, true) + const isUnder = overUnderClock <= 0 + + return ( +
+ + {isUnder ? 'Under' : 'Over'} + + {isUnder ? '−' : '+'} + {timeStr} + + +
+ ) +} + +function RundownHeaderExpectedEnd({ playlist }: { playlist: DBRundownPlaylist }): JSX.Element | null { + const { t } = useTranslation() + const timingDurations = useTiming() + + const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) + const expectedEnd = PlaylistTiming.getExpectedEnd(playlist.timing) + const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) + + const now = timingDurations.currentTime ?? Date.now() + const estEnd = + expectedStart != null && timingDurations.remainingPlaylistDuration != null + ? now + timingDurations.remainingPlaylistDuration + : null + + if (!expectedEnd && !expectedDuration && !estEnd) return null + + return ( +
+ {expectedEnd ? ( + + {t('Plan. End')} + + + + + ) : null} + {estEnd ? ( + + {t('Est. End')} + + + + + ) : null} +
+ ) +} + +function HeaderFreezeFrameIcon({ partInstanceId }: { partInstanceId: PartInstanceId }) { + 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/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index d5de3a59423..0b7a5aec4fd 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -12,9 +12,11 @@ interface IProps { export const RundownHeaderTimers: React.FC = ({ tTimers }) => { useTiming() - const activeTimers = tTimers.filter((t) => t.mode) + if (!tTimers?.length) { + return null + } - if (activeTimers.length == 0) return null + const activeTimers = tTimers.filter((t) => t.mode) return (
@@ -76,7 +78,7 @@ function SingleTimer({ timer }: ISingleTimerProps) { } function calculateDiff(timer: RundownTTimer, now: number): number { - if (!timer.state) { + if (!timer.state || timer.state.paused === undefined) { return 0 } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeaderTimers.tsx new file mode 100644 index 00000000000..132963d6968 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeaderTimers.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { useTiming } from '../RundownTiming/withTiming' +import { RundownUtils } from '../../../lib/rundown' +import classNames from 'classnames' +import { getCurrentTime } from '../../../lib/systemTime' + +interface IProps { + tTimers: [RundownTTimer, RundownTTimer, RundownTTimer] +} + +export const RundownHeaderTimers: React.FC = ({ tTimers }) => { + useTiming() + + const activeTimers = tTimers.filter((t) => t.mode) + + if (activeTimers.length == 0) return null + + return ( +
+ {activeTimers.map((timer) => ( + + ))} +
+ ) +} + +interface ISingleTimerProps { + timer: RundownTTimer +} + +function SingleTimer({ timer }: ISingleTimerProps) { + const now = getCurrentTime() + + const isRunning = !!timer.state && !timer.state.paused + + const diff = calculateDiff(timer, now) + const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(diff), false, true, true, false, true) + const parts = timeStr.split(':') + + const timerSign = diff >= 0 ? '+' : '-' + + const isCountingDown = timer.mode?.type === 'countdown' && diff < 0 && isRunning + + return ( +
+ {timer.label} +
+ {timerSign} + {parts.map((p, i) => ( + + + {p} + + {i < parts.length - 1 && :} + + ))} +
+
+ ) +} + +function calculateDiff(timer: RundownTTimer, now: number): number { + if (!timer.state || timer.state.paused === undefined) { + return 0 + } + + // Get current time: either frozen duration or calculated from zeroTime + const currentTime = timer.state.paused ? timer.state.duration : timer.state.zeroTime - now + + // Free run counts up, so negate to get positive elapsed time + if (timer.mode?.type === 'freeRun') { + return -currentTime + } + + // Apply stopAtZero if configured + if (timer.mode?.stopAtZero && currentTime < 0) { + return 0 + } + + return currentTime +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeader_old.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeader_old.tsx new file mode 100644 index 00000000000..e235cb792f4 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeader_old.tsx @@ -0,0 +1,235 @@ +import React, { useCallback, useContext, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import * as CoreIcon from '@nrk/core-icons/jsx' +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 { 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' + +interface IRundownHeaderProps { + playlist: DBRundownPlaylist + showStyleBase: UIShowStyleBase + showStyleVariant: DBShowStyleVariant + currentRundown: Rundown | undefined + studio: UIStudio + rundownIds: RundownId[] + firstRundown: Rundown | undefined + onActivate?: (isRehearsal: boolean) => void + inActiveRundownView?: boolean + layout: RundownLayoutRundownHeader | undefined +} + +export function RundownHeader_old({ + playlist, + showStyleBase, + showStyleVariant, + currentRundown, + studio, + rundownIds, + firstRundown, + inActiveRundownView, + layout, +}: IRundownHeaderProps): JSX.Element { + const { t } = useTranslation() + + const userPermissions = useContext(UserPermissionsContext) + + const [selectedPiece, setSelectedPiece] = useState(undefined) + const [shouldQueueAdlibs, setShouldQueueAdlibs] = useState(false) + + const operations = useRundownPlaylistOperations() + + 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]) + + const canClearQuickLoop = + !!studio.settings.enableQuickLoop && + !RundownResolver.isLoopLocked(playlist) && + RundownResolver.isAnyLoopMarkerDefined(playlist) + + const rundownTimesInfo = checkRundownTimes(playlist.timing) + + useEffect(() => { + console.debug(`Rundown T-Timers Info: `, JSON.stringify(playlist.tTimers, undefined, 2)) + }, [playlist.tTimers]) + + 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')} + + )} +
+
+ + + + noResetOnActivate ? operations.activateRundown(e) : operations.resetAndActivateRundown(e) + } + /> +
+
+
+ +
+ +
+
+ {layout && RundownLayoutsAPI.isDashboardLayout(layout) ? ( + + ) : ( + <> + + + + )} +
+
+ + + +
+
+
+ + + + ) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownReloadResponse.ts b/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownReloadResponse.ts similarity index 100% rename from packages/webui/src/client/ui/RundownView/RundownHeader/RundownReloadResponse.ts rename to packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownReloadResponse.ts diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/TimingDisplay.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx similarity index 100% rename from packages/webui/src/client/ui/RundownView/RundownHeader/TimingDisplay.tsx rename to packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/useRundownPlaylistOperations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader_old/useRundownPlaylistOperations.tsx similarity index 100% rename from packages/webui/src/client/ui/RundownView/RundownHeader/useRundownPlaylistOperations.tsx rename to packages/webui/src/client/ui/RundownView/RundownHeader_old/useRundownPlaylistOperations.tsx diff --git a/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx b/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx index 59f43cf7572..ccaa737756d 100644 --- a/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx @@ -24,7 +24,7 @@ import { doUserAction, UserAction } from '../../lib/clientUserAction.js' 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 '../RundownView/RundownHeader_old/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' diff --git a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx index cd23aaea9ac..5ebb40591f8 100644 --- a/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownRightHandControls.tsx @@ -141,7 +141,7 @@ export function RundownRightHandControls(props: Readonly): JSX.Element { )}
diff --git a/packages/webui/src/client/ui/RundownView/RundownViewContextProviders.tsx b/packages/webui/src/client/ui/RundownView/RundownViewContextProviders.tsx index a5eb33429e9..552f54afeb9 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_old/useRundownPlaylistOperations' 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/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} From ef63bf9a3f0158f68ab23b4196d8bca045e28929 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:14:58 +0100 Subject: [PATCH 177/291] New top bar UI - WIP --- .../client/ui/ClockView/CameraScreen/Part.tsx | 2 +- .../DirectorScreen/DirectorScreen.tsx | 2 +- .../client/ui/ClockView/PresenterScreen.tsx | 2 +- .../RundownView/RundownHeader/Countdown.scss | 24 +++ .../RundownView/RundownHeader/Countdown.tsx | 22 +++ .../CurrentPartOrSegmentRemaining.tsx | 139 +++++++++++++++++ .../RundownHeader/HeaderFreezeFrameIcon.tsx | 59 +++++++ .../RundownHeader/RundownHeader.scss | 82 ++-------- .../RundownHeader/RundownHeader.tsx | 137 ++-------------- .../RundownHeader/RundownHeaderDurations.tsx | 33 ++++ .../RundownHeaderExpectedEnd.tsx | 29 ++++ .../RundownHeaderPlannedStart.tsx | 17 ++ .../RundownHeader/RundownHeaderTimers.tsx | 35 ++--- .../RundownHeaderTimingDisplay.tsx | 30 ++++ .../RundownHeader_old/TimingDisplay.tsx | 2 +- .../CurrentPartOrSegmentRemaining.tsx | 147 ------------------ .../src/client/ui/SegmentList/LinePart.tsx | 2 +- .../src/client/ui/SegmentList/OnAirLine.tsx | 2 +- .../ui/SegmentStoryboard/StoryboardPart.tsx | 2 +- .../ui/SegmentTimeline/SegmentTimeline.tsx | 2 +- .../src/client/ui/Shelf/PartTimingPanel.tsx | 2 +- 21 files changed, 402 insertions(+), 370 deletions(-) create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/HeaderFreezeFrameIcon.tsx create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx delete mode 100644 packages/webui/src/client/ui/RundownView/RundownTiming/CurrentPartOrSegmentRemaining.tsx 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/DirectorScreen/DirectorScreen.tsx b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx index f8638b616f3..6465595d7d6 100644 --- a/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx @@ -38,7 +38,7 @@ 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 { CurrentPartOrSegmentRemaining } from '../../RundownView/RundownHeader/CurrentPartOrSegmentRemaining.js' import { AdjustLabelFit } from '../../util/AdjustLabelFit.js' import { AutoNextStatus } from '../../RundownView/RundownTiming/AutoNextStatus.js' diff --git a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx index 95b09a26b0f..8e9dad2dd08 100644 --- a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx @@ -52,7 +52,7 @@ 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' interface SegmentUi extends DBSegment { items: Array diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss new file mode 100644 index 00000000000..16e73ba3852 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -0,0 +1,24 @@ +@import '../../../styles/colorScheme'; + +.countdown { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 0.6em; + color: rgba(255, 255, 255, 0.6); + transition: color 0.2s; + + &__label { + font-size: 0.7em; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + white-space: nowrap; + } + + &__value { + font-size: 1.4em; + font-variant-numeric: tabular-nums; + letter-spacing: 0.05em; + } +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx new file mode 100644 index 00000000000..487f0a43679 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import Moment from 'react-moment' +import classNames from 'classnames' +import './Countdown.scss' + +interface IProps { + label: string + time?: number + className?: string + children?: React.ReactNode +} + +export function Countdown({ label, time, className, children }: IProps): JSX.Element { + return ( + + {label} + + {time !== undefined ? : children} + + + ) +} 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..905e32b768c --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx @@ -0,0 +1,139 @@ +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' + +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 + +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 + } + } +} + +export const CurrentPartOrSegmentRemaining: React.FC = (props) => { + 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) { + displayTimecode = timingDurations.remainingBudgetOnCurrentSegment ?? displayTimecode + } + + if (displayTimecode === undefined) return null + displayTimecode *= -1 + + return ( + 0 ? props.heavyClassName : undefined)} + role="timer" + > + {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..18318ac74fd --- /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 }) { + 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/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index a37d650965a..f44c5ba570d 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -25,7 +25,7 @@ .rundown-header__segment-remaining, .rundown-header__onair-remaining, - .rundown-header__expected-end { + .countdown { color: #fff; } @@ -33,12 +33,6 @@ color: rgba(255, 255, 255, 0.9); } - .timing-clock { - &.time-now { - color: #fff; - } - } - &.rehearsal { background: $color-header-rehearsal; } @@ -74,14 +68,14 @@ flex: 1; .timing-clock { - color: #40b8fa99; + color: #40b8fa; font-size: 1.4em; font-variant-numeric: tabular-nums; letter-spacing: 0.05em; transition: color 0.2s; &.time-now { - font-size: 1.25em; + font-size: 1.6em; font-style: italic; font-weight: 300; } @@ -142,28 +136,20 @@ flex-direction: column; justify-content: center; /* Center vertically against the entire header height */ align-items: flex-end; + font-size: 0.75em; .timing__header_t-timers__timer { - display: flex; - gap: 0.5em; - justify-content: space-between; - align-items: baseline; white-space: nowrap; line-height: 1.25; - .timing__header_t-timers__timer__label { - font-size: 0.7em; + .countdown__label { color: #b8b8b8; - text-transform: uppercase; - white-space: nowrap; } - .timing__header_t-timers__timer__value { + .countdown__value { font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif; - font-variant-numeric: tabular-nums; font-weight: 500; color: #fff; - font-size: 1.4em; } .timing__header_t-timers__timer__sign { @@ -187,27 +173,8 @@ margin: 0 0.05em; color: #888; } - - &:only-child { - /* For single timers, lift it vertically by exactly half its height to match the SegBudget top row height */ - transform: translateY(-65%); - } } } - - &:hover { - .timing-clock { - color: #40b8fa; - } - } - } - - .rundown-header__right { - display: flex; - align-items: center; - justify-content: flex-end; - flex: 1; - padding-right: 1rem; } .rundown-header__hamburger-btn { @@ -261,35 +228,15 @@ } } - // Stacked Plan. End / Est. End in right section + // Stacked Plan. Start / Plan. End / Est. End in right section .rundown-header__endtimes { display: flex; flex-direction: column; - justify-content: center; - gap: 0.1em; - min-width: 9em; - } - - .rundown-header__expected-end { - display: flex; - align-items: baseline; - justify-content: space-between; - gap: 0.6em; - color: rgba(255, 255, 255, 0.6); - transition: color 0.2s; - - &__label { - font-size: 0.7em; - font-weight: 600; - letter-spacing: 0.1em; - text-transform: uppercase; - } - - &__value { - font-size: 1.4em; - font-variant-numeric: tabular-nums; - letter-spacing: 0.05em; - } + justify-content: flex-start; + gap: 0.15em; + min-width: 7em; + font-size: 0.75em; + padding-top: 0.8em; // Align with the top row of the T-Timers and Seg Budget } .rundown-header__onair-remaining__label { @@ -318,14 +265,13 @@ } &:hover { - .rundown-header__hamburger-btn, - .rundown-header__center .timing-clock { + .rundown-header__hamburger-btn { color: #40b8fa; } .rundown-header__segment-remaining, .rundown-header__onair-remaining, - .rundown-header__expected-end { + .countdown { color: white; } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index 3cce7350805..ccf7756bd3a 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -5,25 +5,21 @@ import { NavLink } from 'react-router-dom' import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' import { RundownLayoutRundownHeader } from '@sofie-automation/meteor-lib/dist/collections/RundownLayouts' -import { VTContent } from '@sofie-automation/blueprints-integration' import { DBShowStyleVariant } from '@sofie-automation/corelib/dist/dataModel/ShowStyleVariant' 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 Navbar from 'react-bootstrap/Navbar' -import Moment from 'react-moment' import { RundownContextMenu, RundownHeaderContextMenuTrigger, RundownHamburgerButton } from './RundownContextMenu' import { TimeOfDay } from '../RundownTiming/TimeOfDay' -import { CurrentPartOrSegmentRemaining } from '../RundownTiming/CurrentPartOrSegmentRemaining' +import { CurrentPartOrSegmentRemaining } from '../RundownHeader/CurrentPartOrSegmentRemaining' import { RundownHeaderTimers } from './RundownHeaderTimers' -import { FreezeFrameIcon } from '../../../lib/ui/icons/freezeFrame' -import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' -import { PieceInstances, PartInstances } from '../../../collections/index' -import { useTiming, TimingTickResolution, TimingDataResolution } from '../RundownTiming/withTiming' -import { getPlaylistTimingDiff } from '../../../lib/rundownTiming' -import { RundownUtils } from '../../../lib/rundown' -import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' + +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 { @@ -93,6 +89,8 @@ export function RundownHeader({ playlist, studio, firstRundown }: IRundownHeader
+ + @@ -104,120 +102,3 @@ export function RundownHeader({ playlist, studio, firstRundown }: IRundownHeader ) } - -interface IRundownHeaderTimingDisplayProps { - playlist: DBRundownPlaylist -} - -function RundownHeaderTimingDisplay({ playlist }: IRundownHeaderTimingDisplayProps): JSX.Element | null { - const timingDurations = useTiming() - - const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) ?? 0 - const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(overUnderClock), false, false, true, true, true) - const isUnder = overUnderClock <= 0 - - return ( -
- - {isUnder ? 'Under' : 'Over'} - - {isUnder ? '−' : '+'} - {timeStr} - - -
- ) -} - -function RundownHeaderExpectedEnd({ playlist }: { playlist: DBRundownPlaylist }): JSX.Element | null { - const { t } = useTranslation() - const timingDurations = useTiming() - - const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) - const expectedEnd = PlaylistTiming.getExpectedEnd(playlist.timing) - const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) - - const now = timingDurations.currentTime ?? Date.now() - const estEnd = - expectedStart != null && timingDurations.remainingPlaylistDuration != null - ? now + timingDurations.remainingPlaylistDuration - : null - - if (!expectedEnd && !expectedDuration && !estEnd) return null - - return ( -
- {expectedEnd ? ( - - {t('Plan. End')} - - - - - ) : null} - {estEnd ? ( - - {t('Est. End')} - - - - - ) : null} -
- ) -} - -function HeaderFreezeFrameIcon({ partInstanceId }: { partInstanceId: PartInstanceId }) { - 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/RundownHeaderDurations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx new file mode 100644 index 00000000000..8c98979360b --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx @@ -0,0 +1,33 @@ +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 }: { playlist: DBRundownPlaylist }): JSX.Element | null { + const { t } = useTranslation() + const timingDurations = useTiming() + + const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) + const planned = + expectedDuration != null ? RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, true, true) : null + + const remainingMs = timingDurations.remainingPlaylistDuration + const startedMs = playlist.startedPlayback + const estDuration = + remainingMs != null && startedMs != null + ? (timingDurations.currentTime ?? Date.now()) - startedMs + remainingMs + : null + const estimated = + estDuration != null ? RundownUtils.formatDiffToTimecode(estDuration, false, true, true, true, true) : null + + if (!planned && !estimated) return null + + return ( +
+ {planned ? {planned} : null} + {estimated ? {estimated} : 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..c78f232685d --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -0,0 +1,29 @@ +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 }: { playlist: DBRundownPlaylist }): JSX.Element | null { + const { t } = useTranslation() + const timingDurations = useTiming() + + const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) + const expectedEnd = PlaylistTiming.getExpectedEnd(playlist.timing) + const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) + + const now = timingDurations.currentTime ?? Date.now() + const estEnd = + expectedStart != null && timingDurations.remainingPlaylistDuration != null + ? now + timingDurations.remainingPlaylistDuration + : null + + if (!expectedEnd && !expectedDuration && !estEnd) return null + + return ( +
+ {expectedEnd ? : null} + {estEnd ? : 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..7bc995f23eb --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx @@ -0,0 +1,17 @@ +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' + +export function RundownHeaderPlannedStart({ playlist }: { playlist: DBRundownPlaylist }): JSX.Element | null { + const { t } = useTranslation() + const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) + + if (expectedStart == null) return null + + return ( +
+ +
+ ) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index 0b7a5aec4fd..9e265990f44 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -4,6 +4,7 @@ import { useTiming } from '../RundownTiming/withTiming' import { RundownUtils } from '../../../lib/rundown' import classNames from 'classnames' import { getCurrentTime } from '../../../lib/systemTime' +import { Countdown } from './Countdown' interface IProps { tTimers: [RundownTTimer, RundownTTimer, RundownTTimer] @@ -45,7 +46,8 @@ function SingleTimer({ timer }: ISingleTimerProps) { const isCountingDown = timer.mode?.type === 'countdown' && diff < 0 && isRunning return ( -
- {timer.label} -
- {timerSign} - {parts.map((p, i) => ( - - - {p} - - {i < parts.length - 1 && :} - - ))} -
-
+ {timerSign} + {parts.map((p, i) => ( + + + {p} + + {i < parts.length - 1 && :} + + ))} + ) } 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..b6f1c971c1a --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx @@ -0,0 +1,30 @@ +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +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 timingDurations = useTiming() + + const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) ?? 0 + const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(overUnderClock), false, false, true, true, true) + const isUnder = overUnderClock <= 0 + + return ( +
+ + {isUnder ? 'Under' : 'Over'} + + {isUnder ? '−' : '+'} + {timeStr} + + +
+ ) +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx index 0ade4670753..809c544fff4 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx @@ -5,7 +5,7 @@ import { RundownLayoutRundownHeader } from '@sofie-automation/meteor-lib/dist/co import { useTranslation } from 'react-i18next' import * as RundownResolver from '../../../lib/RundownResolver' import { AutoNextStatus } from '../RundownTiming/AutoNextStatus' -import { CurrentPartOrSegmentRemaining } from '../RundownTiming/CurrentPartOrSegmentRemaining' +import { CurrentPartOrSegmentRemaining } from '../RundownHeader/CurrentPartOrSegmentRemaining' import { NextBreakTiming } from '../RundownTiming/NextBreakTiming' import { PlaylistEndTiming } from '../RundownTiming/PlaylistEndTiming' import { PlaylistStartTiming } from '../RundownTiming/PlaylistStartTiming' 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/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/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/SegmentTimeline.tsx b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx index 4adc96a3463..d851610ec7c 100644 --- a/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx +++ b/packages/webui/src/client/ui/SegmentTimeline/SegmentTimeline.tsx @@ -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' 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' From 132266951d28043b6542b1f82cd7d9fce4419b2f Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:53:25 +0100 Subject: [PATCH 178/291] top-bar ui: some style changes, implement est. end properly' --- .../RundownView/RundownHeader/Countdown.tsx | 4 +-- .../CurrentPartOrSegmentRemaining.tsx | 9 +++-- .../RundownHeader/RundownHeader.scss | 19 ++--------- .../RundownHeader/RundownHeader.tsx | 24 ++++++-------- .../RundownHeaderExpectedEnd.tsx | 33 +++++++++++++++---- 5 files changed, 46 insertions(+), 43 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx index 487f0a43679..7e79aaab315 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames' import './Countdown.scss' interface IProps { - label: string + label?: string time?: number className?: string children?: React.ReactNode @@ -13,7 +13,7 @@ interface IProps { export function Countdown({ label, time, className, children }: IProps): JSX.Element { return ( - {label} + {label && {label}} {time !== undefined ? : children} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx index 905e32b768c..e2a61436374 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx @@ -5,10 +5,13 @@ 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 @@ -129,11 +132,11 @@ export const CurrentPartOrSegmentRemaining: React.FC = (pro displayTimecode *= -1 return ( - 0 ? props.heavyClassName : undefined)} - role="timer" > {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} - + ) } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index f44c5ba570d..612018c8cc1 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -136,7 +136,6 @@ flex-direction: column; justify-content: center; /* Center vertically against the entire header height */ align-items: flex-end; - font-size: 0.75em; .timing__header_t-timers__timer { white-space: nowrap; @@ -217,14 +216,8 @@ transition: opacity 0.2s; } - &__value { - font-size: 1.4em; - font-variant-numeric: tabular-nums; - letter-spacing: 0.05em; - - .overtime { - color: $general-late-color; - } + .overtime { + color: $general-late-color; } } @@ -235,8 +228,6 @@ justify-content: flex-start; gap: 0.15em; min-width: 7em; - font-size: 0.75em; - padding-top: 0.8em; // Align with the top row of the T-Timers and Seg Budget } .rundown-header__onair-remaining__label { @@ -269,12 +260,6 @@ color: #40b8fa; } - .rundown-header__segment-remaining, - .rundown-header__onair-remaining, - .countdown { - color: white; - } - .rundown-header__segment-remaining__label, .rundown-header__onair-remaining__label { opacity: 1; diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index ccf7756bd3a..e5348473a4f 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -60,23 +60,19 @@ export function RundownHeader({ playlist, studio, firstRundown }: IRundownHeader
{t('Seg. Budg.')} - - - + {t('On Air')} - - - - + +
)} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx index c78f232685d..4bdc83b3780 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -1,5 +1,6 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { useTranslation } from 'react-i18next' import { Countdown } from './Countdown' import { useTiming } from '../RundownTiming/withTiming' @@ -8,17 +9,35 @@ export function RundownHeaderExpectedEnd({ playlist }: { playlist: DBRundownPlay const { t } = useTranslation() const timingDurations = useTiming() - const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) const expectedEnd = PlaylistTiming.getExpectedEnd(playlist.timing) - const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) const now = timingDurations.currentTime ?? Date.now() - const estEnd = - expectedStart != null && timingDurations.remainingPlaylistDuration != null - ? now + timingDurations.remainingPlaylistDuration - : null - if (!expectedEnd && !expectedDuration && !estEnd) return null + // Calculate Est. End by summing partExpectedDurations for all parts after the current one. + // Both partStartsAt and partExpectedDurations use PartInstanceId keys, so they match. + let estEnd: number | null = null + const currentPartInstanceId = playlist.currentPartInfo?.partInstanceId + const partStartsAt = timingDurations.partStartsAt + const partExpectedDurations = timingDurations.partExpectedDurations + + if (currentPartInstanceId && partStartsAt && partExpectedDurations) { + const currentKey = unprotectString(currentPartInstanceId) + const currentStartsAt = partStartsAt[currentKey] + + if (currentStartsAt != null) { + let remainingDuration = 0 + for (const [partId, startsAt] of Object.entries(partStartsAt)) { + if (startsAt > currentStartsAt) { + remainingDuration += partExpectedDurations[partId] ?? 0 + } + } + if (remainingDuration > 0) { + estEnd = now + remainingDuration + } + } + } + + if (!expectedEnd && !estEnd) return null return (
From fbd7c4ea3a85c5918f62a6925f157f3bc84d2e9d Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 26 Feb 2026 11:53:36 +0100 Subject: [PATCH 179/291] chore: Styling walltime clock with correct font properties. --- .../ui/RundownView/RundownHeader/RundownHeader.scss | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 612018c8cc1..eaffe9bd884 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -7,7 +7,9 @@ width: 100%; border-bottom: 1px solid #333; transition: background-color 0.5s; - font-family: 'Roboto Flex', sans-serif; + font-family: 'Roboto Flex', 'Roboto', sans-serif; + font-feature-settings: 'liga' 0, 'tnum'; + font-variant-numeric: tabular-nums; .rundown-header__trigger { height: 100%; @@ -70,14 +72,13 @@ .timing-clock { color: #40b8fa; font-size: 1.4em; - font-variant-numeric: tabular-nums; - letter-spacing: 0.05em; + + letter-spacing: 0.0 em; transition: color 0.2s; &.time-now { - font-size: 1.6em; - font-style: italic; - font-weight: 300; + font-size: 1.8em; + font-variation-settings: 'wdth' 70, 'wght' 400, 'slnt' -5, 'GRAD' 0, 'opsz' 44, 'XOPQ' 96, 'XTRA' 468, 'YOPQ' 79, 'YTAS' 750, 'YTFI' 738, 'YTLC' 548, 'YTDE' -203, 'YTUC' 712; } } From a24d144e56b7590ffc07c9f7f35ed7d5b5d59da6 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 26 Feb 2026 13:15:55 +0100 Subject: [PATCH 180/291] chore: Styling of Over/Under labels and ON AIR label. --- .../RundownHeader/RundownHeader.scss | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index eaffe9bd884..28ef20814fa 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -92,16 +92,15 @@ display: flex; align-items: center; gap: 0.4em; - font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif; font-variant-numeric: tabular-nums; white-space: nowrap; .rundown-header__diff__label { - font-size: 0.85em; - font-weight: 700; + 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; text-transform: uppercase; - letter-spacing: 0.06em; - color: #888; + letter-spacing: 0.01em; + color: #666666; } .rundown-header__diff__chip { @@ -143,11 +142,9 @@ line-height: 1.25; .countdown__label { - color: #b8b8b8; } .countdown__value { - font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif; font-weight: 500; color: #fff; } @@ -209,9 +206,9 @@ transition: color 0.2s; &__label { - font-size: 0.7em; - font-weight: 600; - letter-spacing: 0.1em; + font-size: 0.8em; + font-variation-settings: 'wdth' 80, 'wght' 600, '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.05em; text-transform: uppercase; opacity: 0; transition: opacity 0.2s; @@ -233,8 +230,8 @@ .rundown-header__onair-remaining__label { background-color: $general-live-color; - color: #fff; - padding: 0.1em 0.6em 0.1em 0.3em; + color: #ffffff; + padding: 0.03em 0.45em 0.02em 0.2em; border-radius: 2px 999px 999px 2px; font-weight: bold; opacity: 1 !important; From b1bac188c77861c8b1a7bc47ce62e419096de899 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Fri, 27 Feb 2026 10:45:38 +0100 Subject: [PATCH 181/291] chore: Common label style for header labels that react to hover. Fixed the hover state of labels. Show timer labels are still font-size 600 for some weird reason, needs to be fixed later. --- .../RundownView/RundownHeader/Countdown.scss | 3 +- .../RundownHeader/RundownHeader.scss | 42 ++++++++++++------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index 16e73ba3852..b46ccad9ab9 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -1,4 +1,5 @@ @import '../../../styles/colorScheme'; +@import './RundownHeader.scss'; .countdown { display: flex; @@ -9,10 +10,10 @@ transition: color 0.2s; &__label { + @extend .rundown-header__hoverable-label; font-size: 0.7em; font-weight: 600; letter-spacing: 0.1em; - text-transform: uppercase; white-space: nowrap; } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 28ef20814fa..474cf839f0b 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -96,16 +96,16 @@ white-space: nowrap; .rundown-header__diff__label { + @extend .rundown-header__hoverable-label; 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; - text-transform: uppercase; - letter-spacing: 0.01em; - color: #666666; + color: #fff; + opacity: 0.6; } .rundown-header__diff__chip { font-size: 1.1em; - font-weight: 500; + //font-weight: 500; padding: 0.15em 0.75em; border-radius: 999px; font-variant-numeric: tabular-nums; @@ -142,10 +142,12 @@ line-height: 1.25; .countdown__label { + @extend .rundown-header__hoverable-label; + font-size: 0.65em; } .countdown__value { - font-weight: 500; + //font-weight: 500; color: #fff; } @@ -153,7 +155,7 @@ display: inline-block; width: 0.6em; text-align: center; - font-weight: 500; + //font-weight: 500; font-size: 1.1em; color: #fff; margin-right: 0.3em; @@ -196,6 +198,16 @@ gap: 0.1em; } + // Common label style for header labels that react to hover + .rundown-header__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: 0.6; + transition: opacity 0.2s; + } + .rundown-header__segment-remaining, .rundown-header__onair-remaining { display: flex; @@ -206,12 +218,7 @@ transition: color 0.2s; &__label { - font-size: 0.8em; - font-variation-settings: 'wdth' 80, 'wght' 600, '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.05em; - text-transform: uppercase; - opacity: 0; - transition: opacity 0.2s; + @extend .rundown-header__hoverable-label; } .overtime { @@ -233,7 +240,11 @@ color: #ffffff; padding: 0.03em 0.45em 0.02em 0.2em; border-radius: 2px 999px 999px 2px; - font-weight: bold; + // 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 { @@ -258,7 +269,10 @@ color: #40b8fa; } - .rundown-header__segment-remaining__label, + .rundown-header__hoverable-label { + opacity: 1; + } + .rundown-header__onair-remaining__label { opacity: 1; } From 527ad66f4d30bccfb869a7e0b5e825dfbeb2e7ed Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Fri, 27 Feb 2026 12:34:39 +0100 Subject: [PATCH 182/291] chore: Updated styling on the Over/Under (previously known as "Diff") counter and adjusted the close icon placement. --- .../RundownView/RundownHeader/RundownHeader.scss | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 474cf839f0b..1fc71523d3d 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -85,7 +85,7 @@ .rundown-header__timing-display { display: flex; align-items: center; - margin-right: 1.5em; + margin-right: 0.5em; margin-left: 2em; .rundown-header__diff { @@ -99,28 +99,27 @@ @extend .rundown-header__hoverable-label; 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; - color: #fff; opacity: 0.6; } .rundown-header__diff__chip { - font-size: 1.1em; - //font-weight: 500; - padding: 0.15em 0.75em; + font-size: 1.2em; + padding: 0.0em 0.3em; border-radius: 999px; - font-variant-numeric: tabular-nums; + font-variation-settings: 'wdth' 25, 'wght' 600, 'slnt' 0, 'GRAD' 0, 'opsz' 20, 'XOPQ' 96, 'XTRA' 468, 'YOPQ' 79, 'YTAS' 750, 'YTFI' 738, 'YTLC' 548, 'YTDE' -203, 'YTUC' 712; + letter-spacing: -0.02em; } &.rundown-header__diff--under { .rundown-header__diff__chip { - background-color: #c8a800; + background-color: #ff0;//$general-fast-color; color: #000; } } &.rundown-header__diff--over { .rundown-header__diff__chip { - background-color: #b00; + background-color: $general-late-color; color: #fff; } } @@ -258,6 +257,7 @@ .rundown-header__close-btn { display: flex; align-items: center; + margin-right: 0.75em; cursor: pointer; color: #40b8fa; opacity: 0; From e846efea94a530ce7d7c0da9daec31bee97c077e Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Fri, 27 Feb 2026 12:41:00 +0100 Subject: [PATCH 183/291] chore: Removed extra styling of the labels of the Show Counters group. --- .../src/client/ui/RundownView/RundownHeader/Countdown.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index b46ccad9ab9..1ad7a17eaa8 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -6,14 +6,11 @@ align-items: baseline; justify-content: space-between; gap: 0.6em; - color: rgba(255, 255, 255, 0.6); + //color: rgba(255, 255, 255, 0.6); transition: color 0.2s; &__label { @extend .rundown-header__hoverable-label; - font-size: 0.7em; - font-weight: 600; - letter-spacing: 0.1em; white-space: nowrap; } From ad209e7788cc15f81e6e6e4da3d98cc1f21f9e39 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:30:58 +0100 Subject: [PATCH 184/291] add simplified mode to top bar --- .../RundownView/RundownHeader/Countdown.scss | 1 + .../RundownHeader/RundownHeader.scss | 92 +++++++++++++++++-- .../RundownHeader/RundownHeader.tsx | 10 +- .../RundownHeader/RundownHeaderDurations.tsx | 36 ++++++-- .../RundownHeaderExpectedEnd.tsx | 42 ++++----- .../RundownHeaderPlannedStart.tsx | 23 ++++- .../RundownHeader/remainingDuration.ts | 26 ++++++ 7 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/remainingDuration.ts diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index 1ad7a17eaa8..4a0819ba622 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -15,6 +15,7 @@ } &__value { + margin-left: auto; font-size: 1.4em; font-variant-numeric: tabular-nums; letter-spacing: 0.05em; diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 1fc71523d3d..c00eafc7cce 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -8,7 +8,9 @@ border-bottom: 1px solid #333; transition: background-color 0.5s; font-family: 'Roboto Flex', 'Roboto', sans-serif; - font-feature-settings: 'liga' 0, 'tnum'; + font-feature-settings: + 'liga' 0, + 'tnum'; font-variant-numeric: tabular-nums; .rundown-header__trigger { @@ -73,12 +75,25 @@ color: #40b8fa; font-size: 1.4em; - letter-spacing: 0.0 em; + letter-spacing: 0 em; transition: color 0.2s; &.time-now { font-size: 1.8em; - font-variation-settings: 'wdth' 70, 'wght' 400, 'slnt' -5, 'GRAD' 0, 'opsz' 44, 'XOPQ' 96, 'XTRA' 468, 'YOPQ' 79, 'YTAS' 750, 'YTFI' 738, 'YTLC' 548, 'YTDE' -203, 'YTUC' 712; + font-variation-settings: + 'wdth' 70, + 'wght' 400, + 'slnt' -5, + 'GRAD' 0, + 'opsz' 44, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; } } @@ -98,21 +113,47 @@ .rundown-header__diff__label { @extend .rundown-header__hoverable-label; 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; + 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; opacity: 0.6; } .rundown-header__diff__chip { font-size: 1.2em; - padding: 0.0em 0.3em; + padding: 0em 0.3em; border-radius: 999px; - font-variation-settings: 'wdth' 25, 'wght' 600, 'slnt' 0, 'GRAD' 0, 'opsz' 20, 'XOPQ' 96, 'XTRA' 468, 'YOPQ' 79, 'YTAS' 750, 'YTFI' 738, 'YTLC' 548, 'YTDE' -203, 'YTUC' 712; + font-variation-settings: + 'wdth' 25, + 'wght' 600, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 20, + 'XOPQ' 96, + 'XTRA' 468, + 'YOPQ' 79, + 'YTAS' 750, + 'YTFI' 738, + 'YTLC' 548, + 'YTDE' -203, + 'YTUC' 712; letter-spacing: -0.02em; } &.rundown-header__diff--under { .rundown-header__diff__chip { - background-color: #ff0;//$general-fast-color; + background-color: #ff0; //$general-fast-color; color: #000; } } @@ -200,7 +241,20 @@ // Common label style for header labels that react to hover .rundown-header__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; + 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: 0.6; @@ -234,6 +288,13 @@ min-width: 7em; } + .rundown-header__timing-group { + display: flex; + align-items: center; + gap: 1em; + cursor: pointer; + } + .rundown-header__onair-remaining__label { background-color: $general-live-color; color: #ffffff; @@ -242,7 +303,20 @@ // 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; + 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; diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index e5348473a4f..3cd3e59270d 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -1,3 +1,4 @@ +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import ClassNames from 'classnames' @@ -38,6 +39,7 @@ interface IRundownHeaderProps { export function RundownHeader({ playlist, studio, firstRundown }: IRundownHeaderProps): JSX.Element { const { t } = useTranslation() + const [simplified, setSimplified] = useState(false) return ( <> @@ -85,9 +87,11 @@ export function RundownHeader({ playlist, studio, firstRundown }: IRundownHeader
- - - +
setSimplified((s) => !s)}> + + + +
diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx index 8c98979360b..3a978a640a3 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx @@ -4,8 +4,15 @@ import { useTranslation } from 'react-i18next' import { Countdown } from './Countdown' import { useTiming } from '../RundownTiming/withTiming' import { RundownUtils } from '../../../lib/rundown' +import { getRemainingDurationFromCurrentPart } from './remainingDuration' -export function RundownHeaderDurations({ playlist }: { playlist: DBRundownPlaylist }): JSX.Element | null { +export function RundownHeaderDurations({ + playlist, + simplified, +}: { + playlist: DBRundownPlaylist + simplified?: boolean +}): JSX.Element | null { const { t } = useTranslation() const timingDurations = useTiming() @@ -13,12 +20,25 @@ export function RundownHeaderDurations({ playlist }: { playlist: DBRundownPlayli const planned = expectedDuration != null ? RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, true, true) : null - const remainingMs = timingDurations.remainingPlaylistDuration - const startedMs = playlist.startedPlayback - const estDuration = - remainingMs != null && startedMs != null - ? (timingDurations.currentTime ?? Date.now()) - startedMs + remainingMs - : null + const now = timingDurations.currentTime ?? Date.now() + const currentPartInstanceId = playlist.currentPartInfo?.partInstanceId + + let estDuration: number | null = null + if (currentPartInstanceId && timingDurations.partStartsAt && timingDurations.partExpectedDurations) { + const remaining = getRemainingDurationFromCurrentPart( + currentPartInstanceId, + timingDurations.partStartsAt, + timingDurations.partExpectedDurations + ) + if (remaining != null) { + const elapsed = + playlist.startedPlayback != null + ? now - playlist.startedPlayback + : (timingDurations.asDisplayedPlaylistDuration ?? 0) + estDuration = elapsed + remaining + } + } + const estimated = estDuration != null ? RundownUtils.formatDiffToTimecode(estDuration, false, true, true, true, true) : null @@ -27,7 +47,7 @@ export function RundownHeaderDurations({ playlist }: { playlist: DBRundownPlayli return (
{planned ? {planned} : null} - {estimated ? {estimated} : null} + {!simplified && estimated ? {estimated} : null}
) } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx index 4bdc83b3780..a656041756f 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -1,39 +1,33 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' -import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' import { useTranslation } from 'react-i18next' import { Countdown } from './Countdown' import { useTiming } from '../RundownTiming/withTiming' - -export function RundownHeaderExpectedEnd({ playlist }: { playlist: DBRundownPlaylist }): JSX.Element | null { +import { getRemainingDurationFromCurrentPart } from './remainingDuration' + +export function RundownHeaderExpectedEnd({ + playlist, + simplified, +}: { + playlist: DBRundownPlaylist + simplified?: boolean +}): JSX.Element | null { const { t } = useTranslation() const timingDurations = useTiming() const expectedEnd = PlaylistTiming.getExpectedEnd(playlist.timing) - const now = timingDurations.currentTime ?? Date.now() - // Calculate Est. End by summing partExpectedDurations for all parts after the current one. - // Both partStartsAt and partExpectedDurations use PartInstanceId keys, so they match. let estEnd: number | null = null const currentPartInstanceId = playlist.currentPartInfo?.partInstanceId - const partStartsAt = timingDurations.partStartsAt - const partExpectedDurations = timingDurations.partExpectedDurations - - if (currentPartInstanceId && partStartsAt && partExpectedDurations) { - const currentKey = unprotectString(currentPartInstanceId) - const currentStartsAt = partStartsAt[currentKey] - - if (currentStartsAt != null) { - let remainingDuration = 0 - for (const [partId, startsAt] of Object.entries(partStartsAt)) { - if (startsAt > currentStartsAt) { - remainingDuration += partExpectedDurations[partId] ?? 0 - } - } - if (remainingDuration > 0) { - estEnd = now + remainingDuration - } + if (currentPartInstanceId && timingDurations.partStartsAt && timingDurations.partExpectedDurations) { + const remaining = getRemainingDurationFromCurrentPart( + currentPartInstanceId, + timingDurations.partStartsAt, + timingDurations.partExpectedDurations + ) + if (remaining != null && remaining > 0) { + estEnd = now + remaining } } @@ -42,7 +36,7 @@ export function RundownHeaderExpectedEnd({ playlist }: { playlist: DBRundownPlay return (
{expectedEnd ? : null} - {estEnd ? : null} + {!simplified && estEnd ? : null}
) } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx index 7bc995f23eb..8042bea57b5 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx @@ -2,16 +2,37 @@ import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/Rund 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 }: { playlist: DBRundownPlaylist }): JSX.Element | null { +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 && + (playlist.startedPlayback ? ( + + ) : ( + + {diff >= 0 && '-'} + {RundownUtils.formatDiffToTimecode(Math.abs(diff), false, false, true, true, true)} + + ))}
) } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/remainingDuration.ts b/packages/webui/src/client/ui/RundownView/RundownHeader/remainingDuration.ts new file mode 100644 index 00000000000..b54bb6c74fc --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/remainingDuration.ts @@ -0,0 +1,26 @@ +import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' + +/** + * Compute the sum of expected durations of all parts after the current part. + * Uses partStartsAt to determine ordering and partExpectedDurations for the values. + * Returns 0 if the current part can't be found or there are no future parts. + */ +export function getRemainingDurationFromCurrentPart( + currentPartInstanceId: PartInstanceId, + partStartsAt: Record, + partExpectedDurations: Record +): number | null { + const currentKey = unprotectString(currentPartInstanceId) + const currentStartsAt = partStartsAt[currentKey] + + if (currentStartsAt == null) return null + + let remaining = 0 + for (const [partId, startsAt] of Object.entries(partStartsAt)) { + if (startsAt > currentStartsAt) { + remaining += partExpectedDurations[partId] ?? 0 + } + } + return remaining +} From c55f73bf0f1ae6635a2387b822d4f8984056a17d Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:13:11 +0100 Subject: [PATCH 185/291] New top bar UI: refactor class names --- .../RundownHeader/RundownContextMenu.tsx | 2 +- .../RundownHeader/RundownHeader.scss | 91 +++++++++++++------ .../RundownHeader/RundownHeader.tsx | 19 ++-- .../RundownHeader/RundownHeaderDurations.tsx | 14 ++- .../RundownHeaderExpectedEnd.tsx | 10 +- .../RundownHeaderPlannedStart.tsx | 4 +- .../RundownHeader/RundownHeaderTimers.tsx | 26 +++--- .../RundownHeaderTimingDisplay.tsx | 10 +- .../RundownView/RundownTiming/TimeOfDay.tsx | 5 +- 9 files changed, 117 insertions(+), 64 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx index b91da2b0b24..3941357cffd 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx @@ -172,7 +172,7 @@ export function RundownHamburgerButton(): JSX.Element { }, []) return ( - ) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index c00eafc7cce..28b61de95e7 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -27,13 +27,13 @@ &.active { background: $color-header-on-air; - .rundown-header__segment-remaining, - .rundown-header__onair-remaining, - .countdown { + .rundown-header__timers-segment-remaining, + .rundown-header__timers-onair-remaining, + .rundown-header__show-timers-countdown { color: #fff; } - .timing__header_t-timers__timer__label { + .rundown-header__clocks-timers__timer__label { color: rgba(255, 255, 255, 0.9); } @@ -65,7 +65,7 @@ gap: 1em; } - .rundown-header__center { + .rundown-header__clocks { display: flex; align-items: center; justify-content: center; @@ -97,20 +97,35 @@ } } - .rundown-header__timing-display { + .rundown-header__clocks-clock-group { + display: flex; + flex-direction: column; + align-items: center; + } + + .rundown-header__clocks-playlist-name { + @extend .rundown-header__hoverable-label; + font-size: 0.65em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 20em; + } + + .rundown-header__clocks-timing-display { display: flex; align-items: center; margin-right: 0.5em; margin-left: 2em; - .rundown-header__diff { + .rundown-header__clocks-diff { display: flex; align-items: center; gap: 0.4em; font-variant-numeric: tabular-nums; white-space: nowrap; - .rundown-header__diff__label { + .rundown-header__clocks-diff__label { @extend .rundown-header__hoverable-label; font-size: 0.7em; font-variation-settings: @@ -130,7 +145,7 @@ opacity: 0.6; } - .rundown-header__diff__chip { + .rundown-header__clocks-diff__chip { font-size: 1.2em; padding: 0em 0.3em; border-radius: 999px; @@ -151,15 +166,15 @@ letter-spacing: -0.02em; } - &.rundown-header__diff--under { - .rundown-header__diff__chip { + &.rundown-header__clocks-diff--under { + .rundown-header__clocks-diff__chip { background-color: #ff0; //$general-fast-color; color: #000; } } - &.rundown-header__diff--over { - .rundown-header__diff__chip { + &.rundown-header__clocks-diff--over { + .rundown-header__clocks-diff__chip { background-color: $general-late-color; color: #fff; } @@ -167,7 +182,7 @@ } } - .timing__header_t-timers { + .rundown-header__clocks-timers { position: absolute; left: 28%; /* Position exactly between the 15% left edge content and the 50% center clock */ top: 0; @@ -177,7 +192,7 @@ justify-content: center; /* Center vertically against the entire header height */ align-items: flex-end; - .timing__header_t-timers__timer { + .rundown-header__clocks-timers__timer { white-space: nowrap; line-height: 1.25; @@ -191,7 +206,7 @@ color: #fff; } - .timing__header_t-timers__timer__sign { + .rundown-header__clocks-timers__timer__sign { display: inline-block; width: 0.6em; text-align: center; @@ -201,14 +216,14 @@ margin-right: 0.3em; } - .timing__header_t-timers__timer__part { + .rundown-header__clocks-timers__timer__part { color: #fff; - &.timing__header_t-timers__timer__part--dimmed { + &.rundown-header__clocks-timers__timer__part--dimmed { color: #888; font-weight: 400; } } - .timing__header_t-timers__timer__separator { + .rundown-header__clocks-timers__timer__separator { margin: 0 0.05em; color: #888; } @@ -216,7 +231,7 @@ } } - .rundown-header__hamburger-btn { + .rundown-header__menu-btn { background: none; border: none; color: #40b8fa99; @@ -230,7 +245,7 @@ transition: color 0.2s; } - .rundown-header__timers { + .rundown-header__onair { display: flex; flex-direction: column; align-items: stretch; @@ -261,8 +276,8 @@ transition: opacity 0.2s; } - .rundown-header__segment-remaining, - .rundown-header__onair-remaining { + .rundown-header__timers-segment-remaining, + .rundown-header__timers-onair-remaining { display: flex; align-items: center; justify-content: space-between; @@ -280,7 +295,7 @@ } // Stacked Plan. Start / Plan. End / Est. End in right section - .rundown-header__endtimes { + .rundown-header__show-timers-endtimes { display: flex; flex-direction: column; justify-content: flex-start; @@ -288,14 +303,34 @@ min-width: 7em; } - .rundown-header__timing-group { + .rundown-header__show-timers { display: flex; align-items: center; gap: 1em; cursor: pointer; } - .rundown-header__onair-remaining__label { + .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__value { + margin-left: auto; + font-size: 1.4em; + font-variant-numeric: tabular-nums; + letter-spacing: 0.05em; + } + } + + .rundown-header__timers-onair-remaining__label { background-color: $general-live-color; color: #ffffff; padding: 0.03em 0.45em 0.02em 0.2em; @@ -339,7 +374,7 @@ } &:hover { - .rundown-header__hamburger-btn { + .rundown-header__menu-btn { color: #40b8fa; } @@ -347,7 +382,7 @@ opacity: 1; } - .rundown-header__onair-remaining__label { + .rundown-header__timers-onair-remaining__label { opacity: 1; } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index 3cd3e59270d..18aac1abfd9 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -59,17 +59,17 @@ export function RundownHeader({ playlist, studio, firstRundown }: IRundownHeader
{playlist.currentPartInfo && ( -
- - {t('Seg. Budg.')} +
+ + {t('Seg. Budg.')} - - {t('On Air')} + + {t('On Air')} -
+
- +
+ + {playlist.name} +
-
setSimplified((s) => !s)}> +
setSimplified((s) => !s)}> diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx index 3a978a640a3..8c800c5f945 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx @@ -45,9 +45,17 @@ export function RundownHeaderDurations({ if (!planned && !estimated) return null return ( -
- {planned ? {planned} : null} - {!simplified && estimated ? {estimated} : null} +
+ {planned ? ( + + {planned} + + ) : null} + {!simplified && estimated ? ( + + {estimated} + + ) : null}
) } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx index a656041756f..fe90f5b80ac 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -34,9 +34,13 @@ export function RundownHeaderExpectedEnd({ if (!expectedEnd && !estEnd) return null return ( -
- {expectedEnd ? : null} - {!simplified && estEnd ? : null} +
+ {expectedEnd ? ( + + ) : null} + {!simplified && estEnd ? ( + + ) : null}
) } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx index 8042bea57b5..f764b037fe7 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx @@ -22,8 +22,8 @@ export function RundownHeaderPlannedStart({ const diff = now - expectedStart return ( -
- +
+ {!simplified && (playlist.startedPlayback ? ( diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index 9e265990f44..77e46062633 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -20,7 +20,7 @@ export const RundownHeaderTimers: React.FC = ({ tTimers }) => { const activeTimers = tTimers.filter((t) => t.mode) return ( -
+
{activeTimers.map((timer) => ( ))} @@ -48,28 +48,28 @@ function SingleTimer({ timer }: ISingleTimerProps) { return ( - {timerSign} + {timerSign} {parts.map((p, i) => ( {p} - {i < parts.length - 1 && :} + {i < parts.length - 1 && :} ))} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx index b6f1c971c1a..fceea32777f 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx @@ -15,12 +15,14 @@ export function RundownHeaderTimingDisplay({ playlist }: IRundownHeaderTimingDis const isUnder = overUnderClock <= 0 return ( -
+
- {isUnder ? 'Under' : 'Over'} - + {isUnder ? 'Under' : 'Over'} + {isUnder ? '−' : '+'} {timeStr} diff --git a/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx b/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx index 47f205ffd76..75c17b02dd1 100644 --- a/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownTiming/TimeOfDay.tsx @@ -1,11 +1,12 @@ import { useTiming } from './withTiming.js' import Moment from 'react-moment' +import classNames from 'classnames' -export function TimeOfDay(): JSX.Element { +export function TimeOfDay({ className }: { className?: string }): JSX.Element { const timingDurations = useTiming() return ( - + ) From 8b4d8a560b3fcf5ffbd7431ea7a1b4d21bda3a4e Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:42:37 +0100 Subject: [PATCH 186/291] New top bar UI: remove caret on text and restore legacy component for remaining part time for old uses --- .../CurrentPartOrSegmentRemaining.tsx | 33 ++++++++++++++++++- .../RundownHeader/RundownHeader.scss | 4 +-- .../RundownHeader/RundownHeader.tsx | 6 ++-- 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx index e2a61436374..1f85a03ec42 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx @@ -80,7 +80,7 @@ function vibrate(displayTime: number) { } } -export const CurrentPartOrSegmentRemaining: React.FC = (props) => { +function usePartRemaining(props: IPartRemainingProps) { const timingDurations = useTiming(TimingTickResolution.Synced, TimingDataResolution.Synced) const prevPartInstanceId = useRef(null) @@ -131,6 +131,37 @@ export const CurrentPartOrSegmentRemaining: React.FC = (pro 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 ( {t('Seg. Budg.')} - {t('On Air')} - From e6fb449bdd3ddfcc2ca9247b902f20c2be487f99 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:15:55 +0100 Subject: [PATCH 187/291] new top bar UI: make playlist name hidden until hovered over --- .../ui/RundownView/RundownHeader/RundownHeader.scss | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index b33f011554f..f04e5eea118 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -106,12 +106,14 @@ } .rundown-header__clocks-playlist-name { - @extend .rundown-header__hoverable-label; font-size: 0.65em; white-space: nowrap; - overflow: hidden; text-overflow: ellipsis; max-width: 20em; + color: #fff; + max-height: 0; + overflow: hidden; + transition: max-height 0.2s ease; } .rundown-header__clocks-timing-display { @@ -389,5 +391,11 @@ .rundown-header__close-btn { opacity: 1; } + + .rundown-header__clocks-clock-group { + .rundown-header__clocks-playlist-name { + max-height: 1.5em; + } + } } } From d6bd52dc58c5b4e80412f9038425519c1e51590d Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Tue, 13 Jan 2026 16:53:31 +0000 Subject: [PATCH 188/291] Update yarn.lock --- meteor/yarn.lock | 56 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index bfce2542a30..2f038e8f42c 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -12,6 +12,17 @@ __metadata: languageName: node linkType: hard +"@acuminous/bitsyntax@npm:^0.1.2": + version: 0.1.2 + resolution: "@acuminous/bitsyntax@npm:0.1.2" + dependencies: + buffer-more-ints: "npm:~1.0.0" + debug: "npm:^4.3.4" + safe-buffer: "npm:~5.1.2" + checksum: 10/abdc4313ae08e52fb8eeaebf53759c3b9a38983a696d77c46c24de1c065247355a1b5c02ad3618700d3fb3628ccf3ec39227a080bd1fe7adc864144ccf84b0cc + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @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" @@ -1346,6 +1357,7 @@ __metadata: "@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.15.0" @@ -2228,6 +2240,17 @@ __metadata: languageName: node linkType: hard +"amqplib@npm:0.10.5": + version: 0.10.5 + resolution: "amqplib@npm:0.10.5" + dependencies: + "@acuminous/bitsyntax": "npm:^0.1.2" + buffer-more-ints: "npm:~1.0.0" + url-parse: "npm:~1.5.10" + checksum: 10/bcf4bda790f8a356ba4c7d3054ae3ee397a48d6c4d51f1015f703dd7205c097ba9772577567a06eb470d13e0becdc4163c857299e50eb5a4bc888e3007832f87 + languageName: node + linkType: hard + "ansi-escapes@npm:^4.3.2": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" @@ -2875,6 +2898,13 @@ __metadata: languageName: node linkType: hard +"buffer-more-ints@npm:~1.0.0": + version: 1.0.0 + resolution: "buffer-more-ints@npm:1.0.0" + checksum: 10/603a7f35793426c8efd733eb716c2c3bf3e2f5bab95ca13ba31546d89ead3636586479c5a0d8438dd015115361a3b09b1b37ddabc170b6d42bc6c6dc2554dc61 + languageName: node + linkType: hard + "buffer-xor@npm:^1.0.3": version: 1.0.3 resolution: "buffer-xor@npm:1.0.3" @@ -8534,6 +8564,13 @@ __metadata: languageName: node linkType: hard +"querystringify@npm:^2.1.1": + version: 2.2.0 + resolution: "querystringify@npm:2.2.0" + checksum: 10/46ab16f252fd892fc29d6af60966d338cdfeea68a231e9457631ffd22d67cec1e00141e0a5236a2eb16c0d7d74175d9ec1d6f963660c6f2b1c2fc85b194c5680 + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -8831,6 +8868,13 @@ __metadata: languageName: node linkType: hard +"requires-port@npm:^1.0.0": + version: 1.0.0 + resolution: "requires-port@npm:1.0.0" + checksum: 10/878880ee78ccdce372784f62f52a272048e2d0827c29ae31e7f99da18b62a2b9463ea03a75f277352f4697c100183debb0532371ad515a2d49d4bfe596dd4c20 + languageName: node + linkType: hard + "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -8958,7 +9002,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.1.2, safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": +"safe-buffer@npm:5.1.2, safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1, safe-buffer@npm:~5.1.2": version: 5.1.2 resolution: "safe-buffer@npm:5.1.2" checksum: 10/7eb5b48f2ed9a594a4795677d5a150faa7eb54483b2318b568dc0c4fc94092a6cce5be02c7288a0500a156282f5276d5688bce7259299568d1053b2150ef374a @@ -10392,6 +10436,16 @@ __metadata: languageName: node linkType: hard +"url-parse@npm:~1.5.10": + version: 1.5.10 + resolution: "url-parse@npm:1.5.10" + dependencies: + querystringify: "npm:^2.1.1" + requires-port: "npm:^1.0.0" + checksum: 10/c9e96bc8c5b34e9f05ddfeffc12f6aadecbb0d971b3cc26015b58d5b44676a99f50d5aeb1e5c9e61fa4d49961ae3ab1ae997369ed44da51b2f5ac010d188e6ad + languageName: node + linkType: hard + "url@npm:^0.11.4": version: 0.11.4 resolution: "url@npm:0.11.4" From 0dd04eee32b917947e6a32fe05f91a07a174edce Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 14 Jan 2026 13:11:01 +0000 Subject: [PATCH 189/291] Tidy migration Remove unneccesary cast to any --- meteor/server/migration/X_X_X.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meteor/server/migration/X_X_X.ts b/meteor/server/migration/X_X_X.ts index 5940d5acbea..31712163a52 100644 --- a/meteor/server/migration/X_X_X.ts +++ b/meteor/server/migration/X_X_X.ts @@ -69,7 +69,7 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [ }, migrate: async () => { await RundownPlaylists.mutableCollection.updateAsync( - { tTimers: { $exists: false } } as any, + { tTimers: { $exists: false } }, { $set: { tTimers: [ From 8c171635e9594175c85a796ee749e26d3e8a2cb1 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Wed, 14 Jan 2026 14:27:48 +0000 Subject: [PATCH 190/291] Change path of import --- .../src/blueprints/context/services/TTimersService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/job-worker/src/blueprints/context/services/TTimersService.ts b/packages/job-worker/src/blueprints/context/services/TTimersService.ts index b1eeafd49c6..1344b6f9761 100644 --- a/packages/job-worker/src/blueprints/context/services/TTimersService.ts +++ b/packages/job-worker/src/blueprints/context/services/TTimersService.ts @@ -15,7 +15,7 @@ import { resumeTTimer, validateTTimerIndex, } from '../../../playout/tTimers.js' -import { getCurrentTime } from '../../../lib/time.js' +import { getCurrentTime } from '../../../lib/index.js' export class TTimersService { readonly timers: [PlaylistTTimerImpl, PlaylistTTimerImpl, PlaylistTTimerImpl] From 3470bcbac14d7c3339c4e51a9953a3131f9be4f8 Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 19 Jan 2026 16:32:24 +0000 Subject: [PATCH 191/291] Update yarn.lock --- packages/yarn.lock | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/yarn.lock b/packages/yarn.lock index 4ad64d34d4e..6d704d11fb6 100644 --- a/packages/yarn.lock +++ b/packages/yarn.lock @@ -5714,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: @@ -5753,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" @@ -22502,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: @@ -22512,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" From 681e1d01b41f934b713be7cc60b88a0b6b2eb37d Mon Sep 17 00:00:00 2001 From: Julian Waller Date: Mon, 19 Jan 2026 17:02:29 +0000 Subject: [PATCH 192/291] lockfile --- meteor/yarn.lock | 75 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/meteor/yarn.lock b/meteor/yarn.lock index 2f038e8f42c..d44156de6cc 100644 --- a/meteor/yarn.lock +++ b/meteor/yarn.lock @@ -2189,7 +2189,16 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.4, acorn@npm:^8.14.0, acorn@npm:^8.15.0": +"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: @@ -2984,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: @@ -2994,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" @@ -6011,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: @@ -6783,8 +6811,8 @@ __metadata: linkType: hard "koa@npm:^2.13.4": - version: 2.16.3 - resolution: "koa@npm:2.16.3" + version: 2.15.3 + resolution: "koa@npm:2.15.3" dependencies: accepts: "npm:^1.3.5" cache-content-type: "npm:^1.0.0" @@ -6809,7 +6837,7 @@ __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 @@ -6992,9 +7020,9 @@ __metadata: linkType: hard "lodash@npm:^4.0.0": - version: 4.17.23 - resolution: "lodash@npm:4.17.23" - checksum: 10/82504c88250f58da7a5a4289f57a4f759c44946c005dd232821c7688b5fcfbf4a6268f6a6cdde4b792c91edd2f3b5398c1d2a0998274432cff76def48735e233 + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 languageName: node linkType: hard @@ -9130,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: @@ -10641,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: From 3c16f7398ef63428d17bf660b6e301ce235e85b8 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:44:17 +0100 Subject: [PATCH 193/291] SOFIE-261 | add UI for t-timers (WIP) --- .../RundownHeader/RundownHeaderTimers.tsx | 31 ++++++++++++------- .../RundownHeader_old/TimingDisplay.tsx | 1 + 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index 77e46062633..d8c8a771a21 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -32,8 +32,10 @@ interface ISingleTimerProps { timer: RundownTTimer } -function SingleTimer({ timer }: ISingleTimerProps) { +function SingleTimer({ timer }: Readonly) { const now = getCurrentTime() + const mode = timer.mode + if (!mode) return null const isRunning = !!timer.state && !timer.state.paused @@ -49,19 +51,24 @@ function SingleTimer({ timer }: ISingleTimerProps) { {timerSign} - {parts.map((p, i) => ( - + {(() => { + let cursor = 0 + return parts.map((p, i) => { + const key = `${timer.index}-${cursor}-${p}` + cursor += p.length + 1 + return ( + {i < parts.length - 1 && :} - - ))} + + ) + }) + })()} ) } function calculateDiff(timer: RundownTTimer, now: number): number { - if (!timer.state || timer.state.paused === undefined) { + if (!timer.state) { return 0 } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx index 809c544fff4..37d13348f3c 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx @@ -51,6 +51,7 @@ export function TimingDisplay({
+
From d200da61be40978447109b603d22058f98b46f56 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Wed, 4 Feb 2026 04:13:59 +0100 Subject: [PATCH 194/291] SOFIE-261 | change alignment of t-timers in rundown screen --- .../ui/RundownView/RundownHeader/RundownHeaderTimers.tsx | 5 +---- .../ui/RundownView/RundownHeader_old/TimingDisplay.tsx | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index d8c8a771a21..cf741713b7e 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -13,11 +13,8 @@ interface IProps { export const RundownHeaderTimers: React.FC = ({ tTimers }) => { useTiming() - if (!tTimers?.length) { - return null - } - const activeTimers = tTimers.filter((t) => t.mode) + if (activeTimers.length == 0) return null return (
diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx index 37d13348f3c..809c544fff4 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx @@ -51,7 +51,6 @@ export function TimingDisplay({
-
From eb6ee1367cf5fec4102935c8584c22cc7fcf676d Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Wed, 11 Feb 2026 03:23:26 +0100 Subject: [PATCH 195/291] SOFIE-261 | (WIP) add estimates over/under to t-timers UI in director screen --- .../corelib/src/dataModel/RundownPlaylist.ts | 17 ++++ packages/webui/src/client/lib/tTimerUtils.ts | 83 +++++++++++++++++++ .../src/client/styles/countdown/director.scss | 70 ++++++++++++++++ .../client/styles/countdown/presenter.scss | 69 ++++++++++++++- .../webui/src/client/styles/rundownView.scss | 16 ++++ .../DirectorScreen/DirectorScreen.tsx | 9 ++ .../client/ui/ClockView/PresenterScreen.tsx | 6 ++ .../src/client/ui/ClockView/TTimerDisplay.tsx | 55 ++++++++++++ .../RundownHeader/RundownHeaderTimers.tsx | 26 +----- 9 files changed, 326 insertions(+), 25 deletions(-) create mode 100644 packages/webui/src/client/lib/tTimerUtils.ts create mode 100644 packages/webui/src/client/ui/ClockView/TTimerDisplay.tsx diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index b3b3d2c943a..d363480bd47 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -165,6 +165,23 @@ export interface RundownTTimer { */ state: TimerState | null + /** The estimated 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. + * Running means we are progressing towards the anchor (estimate 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. + */ + estimateState?: TimerState + + /** The target Part that this timer is counting towards (the "timing anchor"). + * + * When set, the server calculates estimateState based on when we expect to reach this part. + * If not set, estimateState is not calculated automatically but can still be set manually by a blueprint. + */ + anchorPartId?: PartId + /* * Future ideas: * allowUiControl: boolean diff --git a/packages/webui/src/client/lib/tTimerUtils.ts b/packages/webui/src/client/lib/tTimerUtils.ts new file mode 100644 index 00000000000..08ec4f19e2a --- /dev/null +++ b/packages/webui/src/client/lib/tTimerUtils.ts @@ -0,0 +1,83 @@ +import { RundownTTimer } 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 currentTime = timer.state.paused ? timer.state.duration : timer.state.zeroTime - now + + // Free run counts up, so negate to get positive elapsed time + if (timer.mode?.type === 'freeRun') { + return -currentTime + } + + // Apply stopAtZero if configured + if (timer.mode?.stopAtZero && currentTime < 0) { + return 0 + } + + return currentTime +} + +/** + * Calculate the over/under difference between the timer's current value + * and its estimate. + * + * 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 estimate is available. + */ +export function calculateTTimerOverUnder(timer: RundownTTimer, now: number): number | undefined { + if (!timer.state || !timer.estimateState) { + return undefined + } + + const duration = timer.state.paused ? timer.state.duration : timer.state.zeroTime - now + const estimateDuration = timer.estimateState.paused + ? timer.estimateState.duration + : timer.estimateState.zeroTime - now + + return duration - estimateDuration +} + +// TODO: remove this mock +let mockTimer: RundownTTimer | undefined + +export function getDefaultTTimer(_tTimers: [RundownTTimer, RundownTTimer, RundownTTimer]): RundownTTimer | undefined { + // FORCE MOCK: + /* + const active = tTimers.find((t) => t.mode) + if (active) return active + */ + + if (!mockTimer) { + const now = Date.now() + mockTimer = { + index: 0, + label: 'MOCK TIMER', + mode: { + type: 'countdown', + }, + state: { + zeroTime: now + 60 * 60 * 1000, // 1 hour + duration: 0, + paused: false, + }, + estimateState: { + zeroTime: now + 65 * 60 * 1000, // 65 mins -> 5 mins over + duration: 0, + paused: false, + }, + } as any + } + + return mockTimer +} diff --git a/packages/webui/src/client/styles/countdown/director.scss b/packages/webui/src/client/styles/countdown/director.scss index a053bc45864..0c034624ee1 100644 --- a/packages/webui/src/client/styles/countdown/director.scss +++ b/packages/webui/src/client/styles/countdown/director.scss @@ -480,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/rundownView.scss b/packages/webui/src/client/styles/rundownView.scss index f7c8db52fd8..8ddd23a2c87 100644 --- a/packages/webui/src/client/styles/rundownView.scss +++ b/packages/webui/src/client/styles/rundownView.scss @@ -3644,5 +3644,21 @@ svg.icon { 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/ui/ClockView/DirectorScreen/DirectorScreen.tsx b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx index 6465595d7d6..5b9c2c3f978 100644 --- a/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/DirectorScreen/DirectorScreen.tsx @@ -44,6 +44,8 @@ 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' @@ -564,6 +566,8 @@ function DirectorScreenRender({ } } + const activeTTimer = getDefaultTTimer(playlist.tTimers) + return (
@@ -749,6 +753,11 @@ function DirectorScreenRender({ ) : null}
+ {!!activeTTimer && ( +
+ +
+ )}
) diff --git a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx index 8e9dad2dd08..f0cb0a64e1e 100644 --- a/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx +++ b/packages/webui/src/client/ui/ClockView/PresenterScreen.tsx @@ -53,6 +53,8 @@ import { useSetDocumentClass, useSetDocumentDarkTheme } from '../util/useSetDocu import { useRundownAndShowStyleIdsForPlaylist } from '../util/useRundownAndShowStyleIdsForPlaylist.js' import { RundownPlaylistClientUtil } from '../../lib/rundownPlaylistUtil.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 @@ -488,6 +490,7 @@ function PresenterScreenContentDefaultLayout({ const expectedStart = PlaylistTiming.getExpectedStart(playlist.timing) const overUnderClock = getPlaylistTimingDiff(playlist, timingDurations) ?? 0 + const activeTTimer = getDefaultTTimer(playlist.tTimers) return (
@@ -593,6 +596,9 @@ function PresenterScreenContentDefaultLayout({
{playlist ? playlist.name : 'UNKNOWN'}
+
+ {!!activeTTimer && } +
= 0, 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/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index cf741713b7e..ae6d062f718 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -2,6 +2,7 @@ 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 } from '../../../lib/tTimerUtils' import classNames from 'classnames' import { getCurrentTime } from '../../../lib/systemTime' import { Countdown } from './Countdown' @@ -33,15 +34,13 @@ function SingleTimer({ timer }: Readonly) { const now = getCurrentTime() const mode = timer.mode if (!mode) return null - const isRunning = !!timer.state && !timer.state.paused - const diff = calculateDiff(timer, now) + const diff = calculateTTimerDiff(timer, now) const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(diff), false, true, true, false, true) const parts = timeStr.split(':') const timerSign = diff >= 0 ? '+' : '-' - const isCountingDown = timer.mode?.type === 'countdown' && diff < 0 && isRunning return ( @@ -81,24 +80,3 @@ function SingleTimer({ timer }: Readonly) { ) } - -function calculateDiff(timer: RundownTTimer, now: number): number { - if (!timer.state) { - return 0 - } - - // Get current time: either frozen duration or calculated from zeroTime - const currentTime = timer.state.paused ? timer.state.duration : timer.state.zeroTime - now - - // Free run counts up, so negate to get positive elapsed time - if (timer.mode?.type === 'freeRun') { - return -currentTime - } - - // Apply stopAtZero if configured - if (timer.mode?.stopAtZero && currentTime < 0) { - return 0 - } - - return currentTime -} From df7224ddccf1f336d3917e0d258b75986561dab2 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Tue, 3 Mar 2026 21:29:00 +0100 Subject: [PATCH 196/291] New top bar UI: visual tweaks --- .../RundownView/RundownHeader/Countdown.scss | 23 ++++++- .../RundownHeader/RundownHeader.scss | 69 +++++++++---------- .../RundownHeader/RundownHeader.tsx | 13 ++-- .../RundownHeader/RundownHeaderTimers.tsx | 4 +- .../RundownView/RundownTiming/TimeOfDay.tsx | 6 +- 5 files changed, 71 insertions(+), 44 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index 4a0819ba622..9a11264a470 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -6,12 +6,13 @@ align-items: baseline; justify-content: space-between; gap: 0.6em; - //color: rgba(255, 255, 255, 0.6); transition: color 0.2s; &__label { @extend .rundown-header__hoverable-label; white-space: nowrap; + position: relative; + top: -0.6em; /* Visually push the label up to align with cap height */ } &__value { @@ -20,4 +21,24 @@ font-variant-numeric: tabular-nums; letter-spacing: 0.05em; } + + &--counter { + .countdown__label { + font-size: 0.65em; + } + .countdown__value { + color: #fff; + line-height: 1; + } + } + + &--timeofday { + .countdown__label { + font-size: 0.7em; + } + .countdown__value { + color: $general-fast-color; + font-weight: 300; + } + } } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index f04e5eea118..ec5a46ba941 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -105,22 +105,49 @@ 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.65em; - white-space: nowrap; - text-overflow: ellipsis; - max-width: 20em; + display: flex; + flex-direction: row; + justify-content: center; + gap: 0.4em; + max-width: 40em; color: #fff; max-height: 0; overflow: hidden; transition: max-height 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-weight: 700; + } + } + + .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; - margin-right: 0.5em; - margin-left: 2em; .rundown-header__clocks-diff { display: flex; @@ -187,28 +214,16 @@ } .rundown-header__clocks-timers { - position: absolute; - left: 28%; /* Position exactly between the 15% left edge content and the 50% center clock */ - top: 0; - bottom: 0; display: flex; flex-direction: column; justify-content: center; /* Center vertically against the entire header height */ align-items: flex-end; + margin-right: 3em; .rundown-header__clocks-timers__timer { white-space: nowrap; line-height: 1.25; - .countdown__label { - @extend .rundown-header__hoverable-label; - font-size: 0.65em; - } - - .countdown__value { - color: #fff; - } - .rundown-header__clocks-timers__timer__sign { display: inline-block; width: 0.6em; @@ -313,23 +328,7 @@ } .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__value { - margin-left: auto; - font-size: 1.4em; - font-variant-numeric: tabular-nums; - letter-spacing: 0.05em; - } + @extend .countdown; } .rundown-header__timers-onair-remaining__label { diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index 61b83bb20b0..51726e24915 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -37,7 +37,7 @@ interface IRundownHeaderProps { layout: RundownLayoutRundownHeader | undefined } -export function RundownHeader({ playlist, studio, firstRundown }: IRundownHeaderProps): JSX.Element { +export function RundownHeader({ playlist, studio, firstRundown, currentRundown }: IRundownHeaderProps): JSX.Element { const { t } = useTranslation() const [simplified, setSimplified] = useState(false) @@ -82,10 +82,15 @@ export function RundownHeader({ playlist, studio, firstRundown }: IRundownHeader
-
- - {playlist.name} +
+ + +
+
+ {(currentRundown ?? firstRundown)?.name} + {playlist.name} +
diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index ae6d062f718..d631f7592af 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -41,12 +41,12 @@ function SingleTimer({ timer }: Readonly) { const parts = timeStr.split(':') const timerSign = diff >= 0 ? '+' : '-' - const isCountingDown = timer.mode?.type === 'countdown' && diff < 0 && isRunning + const isCountingDown = mode.type === 'countdown' && diff < 0 && isRunning return ( ): JSX.Element { const timingDurations = useTiming() return ( - + + + ) } From 50728710490358d1d66caf0e977b631f99eebd8c Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Tue, 3 Mar 2026 21:58:28 +0100 Subject: [PATCH 197/291] New top bar UI: fix circular scss dependencies --- .../RundownView/RundownHeader/Countdown.scss | 4 ++-- .../RundownHeader/RundownHeader.scss | 22 +++-------------- .../ui/RundownView/RundownHeader/_shared.scss | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+), 21 deletions(-) create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/_shared.scss diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index 9a11264a470..b51a2014641 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -1,5 +1,5 @@ @import '../../../styles/colorScheme'; -@import './RundownHeader.scss'; +@import './shared'; .countdown { display: flex; @@ -9,7 +9,7 @@ transition: color 0.2s; &__label { - @extend .rundown-header__hoverable-label; + @extend %hoverable-label; white-space: nowrap; position: relative; top: -0.6em; /* Visually push the label up to align with cap height */ diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index ec5a46ba941..9e133e0a64c 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -1,4 +1,6 @@ @import '../../../styles/colorScheme'; +@import './shared'; +@import './Countdown'; .rundown-header { height: 64px; @@ -272,25 +274,7 @@ // Common label style for header labels that react to hover .rundown-header__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: 0.6; - transition: opacity 0.2s; + @extend %hoverable-label; } .rundown-header__timers-segment-remaining, 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..993397b9b42 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/_shared.scss @@ -0,0 +1,24 @@ +// 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: 0.6; + transition: opacity 0.2s; +} From f2f55b90119d478fe6b10a83589dd2b2a3771f1f Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Tue, 3 Mar 2026 16:38:10 +0100 Subject: [PATCH 198/291] chore: Added single-pixel line at the bottom of the Top Bar. --- .../client/ui/RundownView/RundownHeader/RundownHeader.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 9e133e0a64c..a87bc7c5217 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -30,6 +30,7 @@ &.active { background: $color-header-on-air; + border-bottom: 1px solid #256b91; .rundown-header__timers-segment-remaining, .rundown-header__timers-onair-remaining, @@ -115,6 +116,7 @@ } .rundown-header__clocks-playlist-name { + //@extend .rundown-header__hoverable-label; font-size: 0.65em; display: flex; flex-direction: row; @@ -377,6 +379,8 @@ .rundown-header__clocks-clock-group { .rundown-header__clocks-playlist-name { + @extend .rundown-header__hoverable-label; + font-size: 0.65em; max-height: 1.5em; } } From 0f5fb9e99303319f9359251085fb6f7e5bbe9475 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Tue, 3 Mar 2026 17:03:06 +0100 Subject: [PATCH 199/291] chore: Created two distinct styles for two types of counters. --- .../RundownView/RundownHeader/Countdown.scss | 5 +- .../RundownView/RundownHeader/Countdown.tsx | 4 +- .../RundownHeader/RundownHeader.scss | 64 ++++++++++++++++++- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index b51a2014641..8fdcfbd41f6 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -15,9 +15,10 @@ top: -0.6em; /* Visually push the label up to align with cap height */ } - &__value { + &__counter, + &__timeofday { margin-left: auto; - font-size: 1.4em; + font-size: 1.3em; font-variant-numeric: tabular-nums; letter-spacing: 0.05em; } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx index 7e79aaab315..c51a8b1bfdf 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx @@ -11,10 +11,12 @@ interface IProps { } export function Countdown({ label, time, className, children }: IProps): JSX.Element { + const valueClassName = time !== undefined ? 'countdown__timeofday' : 'countdown__counter' + return ( {label && {label}} - + {time !== undefined ? : children} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index a87bc7c5217..78ce5f07973 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -79,8 +79,7 @@ .timing-clock { color: #40b8fa; font-size: 1.4em; - - letter-spacing: 0 em; + letter-spacing: 0em; transition: color 0.2s; &.time-now { @@ -228,6 +227,15 @@ white-space: nowrap; line-height: 1.25; + .countdown__label { + @extend .rundown-header__hoverable-label; + font-size: 0.65em; + } + + .countdown__counter, + .countdown__timeofday { + color: #fff; + } .rundown-header__clocks-timers__timer__sign { display: inline-block; width: 0.6em; @@ -314,7 +322,57 @@ } .rundown-header__show-timers-countdown { - @extend .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' 50, + 'wght' 550, + 'slnt' 0, + 'GRAD' 0, + 'opsz' 33, + '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.05em; + font-variation-settings: + 'wdth' 70, + '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__timers-onair-remaining__label { From aa27dd42892f7bf03432100de46f0649f1495878 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Tue, 3 Mar 2026 19:58:21 +0100 Subject: [PATCH 200/291] chore: Small tweaks to the typographic styles of Top Bar counters. --- .../RundownView/RundownHeader/RundownHeader.scss | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 78ce5f07973..9d6f8457823 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -162,6 +162,7 @@ .rundown-header__clocks-diff__label { @extend .rundown-header__hoverable-label; font-size: 0.7em; + opacity: 0.6; font-variation-settings: 'wdth' 25, 'wght' 500, @@ -176,13 +177,13 @@ 'YTLC' 548, 'YTDE' -203, 'YTUC' 712; - opacity: 0.6; } .rundown-header__clocks-diff__chip { - font-size: 1.2em; + font-size: 1.4em; padding: 0em 0.3em; border-radius: 999px; + letter-spacing: -0.02em; font-variation-settings: 'wdth' 25, 'wght' 600, @@ -197,12 +198,11 @@ 'YTLC' 548, 'YTDE' -203, 'YTUC' 712; - letter-spacing: -0.02em; } &.rundown-header__clocks-diff--under { .rundown-header__clocks-diff__chip { - background-color: #ff0; //$general-fast-color; + background-color: #ff0; // Should probably be changed to $general-fast-color; color: #000; } } @@ -338,11 +338,11 @@ font-size: 1.3em; letter-spacing: 0em; font-variation-settings: - 'wdth' 50, + 'wdth' 60, 'wght' 550, 'slnt' 0, 'GRAD' 0, - 'opsz' 33, + 'opsz' 40, 'XOPQ' 96, 'XTRA' 468, 'YOPQ' 79, @@ -357,13 +357,13 @@ margin-left: auto; font-size: 1.3em; font-variant-numeric: tabular-nums; - letter-spacing: 0.05em; + letter-spacing: 0.02em; font-variation-settings: 'wdth' 70, 'wght' 400, 'slnt' -5, 'GRAD' 0, - 'opsz' 44, + 'opsz' 40, 'XOPQ' 96, 'XTRA' 468, 'YOPQ' 79, From b04456c3f1d7a82354f2f8ad0f1049f59404955f Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:15:10 +0100 Subject: [PATCH 201/291] Top bar UI: fix clocks alignment --- .../RundownHeader/RundownHeader.scss | 88 +++++++++++-------- .../RundownHeader/RundownHeader.tsx | 2 +- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 9d6f8457823..7289ec63e1e 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -75,7 +75,6 @@ align-items: center; justify-content: center; flex: 1; - .timing-clock { color: #40b8fa; font-size: 1.4em; @@ -115,7 +114,6 @@ } .rundown-header__clocks-playlist-name { - //@extend .rundown-header__hoverable-label; font-size: 0.65em; display: flex; flex-direction: row; @@ -215,46 +213,66 @@ } } } + } - .rundown-header__clocks-timers { - display: flex; - flex-direction: column; - justify-content: center; /* Center vertically against the entire header height */ - align-items: flex-end; - margin-right: 3em; + .rundown-header__clocks-timers { + margin-left: auto; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-end; - .rundown-header__clocks-timers__timer { - white-space: nowrap; - line-height: 1.25; + .rundown-header__clocks-timers__timer { + white-space: nowrap; + line-height: 1.25; - .countdown__label { - @extend .rundown-header__hoverable-label; - font-size: 0.65em; - } + .countdown__label { + @extend .rundown-header__hoverable-label; + font-size: 0.65em; + } - .countdown__counter, - .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: 0.3em; + .countdown__counter { + color: #fff; + } + + .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: 0.3em; + } + + .rundown-header__clocks-timers__timer__part { + color: #fff; + &.rundown-header__clocks-timers__timer__part--dimmed { + color: #888; + font-weight: 400; } + } + .rundown-header__clocks-timers__timer__separator { + margin: 0 0.05em; + color: #888; + } - .rundown-header__clocks-timers__timer__part { - color: #fff; - &.rundown-header__clocks-timers__timer__part--dimmed { - color: #888; - font-weight: 400; - } + .rundown-header__clocks-timers__timer__over-under { + font-size: 0.75em; + font-weight: 400; + font-variant-numeric: tabular-nums; + margin-left: 0.5em; + white-space: nowrap; + + &.rundown-header__clocks-timers__timer__over-under--over { + color: $general-late-color; } - .rundown-header__clocks-timers__timer__separator { - margin: 0 0.05em; - color: #888; + + &.rundown-header__clocks-timers__timer__over-under--under { + color: #0f0; } } } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index 51726e24915..ba0b622f6c1 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -78,10 +78,10 @@ export function RundownHeader({ playlist, studio, firstRundown, currentRundown }
)} +
-
From b373609515449ec44138260fbfa029ea9ce11ec6 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Wed, 4 Mar 2026 09:31:03 +0100 Subject: [PATCH 202/291] chore: Created the two separate font stylings for the Over/Under pill, but they are not yet called correctly in code. --- .../RundownHeader/RundownHeader.scss | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 7289ec63e1e..ce45c68f2be 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -177,17 +177,39 @@ 'YTUC' 712; } - .rundown-header__clocks-diff__chip { - font-size: 1.4em; + .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' 600, + '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' 20, + 'opsz' 25, 'XOPQ' 96, 'XTRA' 468, 'YOPQ' 79, @@ -208,7 +230,7 @@ &.rundown-header__clocks-diff--over { .rundown-header__clocks-diff__chip { background-color: $general-late-color; - color: #fff; + color: #000000; } } } From 97398f578f73d9fa532e66266e3fdd5eb6aaad4d Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:06:33 +0100 Subject: [PATCH 203/291] New top bar ui: visual changes - fix hover transition on text in playlist name, time of day color and time of day timers styles --- .../ui/RundownView/RundownHeader/Countdown.scss | 12 ++++++++++-- .../ui/RundownView/RundownHeader/RundownHeader.scss | 11 +++++++++-- .../ui/RundownView/RundownHeader/RundownHeader.tsx | 10 ++++++++-- .../RundownHeader/RundownHeaderTimers.tsx | 4 +++- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index 8fdcfbd41f6..5d43bc9a5fc 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -15,12 +15,20 @@ top: -0.6em; /* Visually push the label up to align with cap height */ } - &__counter, + &__counter { + margin-left: auto; + font-size: 1.3em; + font-variant-numeric: tabular-nums; + letter-spacing: 0.05em; + } + &__timeofday { margin-left: auto; font-size: 1.3em; font-variant-numeric: tabular-nums; letter-spacing: 0.05em; + font-style: italic; + font-weight: 300; } &--counter { @@ -38,7 +46,7 @@ font-size: 0.7em; } .countdown__value { - color: $general-fast-color; + color: #40b8fa; font-weight: 300; } } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index ce45c68f2be..067881b1c3e 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -248,6 +248,15 @@ white-space: nowrap; line-height: 1.25; + &.countdown--timeofday { + .rundown-header__clocks-timers__timer__part, + .rundown-header__clocks-timers__timer__sign, + .rundown-header__clocks-timers__timer__separator { + font-style: italic; + font-weight: 300; + } + } + .countdown__label { @extend .rundown-header__hoverable-label; font-size: 0.65em; @@ -477,8 +486,6 @@ .rundown-header__clocks-clock-group { .rundown-header__clocks-playlist-name { - @extend .rundown-header__hoverable-label; - font-size: 0.65em; max-height: 1.5em; } } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index ba0b622f6c1..8d7d79d6437 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -37,7 +37,13 @@ interface IRundownHeaderProps { layout: RundownLayoutRundownHeader | undefined } -export function RundownHeader({ playlist, studio, firstRundown, currentRundown }: IRundownHeaderProps): JSX.Element { +export function RundownHeader({ + playlist, + studio, + firstRundown, + currentRundown, + rundownCount, +}: IRundownHeaderProps): JSX.Element { const { t } = useTranslation() const [simplified, setSimplified] = useState(false) @@ -89,7 +95,7 @@ export function RundownHeader({ playlist, studio, firstRundown, currentRundown }
{(currentRundown ?? firstRundown)?.name} - {playlist.name} + {rundownCount > 1 && {playlist.name}}
diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index d631f7592af..d673aaf3d11 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -46,7 +46,9 @@ function SingleTimer({ timer }: Readonly) { return ( Date: Wed, 4 Mar 2026 18:19:34 +0100 Subject: [PATCH 204/291] New top bar UI: unify styles, fix visual issues --- .../ui/RundownView/RundownHeader/Countdown.scss | 15 +++++++-------- .../ui/RundownView/RundownHeader/Countdown.tsx | 2 +- .../RundownHeader/RundownHeader.scss | 17 +++++++++++------ .../RundownView/RundownHeader/RundownHeader.tsx | 5 ++++- .../RundownHeaderTimingDisplay.tsx | 2 +- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index 5d43bc9a5fc..dc57ff2f7c7 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -13,41 +13,40 @@ white-space: nowrap; position: relative; top: -0.6em; /* Visually push the label up to align with cap height */ + margin-left: auto; } &__counter { - margin-left: auto; font-size: 1.3em; font-variant-numeric: tabular-nums; letter-spacing: 0.05em; + color: #fff; + line-height: 1; } &__timeofday { - margin-left: auto; font-size: 1.3em; font-variant-numeric: tabular-nums; letter-spacing: 0.05em; font-style: italic; font-weight: 300; + color: #fff; + line-height: 1; } + /* Modifier classes — only used for label font-size overrides */ &--counter { .countdown__label { font-size: 0.65em; } - .countdown__value { - color: #fff; - line-height: 1; - } } &--timeofday { .countdown__label { font-size: 0.7em; } - .countdown__value { + .countdown__timeofday { color: #40b8fa; - font-weight: 300; } } } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx index c51a8b1bfdf..419f323bb6a 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx @@ -15,7 +15,7 @@ export function Countdown({ label, time, className, children }: IProps): JSX.Ele return ( - {label && {label}} + {label} {time !== undefined ? : children} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 067881b1c3e..021ba7833da 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -221,14 +221,14 @@ } &.rundown-header__clocks-diff--under { - .rundown-header__clocks-diff__chip { + .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 { + .rundown-header__clocks-diff__chip--over { background-color: $general-late-color; color: #000000; } @@ -359,15 +359,19 @@ display: flex; flex-direction: column; justify-content: flex-start; - gap: 0.15em; + gap: 0.1em; min-width: 7em; } .rundown-header__show-timers { display: flex; - align-items: center; + align-items: flex-start; gap: 1em; - cursor: pointer; + cursor: zoom-out; + + &.rundown-header__show-timers--simplified { + cursor: zoom-in; + } } .rundown-header__show-timers-countdown { @@ -472,7 +476,8 @@ color: #40b8fa; } - .rundown-header__hoverable-label { + .rundown-header__hoverable-label, + .countdown__label { opacity: 1; } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index 8d7d79d6437..0f67e5980b8 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -101,7 +101,10 @@ export function RundownHeader({
-
setSimplified((s) => !s)}> +
setSimplified((s) => !s)} + > diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx index fceea32777f..050c85af884 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimingDisplay.tsx @@ -22,7 +22,7 @@ export function RundownHeaderTimingDisplay({ playlist }: IRundownHeaderTimingDis }`} > {isUnder ? 'Under' : 'Over'} - + {isUnder ? '−' : '+'} {timeStr} From d26388af8440d96fcfb2e09f1640dfecfaf34aaf Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 5 Mar 2026 09:16:38 +0100 Subject: [PATCH 205/291] chore: Playlist and Rundown font styling. --- .../RundownHeader/RundownHeader.scss | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 021ba7833da..aa310295dcd 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -114,7 +114,21 @@ } .rundown-header__clocks-playlist-name { - font-size: 0.65em; + 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; @@ -134,7 +148,20 @@ min-width: 0; } .playlist-name { - font-weight: 700; + 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; } } From 044002b488a97412bce2d2145505f0147cb73107 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:47:40 +0100 Subject: [PATCH 206/291] New top bar UI: fix countdown classes --- .../RundownView/RundownHeader/Countdown.scss | 42 ++++++++++++++++--- .../RundownHeader/RundownHeaderTimers.tsx | 2 +- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index dc57ff2f7c7..dee8721f31c 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -17,21 +17,53 @@ } &__counter { - font-size: 1.3em; font-variant-numeric: tabular-nums; - letter-spacing: 0.05em; color: #fff; line-height: 1; + + 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; } &__timeofday { - font-size: 1.3em; - font-variant-numeric: tabular-nums; - letter-spacing: 0.05em; font-style: italic; font-weight: 300; color: #fff; line-height: 1; + + 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; } /* Modifier classes — only used for label font-size overrides */ diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index d673aaf3d11..62e09e84ef4 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -14,7 +14,7 @@ interface IProps { export const RundownHeaderTimers: React.FC = ({ tTimers }) => { useTiming() - const activeTimers = tTimers.filter((t) => t.mode) + const activeTimers = tTimers.filter((t) => t.mode).slice(0, 2) if (activeTimers.length == 0) return null return ( From 7622896378b02b592ac723a2453f367cf00c0fe4 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:21:03 +0100 Subject: [PATCH 207/291] Top bar UI: unify over/under in t-timers --- .../RundownHeader/RundownHeader.scss | 233 +++++++++--------- .../RundownHeader/RundownHeaderTimers.tsx | 14 +- 2 files changed, 135 insertions(+), 112 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index aa310295dcd..c27e5c86d5b 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -116,19 +116,19 @@ .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; + '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; @@ -149,19 +149,19 @@ } .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; + '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; } } @@ -176,90 +176,90 @@ 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 { + 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.7em; - opacity: 0.6; - 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__label { + @extend .rundown-header__hoverable-label; + font-size: 0.7em; + opacity: 0.6; + 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__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--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-diff--over { + .rundown-header__clocks-diff__chip--over { + background-color: $general-late-color; + color: #000000; } } } @@ -319,18 +319,29 @@ } .rundown-header__clocks-timers__timer__over-under { - font-size: 0.75em; - font-weight: 400; - font-variant-numeric: tabular-nums; - margin-left: 0.5em; + display: inline-block; + vertical-align: middle; + font-size: 0.65em; + padding: 0.05em 0.35em; + border-radius: 999px; white-space: nowrap; + letter-spacing: -0.02em; + margin-left: 0.5em; + font-variant-numeric: tabular-nums; + font-variation-settings: + 'wdth' 25, + 'wght' 500, + 'slnt' 0, + 'opsz' 14; &.rundown-header__clocks-timers__timer__over-under--over { - color: $general-late-color; + background-color: $general-late-color; + color: #000; } &.rundown-header__clocks-timers__timer__over-under--under { - color: #0f0; + background-color: #ff0; + color: #000; } } } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index 62e09e84ef4..fe0db858347 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -2,7 +2,7 @@ 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 } from '../../../lib/tTimerUtils' +import { calculateTTimerDiff, calculateTTimerOverUnder } from '../../../lib/tTimerUtils' import classNames from 'classnames' import { getCurrentTime } from '../../../lib/systemTime' import { Countdown } from './Countdown' @@ -37,6 +37,7 @@ function SingleTimer({ timer }: Readonly) { 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 parts = timeStr.split(':') @@ -79,6 +80,17 @@ function SingleTimer({ timer }: Readonly) { ) }) })()} + {!!overUnder && ( + 0, + 'rundown-header__clocks-timers__timer__over-under--under': overUnder < 0, + })} + > + {overUnder > 0 ? '+' : '-'} + {RundownUtils.formatDiffToTimecode(Math.abs(overUnder), false, true, true, false, true)} + + )} ) } From df6f5c155a0c8977e26ebe9eb53fd45ec7b1ab0e Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 5 Mar 2026 10:44:55 +0100 Subject: [PATCH 208/291] chore: Counter and TimeOf Day styling. --- .../client/ui/RundownView/RundownHeader/Countdown.scss | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index dee8721f31c..e29e7d4ccb0 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -17,10 +17,8 @@ } &__counter { - font-variant-numeric: tabular-nums; - color: #fff; + color: #ffffff; line-height: 1; - margin-left: auto; font-size: 1.3em; letter-spacing: 0em; @@ -41,14 +39,10 @@ } &__timeofday { - font-style: italic; - font-weight: 300; color: #fff; line-height: 1; - margin-left: auto; font-size: 1.3em; - font-variant-numeric: tabular-nums; letter-spacing: 0.02em; font-variation-settings: 'wdth' 70, From 4d3fce9256b24ac1571f42f49ea6630d008a7b19 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:08:59 +0100 Subject: [PATCH 209/291] Top bar UI: change layout of t-timers to grid --- .../RundownHeader/RundownHeader.scss | 23 +++++++++++++++---- .../RundownHeader/RundownHeaderTimers.tsx | 4 +++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index c27e5c86d5b..6c681fd5959 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -266,12 +266,19 @@ .rundown-header__clocks-timers { margin-left: auto; - display: flex; - flex-direction: column; - justify-content: center; - align-items: flex-end; + display: grid; + grid-template-columns: auto auto; + align-items: baseline; + justify-content: end; + column-gap: 0.6em; + row-gap: 0.15em; + + .rundown-header__clocks-timers__row { + display: contents; + } .rundown-header__clocks-timers__timer { + display: contents; white-space: nowrap; line-height: 1.25; @@ -287,10 +294,18 @@ .countdown__label { @extend .rundown-header__hoverable-label; font-size: 0.65em; + margin-left: 0; + top: 0; + text-align: right; + white-space: nowrap; } .countdown__counter { color: #fff; + margin-left: 0; + display: flex; + align-items: baseline; + gap: 0; } .countdown__timeofday { diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index fe0db858347..06ab3c80661 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -20,7 +20,9 @@ export const RundownHeaderTimers: React.FC = ({ tTimers }) => { return (
{activeTimers.map((timer) => ( - +
+ +
))}
) From c8183edffff77891931062b1003fa06524e3a896 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 5 Mar 2026 12:33:17 +0100 Subject: [PATCH 210/291] Top bar UI: add dimming to inactive timer parts --- .../RundownView/RundownHeader/Countdown.scss | 14 +++++ .../RundownView/RundownHeader/Countdown.tsx | 51 ++++++++++++++++--- .../CurrentPartOrSegmentRemaining.tsx | 1 + .../RundownHeader/RundownHeader.scss | 8 +-- .../RundownHeader/RundownHeaderDurations.tsx | 29 +++++------ .../RundownHeader/RundownHeaderTimers.tsx | 26 +++++++--- 6 files changed, 95 insertions(+), 34 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index e29e7d4ccb0..2b893e99e41 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -75,4 +75,18 @@ color: #40b8fa; } } + + &__digit { + &--dimmed { + opacity: 0.4; + } + } + + &__sep { + margin: 0 0.05em; + + &--dimmed { + opacity: 0.4; + } + } } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx index 419f323bb6a..71218a06deb 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx @@ -3,22 +3,61 @@ import Moment from 'react-moment' import classNames from 'classnames' import './Countdown.scss' +const THRESHOLDS = [3600000, 60000, 1] // hours, minutes, seconds + interface IProps { label?: string time?: number className?: string children?: React.ReactNode + ms?: number +} + +function DimmedValue({ value, ms }: { readonly value: string; readonly ms: number }): JSX.Element { + const parts = value.split(':') + const absDiff = Math.abs(ms) + + return ( + <> + {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 (ms !== undefined && typeof children === 'string') { + return + } + return children } -export function Countdown({ label, time, className, children }: IProps): JSX.Element { - const valueClassName = time !== undefined ? 'countdown__timeofday' : 'countdown__counter' +export function Countdown({ label, time, className, children, ms }: IProps): JSX.Element { + const valueClassName = time === undefined ? 'countdown__counter' : 'countdown__timeofday' return ( - {label} - - {time !== undefined ? : children} - + {label && {label}} + {renderContent(time, ms, children)} ) } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx index 1f85a03ec42..585c19441d7 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx @@ -166,6 +166,7 @@ export const RundownHeaderPartRemaining: React.FC = (props) 0 ? props.heavyClassName : undefined)} + ms={displayTimecode || 0} > {RundownUtils.formatDiffToTimecode(displayTimecode || 0, true, false, true, false, true, '', false, true)} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 6c681fd5959..fda0fb49eff 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -324,13 +324,15 @@ .rundown-header__clocks-timers__timer__part { color: #fff; &.rundown-header__clocks-timers__timer__part--dimmed { - color: #888; - font-weight: 400; + opacity: 0.4; } } .rundown-header__clocks-timers__timer__separator { margin: 0 0.05em; - color: #888; + color: #fff; + &.rundown-header__clocks-timers__timer__separator--dimmed { + opacity: 0.4; + } } .rundown-header__clocks-timers__timer__over-under { diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx index 8c800c5f945..2018192cd90 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx @@ -10,15 +10,13 @@ export function RundownHeaderDurations({ playlist, simplified, }: { - playlist: DBRundownPlaylist - simplified?: boolean + readonly playlist: DBRundownPlaylist + readonly simplified?: boolean }): JSX.Element | null { const { t } = useTranslation() const timingDurations = useTiming() const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) - const planned = - expectedDuration != null ? RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, true, true) : null const now = timingDurations.currentTime ?? Date.now() const currentPartInstanceId = playlist.currentPartInfo?.partInstanceId @@ -32,28 +30,25 @@ export function RundownHeaderDurations({ ) if (remaining != null) { const elapsed = - playlist.startedPlayback != null - ? now - playlist.startedPlayback - : (timingDurations.asDisplayedPlaylistDuration ?? 0) + playlist.startedPlayback == null + ? (timingDurations.asDisplayedPlaylistDuration ?? 0) + : now - playlist.startedPlayback estDuration = elapsed + remaining } } - const estimated = - estDuration != null ? RundownUtils.formatDiffToTimecode(estDuration, false, true, true, true, true) : null - - if (!planned && !estimated) return null + if (expectedDuration == null && estDuration == null) return null return (
- {planned ? ( - - {planned} + {expectedDuration != null ? ( + + {RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, true, true)} ) : null} - {!simplified && estimated ? ( - - {estimated} + {!simplified && estDuration != null ? ( + + {RundownUtils.formatDiffToTimecode(estDuration, false, true, true, true, true)} ) : null}
diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index 06ab3c80661..45340b13ff5 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -68,16 +68,26 @@ function SingleTimer({ timer }: Readonly) { return parts.map((p, i) => { const key = `${timer.index}-${cursor}-${p}` cursor += p.length + 1 + const isDimmed = mode.type !== 'timeOfDay' && Math.abs(diff) < [3600000, 60000, 1][i] + return ( - - {p} - - {i < parts.length - 1 && :} + + {p} + + {i < parts.length - 1 && ( + + : + + )} ) }) From 44ee15de19477f3d6087d66f51fd2b329bdb3368 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 5 Mar 2026 14:25:09 +0100 Subject: [PATCH 211/291] chore: Tweaks to styling of T-timers. --- .../ui/RundownView/RundownHeader/RundownHeader.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index fda0fb49eff..a86cdbd0eaf 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -318,7 +318,7 @@ text-align: center; font-size: 1.1em; color: #fff; - margin-right: 0.3em; + margin-right: 0.0em; } .rundown-header__clocks-timers__timer__part { @@ -328,7 +328,7 @@ } } .rundown-header__clocks-timers__timer__separator { - margin: 0 0.05em; + margin: 0 0em; color: #fff; &.rundown-header__clocks-timers__timer__separator--dimmed { opacity: 0.4; @@ -422,10 +422,10 @@ display: flex; align-items: flex-start; gap: 1em; - cursor: zoom-out; + cursor: pointer; &.rundown-header__show-timers--simplified { - cursor: zoom-in; + cursor: pointer; } } From d6467739ad4cc316ecbed638cfb9b4c0ecdf9db2 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:37:10 +0100 Subject: [PATCH 212/291] Top bar UI: css tweaks --- .../RundownView/RundownHeader/Countdown.scss | 6 +- .../RundownView/RundownHeader/Countdown.tsx | 14 +++-- .../RundownHeader/RundownHeader.scss | 25 +++----- .../RundownHeader/RundownHeaderTimers.tsx | 59 +++++-------------- 4 files changed, 36 insertions(+), 68 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index 2b893e99e41..ef1311d7aed 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -5,15 +5,17 @@ display: flex; align-items: baseline; justify-content: space-between; - gap: 0.6em; + gap: 0.3em; transition: color 0.2s; &__label { @extend %hoverable-label; white-space: nowrap; position: relative; - top: -0.6em; /* Visually push the label up to align with cap height */ + top: -0.4em; /* Visually push the label up to align with cap height */ margin-left: auto; + text-align: right; + width: 100%; } &__counter { diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx index 71218a06deb..85994d3f884 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.tsx @@ -11,11 +11,12 @@ interface IProps { className?: string children?: React.ReactNode ms?: number + postfix?: React.ReactNode } -function DimmedValue({ value, ms }: { readonly value: string; readonly ms: number }): JSX.Element { +function DimmedValue({ value, ms }: { readonly value: string; readonly ms?: number }): JSX.Element { const parts = value.split(':') - const absDiff = Math.abs(ms) + const absDiff = ms !== undefined ? Math.abs(ms) : Infinity return ( <> @@ -45,19 +46,22 @@ function renderContent(time: number | undefined, ms: number | undefined, childre if (time !== undefined) { return } - if (ms !== undefined && typeof children === 'string') { + if (typeof children === 'string') { return } return children } -export function Countdown({ label, time, className, children, ms }: IProps): JSX.Element { +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)} + + {renderContent(time, ms, children)} + {postfix} + ) } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index a86cdbd0eaf..15cfaa607af 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -271,7 +271,7 @@ align-items: baseline; justify-content: end; column-gap: 0.6em; - row-gap: 0.15em; + row-gap: 0.1em; .rundown-header__clocks-timers__row { display: contents; @@ -283,19 +283,16 @@ line-height: 1.25; &.countdown--timeofday { - .rundown-header__clocks-timers__timer__part, - .rundown-header__clocks-timers__timer__sign, - .rundown-header__clocks-timers__timer__separator { + .countdown__digit, + .countdown__sep { font-style: italic; font-weight: 300; + color: #40b8fa; } } - .countdown__label { @extend .rundown-header__hoverable-label; - font-size: 0.65em; margin-left: 0; - top: 0; text-align: right; white-space: nowrap; } @@ -318,21 +315,15 @@ text-align: center; font-size: 1.1em; color: #fff; - margin-right: 0.0em; + margin-right: 0em; } - .rundown-header__clocks-timers__timer__part { + .countdown__digit { color: #fff; - &.rundown-header__clocks-timers__timer__part--dimmed { - opacity: 0.4; - } } - .rundown-header__clocks-timers__timer__separator { + .countdown__sep { margin: 0 0em; color: #fff; - &.rundown-header__clocks-timers__timer__separator--dimmed { - opacity: 0.4; - } } .rundown-header__clocks-timers__timer__over-under { @@ -396,7 +387,7 @@ display: flex; align-items: center; justify-content: space-between; - gap: 0.8em; + gap: 0.3em; color: rgba(255, 255, 255, 0.6); transition: color 0.2s; diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index 45340b13ff5..bd146e1b37f 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -41,9 +41,6 @@ function SingleTimer({ timer }: Readonly) { const diff = calculateTTimerDiff(timer, now) const overUnder = calculateTTimerOverUnder(timer, now) const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(diff), false, true, true, false, true) - const parts = timeStr.split(':') - - const timerSign = diff >= 0 ? '+' : '-' const isCountingDown = mode.type === 'countdown' && diff < 0 && isRunning return ( @@ -61,48 +58,22 @@ function SingleTimer({ timer }: Readonly) { 'rundown-header__clocks-timers__timer__isComplete': mode.type === 'countdown' && timer.state !== null && diff <= 0, })} + ms={mode.type === 'timeOfDay' ? undefined : diff} + postfix={ + overUnder ? ( + 0, + 'rundown-header__clocks-timers__timer__over-under--under': overUnder < 0, + })} + > + {overUnder > 0 ? '+' : '−'} + {RundownUtils.formatDiffToTimecode(Math.abs(overUnder), false, true, true, false, true)} + + ) : undefined + } > - {timerSign} - {(() => { - let cursor = 0 - return parts.map((p, i) => { - const key = `${timer.index}-${cursor}-${p}` - cursor += p.length + 1 - const isDimmed = mode.type !== 'timeOfDay' && Math.abs(diff) < [3600000, 60000, 1][i] - - return ( - - - {p} - - {i < parts.length - 1 && ( - - : - - )} - - ) - }) - })()} - {!!overUnder && ( - 0, - 'rundown-header__clocks-timers__timer__over-under--under': overUnder < 0, - })} - > - {overUnder > 0 ? '+' : '-'} - {RundownUtils.formatDiffToTimecode(Math.abs(overUnder), false, true, true, false, true)} - - )} + {timeStr} ) } From 0e674ca5b637a1651e114415aed18c0ee3fc2817 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:41:53 +0100 Subject: [PATCH 213/291] Top bar UI: css tweaks --- .../client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index bd146e1b37f..ba36951c87b 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -68,7 +68,7 @@ function SingleTimer({ timer }: Readonly) { })} > {overUnder > 0 ? '+' : '−'} - {RundownUtils.formatDiffToTimecode(Math.abs(overUnder), false, true, true, false, true)} + {RundownUtils.formatDiffToTimecode(Math.abs(overUnder), false, false, true, false, true)}
) : undefined } From 1b9f0e19c7095c50f6ae5a019560996fc6545857 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:54:21 +0100 Subject: [PATCH 214/291] Top bar UI: css tweaks --- .../client/ui/RundownView/RundownHeader/RundownHeader.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 15cfaa607af..97f3e375a08 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -388,11 +388,14 @@ align-items: center; justify-content: space-between; gap: 0.3em; - color: rgba(255, 255, 255, 0.6); + color: #fff; transition: color 0.2s; &__label { @extend .rundown-header__hoverable-label; + opacity: 1; + position: relative; + top: -0.4em; /* Match alignment from Countdown.scss */ } .overtime { @@ -475,7 +478,7 @@ } .rundown-header__timers-onair-remaining__label { - background-color: $general-live-color; + background-color: var(--general-live-color); color: #ffffff; padding: 0.03em 0.45em 0.02em 0.2em; border-radius: 2px 999px 999px 2px; From 8f33ced090e2586fa4933e4539b36988e4cfe198 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 5 Mar 2026 16:04:24 +0100 Subject: [PATCH 215/291] chore: Tweaked vertical label placement. --- .../src/client/ui/RundownView/RundownHeader/Countdown.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index ef1311d7aed..3a691c3e25b 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -12,7 +12,7 @@ @extend %hoverable-label; white-space: nowrap; position: relative; - top: -0.4em; /* Visually push the label up to align with cap height */ + top: -0.55em; /* Visually push the label up to align with cap height */ margin-left: auto; text-align: right; width: 100%; From ae61fc192db64ba3791838ab0bcc9a3b5530e0f6 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:41:48 +0100 Subject: [PATCH 216/291] New Top Bar UI: align onair styles with timeline, hide segment budget when it's not used --- .../webui/src/client/lib/rundownTiming.ts | 17 +++++++----- .../CurrentPartOrSegmentRemaining.tsx | 26 +++++++++++++++++-- .../RundownHeader/RundownHeader.scss | 9 +++++-- .../RundownHeader/RundownHeader.tsx | 14 ++++------ 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/packages/webui/src/client/lib/rundownTiming.ts b/packages/webui/src/client/lib/rundownTiming.ts index 428c63f0380..a273b072ed2 100644 --- a/packages/webui/src/client/lib/rundownTiming.ts +++ b/packages/webui/src/client/lib/rundownTiming.ts @@ -167,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 diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx index 585c19441d7..3772d964c16 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/CurrentPartOrSegmentRemaining.tsx @@ -125,7 +125,8 @@ function usePartRemaining(props: IPartRemainingProps) { let displayTimecode = timingDurations.remainingTimeOnCurrentPart if (props.preferSegmentTime) { - displayTimecode = timingDurations.remainingBudgetOnCurrentSegment ?? displayTimecode + if (timingDurations.remainingBudgetOnCurrentSegment === undefined) return null + displayTimecode = timingDurations.remainingBudgetOnCurrentSegment } if (displayTimecode === undefined) return null @@ -166,9 +167,30 @@ export const RundownHeaderPartRemaining: React.FC = (props) 0 ? props.heavyClassName : undefined)} - ms={displayTimecode || 0} > {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/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 97f3e375a08..593065c66cb 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -395,10 +395,15 @@ @extend .rundown-header__hoverable-label; opacity: 1; position: relative; - top: -0.4em; /* Match alignment from Countdown.scss */ + top: -0.2em; /* Match alignment from Countdown.scss */ } - .overtime { + .countdown__counter { + color: $general-countdown-to-next-color; + } + + .overtime, + .overtime .countdown__counter { color: $general-late-color; } } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index 0f67e5980b8..f7878f2371b 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -13,7 +13,7 @@ import { UIShowStyleBase } from '@sofie-automation/meteor-lib/dist/api/showStyle import Navbar from 'react-bootstrap/Navbar' import { RundownContextMenu, RundownHeaderContextMenuTrigger, RundownHamburgerButton } from './RundownContextMenu' import { TimeOfDay } from '../RundownTiming/TimeOfDay' -import { RundownHeaderPartRemaining } from '../RundownHeader/CurrentPartOrSegmentRemaining' +import { RundownHeaderPartRemaining, RundownHeaderSegmentBudget } from '../RundownHeader/CurrentPartOrSegmentRemaining' import { RundownHeaderTimers } from './RundownHeaderTimers' import { RundownHeaderTimingDisplay } from './RundownHeaderTimingDisplay' @@ -66,14 +66,10 @@ export function RundownHeader({ {playlist.currentPartInfo && (
- - {t('Seg. Budg.')} - - + {t('On Air')} Date: Fri, 6 Mar 2026 15:54:36 +0100 Subject: [PATCH 217/291] chore: Corrected the vertical alignment of the ON AIR label. --- .../src/client/ui/RundownView/RundownHeader/RundownHeader.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 593065c66cb..08f3d24888b 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -486,6 +486,7 @@ 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; From c9fcb7dc7e8c842ffe65efa1d6ba5e961346b804 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Fri, 6 Mar 2026 16:09:43 +0100 Subject: [PATCH 218/291] chore: Tweaked the T-timer Over/Under pill and narrowed the gap between counters and labels. --- .../RundownHeader/RundownHeader.scss | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 08f3d24888b..977048cd628 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -270,7 +270,7 @@ grid-template-columns: auto auto; align-items: baseline; justify-content: end; - column-gap: 0.6em; + column-gap: 0.3em; row-gap: 0.1em; .rundown-header__clocks-timers__row { @@ -301,7 +301,7 @@ color: #fff; margin-left: 0; display: flex; - align-items: baseline; + align-items: center; gap: 0; } @@ -328,19 +328,28 @@ .rundown-header__clocks-timers__timer__over-under { display: inline-block; - vertical-align: middle; - font-size: 0.65em; - padding: 0.05em 0.35em; + 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.5em; + margin-left: 0.25em; + margin-top: 0em; font-variant-numeric: tabular-nums; font-variation-settings: 'wdth' 25, - 'wght' 500, + 'wght' 600, 'slnt' 0, - 'opsz' 14; + '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; From 0221812e8442a5a7f7cebe9ec1f93d843531b0e2 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Fri, 6 Mar 2026 16:28:10 +0100 Subject: [PATCH 219/291] chore: Made the Show Timers group glow when the user hovers over the group, to better indicate that it is clickable. --- .../RundownView/RundownHeader/RundownHeader.scss | 16 ++++++++++++++++ .../RundownView/RundownHeader/RundownHeader.tsx | 5 +++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 977048cd628..31e02e0aee1 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -431,6 +431,21 @@ 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 12px rgba(255, 255, 255, 1); + } + + &:focus-visible { + text-shadow: 0 0 12px rgba(255, 255, 255, 1); + } &.rundown-header__show-timers--simplified { cursor: pointer; @@ -558,5 +573,6 @@ max-height: 1.5em; } } + } } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index f7878f2371b..e95cdb9c3e4 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -97,14 +97,15 @@ export function RundownHeader({
-
setSimplified((s) => !s)} > -
+ From 917aacc54970fbd7dd440fce9100f2e7d96b18f1 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Fri, 6 Mar 2026 16:36:21 +0100 Subject: [PATCH 220/291] chore: Tweak to vertical counter label alignment. --- .../src/client/ui/RundownView/RundownHeader/Countdown.scss | 2 +- .../src/client/ui/RundownView/RundownHeader/RundownHeader.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index 3a691c3e25b..c4d4ffcd265 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -12,7 +12,7 @@ @extend %hoverable-label; white-space: nowrap; position: relative; - top: -0.55em; /* Visually push the label up to align with cap height */ + top: -0.51em; /* Visually push the label up to align with cap height */ margin-left: auto; text-align: right; width: 100%; diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 31e02e0aee1..ee26862bfb3 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -404,7 +404,7 @@ @extend .rundown-header__hoverable-label; opacity: 1; position: relative; - top: -0.2em; /* Match alignment from Countdown.scss */ + top: -0.16em; /* Match alignment from Countdown.scss */ } .countdown__counter { From 6837cdc839e8cd534b028be1c54ed135658879c2 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Fri, 30 Jan 2026 15:26:10 +0000 Subject: [PATCH 221/291] feat: Add optional estimateState to T-Timer data type So we can measure if we are over or under time --- packages/corelib/src/dataModel/RundownPlaylist.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index d363480bd47..b6863b53758 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -168,15 +168,18 @@ export interface RundownTTimer { /** The estimated 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. - * Running means we are progressing towards the anchor (estimate moves with real time). - * Paused means we are pushing (e.g. overrunning the current segment, so the anchor is being delayed). + * The over/under diff is calculated as the difference between this estimate and the timer's target (state.zeroTime). * - * Calculated automatically when anchorPartId is set, or can be set manually by a blueprint. + * Running means we are progressing towards the anchor (estimate 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. */ estimateState?: TimerState - /** The target Part that this timer is counting towards (the "timing anchor"). + /** 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 estimateState based on when we expect to reach this part. * If not set, estimateState is not calculated automatically but can still be set manually by a blueprint. */ From c07b4e7f30f2fde278b1926638b9f702bc8cb5ce Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 4 Feb 2026 13:19:36 +0000 Subject: [PATCH 222/291] feat: Add function to Caclulate estimates for anchored T-Timers --- packages/job-worker/src/playout/tTimers.ts | 144 +++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index af86616f82a..2f327550f18 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -4,9 +4,14 @@ import type { 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 { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' +import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { JobContext } from '../jobs/index.js' +import { PlayoutModel } from './model/PlayoutModel.js' export function validateTTimerIndex(index: number): asserts index is RundownTTimerIndex { if (isNaN(index) || index < 1 || index > 3) throw new Error(`T-timer index out of range: ${index}`) @@ -167,3 +172,142 @@ export function calculateNextTimeOfDayTarget(targetTime: string | number): numbe }) return parsed ? parsed.getTime() : null } + +/** + * Recalculate T-Timer estimates based on timing anchors + * + * For each T-Timer that has an anchorPartId set, this function: + * 1. Iterates through ordered parts from current/next onwards + * 2. Accumulates expected durations until the anchor part is reached + * 3. Updates estimateState with the calculated duration + * 4. Sets the estimate as running if we're progressing, or paused if pushing (overrunning) + * + * @param context Job context + * @param playoutModel The playout model containing the playlist and parts + */ +export function recalculateTTimerEstimates(context: JobContext, playoutModel: PlayoutModel): void { + const span = context.startSpan('recalculateTTimerEstimates') + + 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 + } + + const currentPartInstance = playoutModel.currentPartInstance?.partInstance + const nextPartInstance = playoutModel.nextPartInstance?.partInstance + + // Get ordered parts to iterate through + const orderedParts = playoutModel.getAllOrderedParts() + + // Start from next part if available, otherwise current, otherwise first playable part + let startPartIndex: number | undefined + if (nextPartInstance) { + // We have a next part selected, start from there + startPartIndex = orderedParts.findIndex((p) => p._id === nextPartInstance.part._id) + } else if (currentPartInstance) { + // No next, but we have current - start from the part after current + const currentIndex = orderedParts.findIndex((p) => p._id === currentPartInstance.part._id) + if (currentIndex >= 0 && currentIndex < orderedParts.length - 1) { + startPartIndex = currentIndex + 1 + } + } + + // If we couldn't find a starting point, start from the first playable part + startPartIndex ??= orderedParts.findIndex((p) => isPartPlayable(p)) + + if (startPartIndex === undefined || startPartIndex < 0) { + // No parts to iterate through, clear estimates + for (const timer of tTimers) { + if (timer.anchorPartId) { + playoutModel.updateTTimer({ ...timer, estimateState: undefined }) + } + } + if (span) span.end() + return + } + + // Iterate through parts and accumulate durations + const playablePartsSlice = orderedParts.slice(startPartIndex).filter((p) => isPartPlayable(p)) + + const now = getCurrentTime() + let accumulatedDuration = 0 + + // Calculate remaining time for current part + // If not started, treat as if it starts now (elapsed = 0, remaining = full duration) + // Account for playOffset (e.g., from play-from-anywhere feature) + let isPushing = false + if (currentPartInstance) { + 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 + accumulatedDuration = Math.max(0, remaining) + } + } + + for (const part of playablePartsSlice) { + // Add this part's expected duration to the accumulator + const partDuration = part.expectedDurationWithTransition ?? part.expectedDuration ?? 0 + accumulatedDuration += partDuration + + // Check if this part is an anchor for any timer + const timersForThisPart = timerAnchors.get(part._id) + if (timersForThisPart) { + for (const timerIndex of timersForThisPart) { + const timer = tTimers[timerIndex - 1] + + // Update the timer's estimate + const estimateState: TimerState = isPushing + ? literal({ + paused: true, + duration: accumulatedDuration, + }) + : literal({ + paused: false, + zeroTime: now + accumulatedDuration, + }) + + playoutModel.updateTTimer({ ...timer, estimateState }) + } + // Remove this anchor since we've processed it + timerAnchors.delete(part._id) + } + + // Early exit if we've resolved all timers + if (timerAnchors.size === 0) { + break + } + } + + // Clear estimates for any timers whose anchors weren't found (e.g., anchor is in the past or removed) + // Any remaining entries in timerAnchors are anchors that weren't reached + for (const timerIndices of timerAnchors.values()) { + for (const timerIndex of timerIndices) { + const timer = tTimers[timerIndex - 1] + playoutModel.updateTTimer({ ...timer, estimateState: undefined }) + } + } + + if (span) span.end() +} From 36c662bed112cf6d061588c8a491262fd2d7ff58 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 4 Feb 2026 15:45:27 +0000 Subject: [PATCH 223/291] feat: Add RecalculateTTimerEstimates job and integrate into playout workflow --- packages/corelib/src/worker/studio.ts | 8 ++++ packages/job-worker/src/ingest/commit.ts | 21 ++++++--- packages/job-worker/src/playout/setNext.ts | 4 ++ .../job-worker/src/playout/tTimersJobs.ts | 44 +++++++++++++++++++ .../job-worker/src/workers/studio/jobs.ts | 3 ++ 5 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 packages/job-worker/src/playout/tTimersJobs.ts diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index de354d3202f..bcbde0b94ad 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 estimates based on current playlist state + * Called after setNext, takes, and ingest changes to update timing anchor estimates + */ + RecalculateTTimerEstimates = 'recalculateTTimerEstimates', + /** * Update the timeline with a regenerated Studio Baseline * Has no effect if a Playlist is active @@ -417,6 +423,8 @@ export type StudioJobFunc = { [StudioJobs.OnPlayoutPlaybackChanged]: (data: OnPlayoutPlaybackChangedProps) => void [StudioJobs.OnTimelineTriggerTime]: (data: OnTimelineTriggerTimeProps) => void + [StudioJobs.RecalculateTTimerEstimates]: () => void + [StudioJobs.UpdateStudioBaseline]: () => string | false [StudioJobs.CleanupEmptyPlaylists]: () => void diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index f5b7f0a9531..93e1f4d5a21 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 { recalculateTTimerEstimates } 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 estimates after ingest changes + recalculateTTimerEstimates(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 { @@ -613,6 +617,9 @@ export async function updatePlayoutAfterChangingRundownInPlaylist( const shouldUpdateTimeline = await ensureNextPartIsValid(context, playoutModel) + // Recalculate T-Timer estimates after playlist changes + recalculateTTimerEstimates(context, playoutModel) + if (playoutModel.playlist.activationId || shouldUpdateTimeline) { triggerUpdateTimelineAfterIngestData(context, playoutModel.playlistId) } diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index ebe5cdf6755..172497909a5 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -36,6 +36,7 @@ import { PersistentPlayoutStateStore } from '../blueprints/context/services/Pers import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PlayoutPartInstanceModelImpl } from './model/implementation/PlayoutPartInstanceModelImpl.js' import { QuickLoopService } from './model/services/QuickLoopService.js' +import { recalculateTTimerEstimates } from './tTimers.js' /** * Set or clear the nexted part, from a given PartInstance, or SelectNextPartResult @@ -99,6 +100,9 @@ export async function setNextPart( await cleanupOrphanedItems(context, playoutModel) + // Recalculate T-Timer estimates based on the new next part + recalculateTTimerEstimates(context, playoutModel) + 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..b1fede76426 --- /dev/null +++ b/packages/job-worker/src/playout/tTimersJobs.ts @@ -0,0 +1,44 @@ +import { JobContext } from '../jobs/index.js' +import { recalculateTTimerEstimates } from './tTimers.js' +import { runWithPlayoutModel, runWithPlaylistLock } from './lock.js' + +/** + * Handle RecalculateTTimerEstimates job + * This is called after setNext, takes, and ingest changes to update T-Timer estimates + * Since this job doesn't take a playlistId parameter, it finds the active playlist in the studio + */ +export async function handleRecalculateTTimerEstimates(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) => { + recalculateTTimerEstimates(context, playoutModel) + }) + }) + } +} diff --git a/packages/job-worker/src/workers/studio/jobs.ts b/packages/job-worker/src/workers/studio/jobs.ts index be5d81787da..7b66526a4d4 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 { handleRecalculateTTimerEstimates } from '../../playout/tTimersJobs.js' type ExecutableFunction = ( context: JobContext, @@ -87,6 +88,8 @@ export const studioJobHandlers: StudioJobHandlers = { [StudioJobs.OnPlayoutPlaybackChanged]: handleOnPlayoutPlaybackChanged, [StudioJobs.OnTimelineTriggerTime]: handleTimelineTriggerTime, + [StudioJobs.RecalculateTTimerEstimates]: handleRecalculateTTimerEstimates, + [StudioJobs.UpdateStudioBaseline]: handleUpdateStudioBaseline, [StudioJobs.CleanupEmptyPlaylists]: handleRemoveEmptyPlaylists, From de867b1b261d331f6fdeeebf6b6d2fc4bbb4c3ce Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 4 Feb 2026 15:46:30 +0000 Subject: [PATCH 224/291] feat: add timeout for T-Timer recalculations when pushing expected to start --- packages/job-worker/src/playout/tTimers.ts | 32 ++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index 2f327550f18..15f2e27a376 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -9,9 +9,18 @@ import { getCurrentTime } from '../lib/index.js' import type { ReadonlyDeep } from 'type-fest' import * as chrono from 'chrono-node' import { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' -import { PartId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { JobContext } from '../jobs/index.js' import { PlayoutModel } from './model/PlayoutModel.js' +import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' +import { logger } from '../logging.js' +import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' + +/** + * Map of active setTimeout timeouts by studioId + * Used to clear previous timeout when recalculation is triggered before the timeout fires + */ +const activeTimeouts = new Map() export function validateTTimerIndex(index: number): asserts index is RundownTTimerIndex { if (isNaN(index) || index < 1 || index > 3) throw new Error(`T-timer index out of range: ${index}`) @@ -189,6 +198,14 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl const span = context.startSpan('recalculateTTimerEstimates') const playlist = playoutModel.playlist + + // Clear any existing timeout for this studio + const existingTimeout = activeTimeouts.get(playlist.studioId) + if (existingTimeout) { + clearTimeout(existingTimeout) + activeTimeouts.delete(playlist.studioId) + } + const tTimers = playlist.tTimers // Find which timers have anchors that need calculation @@ -204,7 +221,7 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl // If no timers have anchors, nothing to do if (timerAnchors.size === 0) { if (span) span.end() - return + return undefined } const currentPartInstance = playoutModel.currentPartInstance?.partInstance @@ -263,6 +280,17 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl isPushing = remaining < 0 accumulatedDuration = Math.max(0, remaining) + + // Schedule next recalculation for when current part ends (if not pushing and no autoNext) + if (!isPushing && !currentPartInstance.part.autoNext) { + const delay = remaining + 5 // 5ms buffer + const timeoutId = setTimeout(() => { + context.queueStudioJob(StudioJobs.RecalculateTTimerEstimates, undefined, undefined).catch((err) => { + logger.error(`Failed to queue T-Timer recalculation: ${stringifyError(err)}`) + }) + }, delay) + activeTimeouts.set(playlist.studioId, timeoutId) + } } } From f6150917ee42ccef7d60d595a6ff5f8dd6b313bb Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 4 Feb 2026 15:47:51 +0000 Subject: [PATCH 225/291] feat: queue initial T-Timer recalculation when job-worker restarts This will ensure a timeout is set for the next expected push start time. --- packages/job-worker/src/workers/studio/child.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/job-worker/src/workers/studio/child.ts b/packages/job-worker/src/workers/studio/child.ts index 57974fbb737..138bfd10d0d 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.RecalculateTTimerEstimates, + 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') From c0dfed64f6b71b1b337d303a5d58b6172f556c61 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 4 Feb 2026 16:10:48 +0000 Subject: [PATCH 226/291] feat(blueprints): Add blueprint interface methods for T-Timer estimate management Add three new methods to IPlaylistTTimer interface: - clearEstimate() - Clear both manual estimates and anchor parts - setEstimateAnchorPart(partId) - Set anchor part for automatic calculation - setEstimateTime(time, paused?) - Manually set estimate as timestamp - setEstimateDuration(duration, paused?) - Manually set estimate as duration When anchor part is set, automatically queues RecalculateTTimerEstimates job. Manual estimates clear anchor parts and vice versa. Updated TTimersService to accept JobContext for job queueing capability. Updated all blueprint context instantiations and tests. --- .../src/context/tTimersContext.ts | 36 ++++ .../blueprints/context/OnSetAsNextContext.ts | 2 +- .../src/blueprints/context/OnTakeContext.ts | 2 +- .../context/RundownActivationContext.ts | 2 +- .../SyncIngestUpdateToPartInstanceContext.ts | 15 +- .../src/blueprints/context/adlibActions.ts | 2 +- .../context/services/TTimersService.ts | 91 ++++++++- .../services/__tests__/TTimersService.test.ts | 188 ++++++++++++------ .../src/ingest/syncChangesToPartInstance.ts | 1 + 9 files changed, 263 insertions(+), 76 deletions(-) diff --git a/packages/blueprints-integration/src/context/tTimersContext.ts b/packages/blueprints-integration/src/context/tTimersContext.ts index 8747f450a2c..cce8ca198dc 100644 --- a/packages/blueprints-integration/src/context/tTimersContext.ts +++ b/packages/blueprints-integration/src/context/tTimersContext.ts @@ -71,6 +71,42 @@ export interface IPlaylistTTimer { * @returns True if the timer was restarted, false if it could not be restarted */ restart(): boolean + + /** + * Clear any estimate (manual or anchor-based) for this timer + * This removes both manual estimates set via setEstimateTime/setEstimateDuration + * and automatic estimates based on anchor parts set via setEstimateAnchorPart. + */ + clearEstimate(): void + + /** + * Set the anchor part for automatic estimate calculation + * When set, the server automatically calculates when we expect to reach this part + * based on remaining part durations, and updates the estimate accordingly. + * Clears any manual estimate set via setEstimateTime/setEstimateDuration. + * @param partId The ID of the part to use as timing anchor + */ + setEstimateAnchorPart(partId: string): void + + /** + * Manually set the estimate 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 (estimate won't update with time passing). + * If false (default), we're progressing normally (estimate counts down in real-time). + */ + setEstimateTime(time: number, paused?: boolean): void + + /** + * Manually set the estimate as a relative duration from now + * Use this when you want to express the estimate 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 (estimate won't update with time passing). + * If false (default), we're progressing normally (estimate counts down in real-time). + */ + setEstimateDuration(duration: number, paused?: boolean): void } export type IPlaylistTTimerState = diff --git a/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts b/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts index 2be2d1b7f5f..1d168e84f88 100644 --- a/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts +++ b/packages/job-worker/src/blueprints/context/OnSetAsNextContext.ts @@ -49,7 +49,7 @@ export class OnSetAsNextContext public readonly manuallySelected: boolean ) { super(contextInfo, context, showStyle, watchedPackages) - this.#tTimersService = TTimersService.withPlayoutModel(playoutModel) + this.#tTimersService = TTimersService.withPlayoutModel(playoutModel, context) } public get quickLoopInfo(): BlueprintQuickLookInfo | null { diff --git a/packages/job-worker/src/blueprints/context/OnTakeContext.ts b/packages/job-worker/src/blueprints/context/OnTakeContext.ts index 82690fa7481..e028b31f1d8 100644 --- a/packages/job-worker/src/blueprints/context/OnTakeContext.ts +++ b/packages/job-worker/src/blueprints/context/OnTakeContext.ts @@ -66,7 +66,7 @@ export class OnTakeContext extends ShowStyleUserContext implements IOnTakeContex ) { super(contextInfo, _context, showStyle, watchedPackages) this.isTakeAborted = false - this.#tTimersService = TTimersService.withPlayoutModel(_playoutModel) + this.#tTimersService = TTimersService.withPlayoutModel(_playoutModel, _context) } async getUpcomingParts(limit: number = 5): Promise> { diff --git a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts index 9f6418b3a59..5335d041bc6 100644 --- a/packages/job-worker/src/blueprints/context/RundownActivationContext.ts +++ b/packages/job-worker/src/blueprints/context/RundownActivationContext.ts @@ -48,7 +48,7 @@ export class RundownActivationContext extends RundownEventContext implements IRu this._previousState = options.previousState this._currentState = options.currentState - this.#tTimersService = TTimersService.withPlayoutModel(this._playoutModel) + this.#tTimersService = TTimersService.withPlayoutModel(this._playoutModel, this._context) } get previousState(): IRundownActivationContextState { diff --git a/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts b/packages/job-worker/src/blueprints/context/SyncIngestUpdateToPartInstanceContext.ts index 3bbec8cdaad..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' @@ -45,6 +46,7 @@ export class SyncIngestUpdateToPartInstanceContext implements ISyncIngestUpdateToPartInstanceContext { readonly #context: JobContext + readonly #playoutModel: PlayoutModel readonly #proposedPieceInstances: Map> readonly #tTimersService: TTimersService readonly #changedTTimers = new Map() @@ -61,6 +63,7 @@ export class SyncIngestUpdateToPartInstanceContext constructor( context: JobContext, + playoutModel: PlayoutModel, contextInfo: ContextInfo, studio: ReadonlyDeep, showStyleCompound: ReadonlyDeep, @@ -80,12 +83,18 @@ export class SyncIngestUpdateToPartInstanceContext ) 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.#tTimersService = new TTimersService( + playlist.tTimers, + (updatedTimer) => { + this.#changedTTimers.set(updatedTimer.index, updatedTimer) + }, + this.#playoutModel, + this.#context + ) } getTimer(index: RundownTTimerIndex): IPlaylistTTimer { diff --git a/packages/job-worker/src/blueprints/context/adlibActions.ts b/packages/job-worker/src/blueprints/context/adlibActions.ts index 6251a791d54..80b4b312448 100644 --- a/packages/job-worker/src/blueprints/context/adlibActions.ts +++ b/packages/job-worker/src/blueprints/context/adlibActions.ts @@ -117,7 +117,7 @@ export class ActionExecutionContext extends ShowStyleUserContext implements IAct private readonly partAndPieceInstanceService: PartAndPieceInstanceActionService ) { super(contextInfo, _context, showStyle, watchedPackages) - this.#tTimersService = TTimersService.withPlayoutModel(_playoutModel) + this.#tTimersService = TTimersService.withPlayoutModel(_playoutModel, _context) } async getUpcomingParts(limit: number = 5): Promise> { diff --git a/packages/job-worker/src/blueprints/context/services/TTimersService.ts b/packages/job-worker/src/blueprints/context/services/TTimersService.ts index 1344b6f9761..9d92ff4886d 100644 --- a/packages/job-worker/src/blueprints/context/services/TTimersService.ts +++ b/packages/job-worker/src/blueprints/context/services/TTimersService.ts @@ -2,8 +2,10 @@ import type { IPlaylistTTimer, IPlaylistTTimerState, } from '@sofie-automation/blueprints-integration/dist/context/tTimersContext' -import type { RundownTTimer, RundownTTimerIndex } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { assertNever } from '@sofie-automation/corelib/dist/lib' +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 } from '@sofie-automation/corelib/dist/protectedString' import type { PlayoutModel } from '../../../playout/model/PlayoutModel.js' import { ReadonlyDeep } from 'type-fest' import { @@ -14,27 +16,36 @@ import { restartTTimer, resumeTTimer, validateTTimerIndex, + recalculateTTimerEstimates, } 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 + emitChange: (updatedTimer: ReadonlyDeep) => void, + playoutModel: PlayoutModel, + jobContext: JobContext ) { this.timers = [ - new PlaylistTTimerImpl(timers[0], emitChange), - new PlaylistTTimerImpl(timers[1], emitChange), - new PlaylistTTimerImpl(timers[2], emitChange), + 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): TTimersService { - return new TTimersService(playoutModel.playlist.tTimers, (updatedTimer) => { - playoutModel.updateTTimer(updatedTimer) - }) + static withPlayoutModel(playoutModel: PlayoutModel, jobContext: JobContext): TTimersService { + return new TTimersService( + playoutModel.playlist.tTimers, + (updatedTimer) => { + playoutModel.updateTTimer(updatedTimer) + }, + playoutModel, + jobContext + ) } getTimer(index: RundownTTimerIndex): IPlaylistTTimer { @@ -50,6 +61,8 @@ export class TTimersService { export class PlaylistTTimerImpl implements IPlaylistTTimer { readonly #emitChange: (updatedTimer: ReadonlyDeep) => void + readonly #playoutModel: PlayoutModel + readonly #jobContext: JobContext #timer: ReadonlyDeep @@ -96,9 +109,18 @@ export class PlaylistTTimerImpl implements IPlaylistTTimer { } } - constructor(timer: ReadonlyDeep, emitChange: (updatedTimer: ReadonlyDeep) => void) { + 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 { @@ -168,4 +190,51 @@ export class PlaylistTTimerImpl implements IPlaylistTTimer { this.#emitChange(newTimer) return true } + + clearEstimate(): void { + this.#timer = { + ...this.#timer, + anchorPartId: undefined, + estimateState: undefined, + } + this.#emitChange(this.#timer) + } + + setEstimateAnchorPart(partId: string): void { + this.#timer = { + ...this.#timer, + anchorPartId: protectString(partId), + estimateState: undefined, // Clear manual estimate + } + this.#emitChange(this.#timer) + + // Recalculate estimates immediately since we already have the playout model + recalculateTTimerEstimates(this.#jobContext, this.#playoutModel) + } + + setEstimateTime(time: number, paused: boolean = false): void { + const estimateState: TimerState = paused + ? literal({ paused: true, duration: time - getCurrentTime() }) + : literal({ paused: false, zeroTime: time }) + + this.#timer = { + ...this.#timer, + anchorPartId: undefined, // Clear automatic anchor + estimateState, + } + this.#emitChange(this.#timer) + } + + setEstimateDuration(duration: number, paused: boolean = false): void { + const estimateState: TimerState = paused + ? literal({ paused: true, duration }) + : literal({ paused: false, zeroTime: getCurrentTime() + duration }) + + this.#timer = { + ...this.#timer, + anchorPartId: undefined, // Clear automatic anchor + estimateState, + } + this.#emitChange(this.#timer) + } } 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 index 2fe7a21b299..9f8355cac65 100644 --- a/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts +++ b/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts @@ -6,6 +6,11 @@ import type { RundownTTimer, RundownTTimerIndex } from '@sofie-automation/coreli 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() @@ -42,8 +47,10 @@ describe('TTimersService', () => { 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) + const service = new TTimersService(timers, updateFn, mockPlayoutModel, mockJobContext) expect(service.timers).toHaveLength(3) expect(service.timers[0]).toBeInstanceOf(PlaylistTTimerImpl) @@ -54,8 +61,9 @@ describe('TTimersService', () => { it('from playout model', () => { const mockPlayoutModel = createMockPlayoutModel(createEmptyTTimers()) + const mockJobContext = createMockJobContext() - const service = TTimersService.withPlayoutModel(mockPlayoutModel) + const service = TTimersService.withPlayoutModel(mockPlayoutModel, mockJobContext) expect(service.timers).toHaveLength(3) const timer = service.getTimer(1) @@ -71,8 +79,10 @@ describe('TTimersService', () => { 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) + const service = new TTimersService(timers, updateFn, mockPlayoutModel, mockJobContext) const timer = service.getTimer(1) @@ -82,8 +92,10 @@ describe('TTimersService', () => { 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) + const service = new TTimersService(timers, updateFn, mockPlayoutModel, mockJobContext) const timer = service.getTimer(2) @@ -93,8 +105,10 @@ describe('TTimersService', () => { 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) + const service = new TTimersService(timers, updateFn, mockPlayoutModel, mockJobContext) const timer = service.getTimer(3) @@ -104,8 +118,10 @@ describe('TTimersService', () => { 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) + 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') @@ -120,10 +136,11 @@ describe('TTimersService', () => { tTimers[1].mode = { type: 'countdown', duration: 60000, stopAtZero: true } tTimers[1].state = { paused: false, zeroTime: 65000 } - const timers = createEmptyTTimers() const updateFn = jest.fn() + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() - const service = new TTimersService(timers, updateFn) + const service = new TTimersService(tTimers, updateFn, mockPlayoutModel, mockJobContext) service.clearAllTimers() @@ -149,7 +166,9 @@ describe('PlaylistTTimerImpl', () => { it('should return the correct index', () => { const tTimers = createEmptyTTimers() const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[1], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[1], updateFn, mockPlayoutModel, mockJobContext) expect(timer.index).toBe(2) }) @@ -158,16 +177,19 @@ describe('PlaylistTTimerImpl', () => { const tTimers = createEmptyTTimers() tTimers[1].label = 'Custom Label' const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[1], updateFn) + 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 timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) expect(timer.state).toBeNull() }) @@ -177,7 +199,9 @@ describe('PlaylistTTimerImpl', () => { tTimers[0].mode = { type: 'freeRun' } tTimers[0].state = { paused: false, zeroTime: 15000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) expect(timer.state).toEqual({ mode: 'freeRun', @@ -191,7 +215,9 @@ describe('PlaylistTTimerImpl', () => { tTimers[0].mode = { type: 'freeRun' } tTimers[0].state = { paused: true, duration: 3000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) expect(timer.state).toEqual({ mode: 'freeRun', @@ -209,7 +235,9 @@ describe('PlaylistTTimerImpl', () => { } tTimers[0].state = { paused: false, zeroTime: 15000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) expect(timer.state).toEqual({ mode: 'countdown', @@ -229,7 +257,9 @@ describe('PlaylistTTimerImpl', () => { } tTimers[0].state = { paused: true, duration: 2000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) expect(timer.state).toEqual({ mode: 'countdown', @@ -249,7 +279,9 @@ describe('PlaylistTTimerImpl', () => { } tTimers[0].state = { paused: false, zeroTime: 20000 } // 10 seconds in the future const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) expect(timer.state).toEqual({ mode: 'timeOfDay', @@ -270,7 +302,9 @@ describe('PlaylistTTimerImpl', () => { } tTimers[0].state = { paused: false, zeroTime: targetTimestamp } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) expect(timer.state).toEqual({ mode: 'timeOfDay', @@ -285,9 +319,10 @@ describe('PlaylistTTimerImpl', () => { describe('setLabel', () => { it('should update the label', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) timer.setLabel('New Label') @@ -306,7 +341,9 @@ describe('PlaylistTTimerImpl', () => { tTimers[0].mode = { type: 'freeRun' } tTimers[0].state = { paused: false, zeroTime: 5000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) timer.clearTimer() @@ -322,9 +359,10 @@ describe('PlaylistTTimerImpl', () => { describe('startCountdown', () => { it('should start a running countdown with default options', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) timer.startCountdown(60000) @@ -342,9 +380,10 @@ describe('PlaylistTTimerImpl', () => { it('should start a paused countdown', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) timer.startCountdown(30000, { startPaused: true, stopAtZero: false }) @@ -364,9 +403,10 @@ describe('PlaylistTTimerImpl', () => { describe('startFreeRun', () => { it('should start a running free-run timer', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) timer.startFreeRun() @@ -382,9 +422,10 @@ describe('PlaylistTTimerImpl', () => { it('should start a paused free-run timer', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) timer.startFreeRun({ startPaused: true }) @@ -402,9 +443,10 @@ describe('PlaylistTTimerImpl', () => { describe('startTimeOfDay', () => { it('should start a timeOfDay timer with time string', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) timer.startTimeOfDay('15:30') @@ -425,9 +467,10 @@ describe('PlaylistTTimerImpl', () => { it('should start a timeOfDay timer with numeric timestamp', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const targetTimestamp = 1737331200000 timer.startTimeOfDay(targetTimestamp) @@ -449,9 +492,10 @@ describe('PlaylistTTimerImpl', () => { it('should start a timeOfDay timer with stopAtZero false', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) timer.startTimeOfDay('18:00', { stopAtZero: false }) @@ -472,9 +516,10 @@ describe('PlaylistTTimerImpl', () => { it('should start a timeOfDay timer with 12-hour format', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) timer.startTimeOfDay('5:30pm') @@ -495,18 +540,20 @@ describe('PlaylistTTimerImpl', () => { it('should throw for invalid time string', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + 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 timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + 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') }) @@ -518,7 +565,9 @@ describe('PlaylistTTimerImpl', () => { tTimers[0].mode = { type: 'freeRun' } tTimers[0].state = { paused: false, zeroTime: 5000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.pause() @@ -538,7 +587,9 @@ describe('PlaylistTTimerImpl', () => { tTimers[0].mode = { type: 'countdown', duration: 60000, stopAtZero: true } tTimers[0].state = { paused: false, zeroTime: 70000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.pause() @@ -557,9 +608,10 @@ describe('PlaylistTTimerImpl', () => { it('should return false for timer with no mode', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.pause() @@ -576,7 +628,9 @@ describe('PlaylistTTimerImpl', () => { } tTimers[0].state = { paused: false, zeroTime: 20000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.pause() @@ -591,7 +645,9 @@ describe('PlaylistTTimerImpl', () => { tTimers[0].mode = { type: 'freeRun' } tTimers[0].state = { paused: true, duration: -3000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.resume() @@ -611,7 +667,9 @@ describe('PlaylistTTimerImpl', () => { tTimers[0].mode = { type: 'freeRun' } tTimers[0].state = { paused: false, zeroTime: 5000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.resume() @@ -622,9 +680,10 @@ describe('PlaylistTTimerImpl', () => { it('should return false for timer with no mode', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.resume() @@ -641,7 +700,9 @@ describe('PlaylistTTimerImpl', () => { } tTimers[0].state = { paused: false, zeroTime: 20000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.resume() @@ -656,7 +717,9 @@ describe('PlaylistTTimerImpl', () => { tTimers[0].mode = { type: 'countdown', duration: 60000, stopAtZero: true } tTimers[0].state = { paused: false, zeroTime: 40000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.restart() @@ -682,7 +745,9 @@ describe('PlaylistTTimerImpl', () => { } tTimers[0].state = { paused: true, duration: 15000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.restart() @@ -704,7 +769,9 @@ describe('PlaylistTTimerImpl', () => { tTimers[0].mode = { type: 'freeRun' } tTimers[0].state = { paused: false, zeroTime: 5000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.restart() @@ -721,7 +788,9 @@ describe('PlaylistTTimerImpl', () => { } tTimers[0].state = { paused: false, zeroTime: 5000 } // old target time const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.restart() @@ -750,7 +819,9 @@ describe('PlaylistTTimerImpl', () => { } tTimers[0].state = { paused: false, zeroTime: 5000 } const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.restart() @@ -760,9 +831,10 @@ describe('PlaylistTTimerImpl', () => { it('should return false for timer with no mode', () => { const tTimers = createEmptyTTimers() - const updateFn = jest.fn() - const timer = new PlaylistTTimerImpl(tTimers[0], updateFn) + const mockPlayoutModel = createMockPlayoutModel(tTimers) + const mockJobContext = createMockJobContext() + const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) const result = timer.restart() diff --git a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts index afee746ca29..41de01b1bfa 100644 --- a/packages/job-worker/src/ingest/syncChangesToPartInstance.ts +++ b/packages/job-worker/src/ingest/syncChangesToPartInstance.ts @@ -130,6 +130,7 @@ 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}`, From 5e26041c9f4e63a25b09758759b0aec03726f44f Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Tue, 17 Feb 2026 22:24:02 +0000 Subject: [PATCH 227/291] feat: Add ignoreQuickLoop parameter to getOrderedPartsAfterPlayhead function --- packages/job-worker/src/playout/lookahead/util.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 From 69657d1dbed29a0f8fd5fcb09072276c163d42fd Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Tue, 17 Feb 2026 22:24:19 +0000 Subject: [PATCH 228/291] feat: Refactor recalculateTTimerEstimates to use getOrderedPartsAfterPlayhead for improved part iteration --- packages/job-worker/src/playout/tTimers.ts | 28 ++++------------------ 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index 15f2e27a376..0615294d719 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -8,13 +8,13 @@ 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 { isPartPlayable } from '@sofie-automation/corelib/dist/dataModel/Part' import { PartId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { JobContext } from '../jobs/index.js' import { PlayoutModel } from './model/PlayoutModel.js' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' import { logger } from '../logging.js' import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' +import { getOrderedPartsAfterPlayhead } from './lookahead/util.js' /** * Map of active setTimeout timeouts by studioId @@ -225,28 +225,13 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl } const currentPartInstance = playoutModel.currentPartInstance?.partInstance - const nextPartInstance = playoutModel.nextPartInstance?.partInstance - // Get ordered parts to iterate through + // Get ordered parts after playhead (excludes previous, current, and next) + // Use ignoreQuickLoop=true to count parts linearly without loop-back behavior const orderedParts = playoutModel.getAllOrderedParts() + const playablePartsSlice = getOrderedPartsAfterPlayhead(context, playoutModel, orderedParts.length, true) - // Start from next part if available, otherwise current, otherwise first playable part - let startPartIndex: number | undefined - if (nextPartInstance) { - // We have a next part selected, start from there - startPartIndex = orderedParts.findIndex((p) => p._id === nextPartInstance.part._id) - } else if (currentPartInstance) { - // No next, but we have current - start from the part after current - const currentIndex = orderedParts.findIndex((p) => p._id === currentPartInstance.part._id) - if (currentIndex >= 0 && currentIndex < orderedParts.length - 1) { - startPartIndex = currentIndex + 1 - } - } - - // If we couldn't find a starting point, start from the first playable part - startPartIndex ??= orderedParts.findIndex((p) => isPartPlayable(p)) - - if (startPartIndex === undefined || startPartIndex < 0) { + if (playablePartsSlice.length === 0 && !currentPartInstance) { // No parts to iterate through, clear estimates for (const timer of tTimers) { if (timer.anchorPartId) { @@ -257,9 +242,6 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl return } - // Iterate through parts and accumulate durations - const playablePartsSlice = orderedParts.slice(startPartIndex).filter((p) => isPartPlayable(p)) - const now = getCurrentTime() let accumulatedDuration = 0 From 477b3787355c528ccc7c36a44123e997586bd4a6 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 4 Feb 2026 22:49:19 +0000 Subject: [PATCH 229/291] test: Add tests for new T-Timers functions --- .../services/__tests__/TTimersService.test.ts | 224 ++++++++++++++++++ .../src/playout/__tests__/tTimersJobs.test.ts | 211 +++++++++++++++++ 2 files changed, 435 insertions(+) create mode 100644 packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts 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 index 9f8355cac65..8922d386ccc 100644 --- a/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts +++ b/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts @@ -842,4 +842,228 @@ describe('PlaylistTTimerImpl', () => { expect(updateFn).not.toHaveBeenCalled() }) }) + + describe('clearEstimate', () => { + it('should clear both anchorPartId and estimateState', () => { + const tTimers = createEmptyTTimers() + tTimers[0].anchorPartId = 'part1' as any + tTimers[0].estimateState = { 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.clearEstimate() + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + estimateState: undefined, + }) + }) + + it('should work when estimates 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.clearEstimate() + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + estimateState: undefined, + }) + }) + }) + + describe('setEstimateAnchorPart', () => { + it('should set anchorPartId and clear estimateState', () => { + const tTimers = createEmptyTTimers() + tTimers[0].estimateState = { 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.setEstimateAnchorPart('part123') + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: 'part123', + estimateState: 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.setEstimateAnchorPart('part456')).not.toThrow() + + // Job queue should not be called (recalculate is called directly) + expect(mockJobContext.queueStudioJob).not.toHaveBeenCalled() + }) + }) + + describe('setEstimateTime', () => { + it('should set estimateState 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.setEstimateTime(50000, false) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + estimateState: { paused: false, zeroTime: 50000 }, + }) + }) + + it('should set estimateState 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.setEstimateTime(50000, true) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + estimateState: { paused: true, duration: 40000 }, // 50000 - 10000 (current time) + }) + }) + + it('should clear anchorPartId when setting manual estimate', () => { + 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.setEstimateTime(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.setEstimateTime(50000) + + expect(updateFn).toHaveBeenCalledWith( + expect.objectContaining({ + estimateState: { paused: false, zeroTime: 50000 }, + }) + ) + }) + }) + + describe('setEstimateDuration', () => { + it('should set estimateState 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.setEstimateDuration(30000, false) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + estimateState: { paused: false, zeroTime: 40000 }, // 10000 (current) + 30000 (duration) + }) + }) + + it('should set estimateState 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.setEstimateDuration(30000, true) + + expect(updateFn).toHaveBeenCalledWith({ + index: 1, + label: 'Timer 1', + mode: null, + state: null, + anchorPartId: undefined, + estimateState: { paused: true, duration: 30000 }, + }) + }) + + it('should clear anchorPartId when setting manual estimate', () => { + 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.setEstimateDuration(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.setEstimateDuration(30000) + + expect(updateFn).toHaveBeenCalledWith( + expect.objectContaining({ + estimateState: { paused: false, zeroTime: 40000 }, + }) + ) + }) + }) }) 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..e6623a952b7 --- /dev/null +++ b/packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts @@ -0,0 +1,211 @@ +import { setupDefaultJobEnvironment, MockJobContext } from '../../__mocks__/context.js' +import { handleRecalculateTTimerEstimates } 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('handleRecalculateTTimerEstimates', () => { + 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(handleRecalculateTTimerEstimates(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(handleRecalculateTTimerEstimates(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(handleRecalculateTTimerEstimates(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(handleRecalculateTTimerEstimates(context)).resolves.toBeUndefined() + }) + }) +}) From 0baeaa8fdebe9532e087deaa27605aef2f800cca Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 5 Feb 2026 12:05:45 +0000 Subject: [PATCH 230/291] feat(T-Timers): Add segment budget timing support to estimate calculations Implements segment budget timing for T-Timer estimate calculations in recalculateTTimerEstimates(). When a segment has a budgetDuration set, the function now: - Uses the segment budget instead of individual part durations - Tracks budget consumption as parts are traversed - Ignores budget timing if the anchor is within the budget segment (anchor part uses normal part duration timing) This matches the front-end timing behavior in rundownTiming.ts and ensures server-side estimates align with UI countdown calculations for budget-controlled segments. --- packages/job-worker/src/playout/tTimers.ts | 137 +++++++++++++-------- 1 file changed, 89 insertions(+), 48 deletions(-) diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index 0615294d719..b1c9b6192ea 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -8,7 +8,7 @@ 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, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartId, SegmentId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { JobContext } from '../jobs/index.js' import { PlayoutModel } from './model/PlayoutModel.js' import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' @@ -183,13 +183,17 @@ export function calculateNextTimeOfDayTarget(targetTime: string | number): numbe } /** - * Recalculate T-Timer estimates based on timing anchors + * Recalculate T-Timer estimates based on timing anchors using segment budget timing. * - * For each T-Timer that has an anchorPartId set, this function: - * 1. Iterates through ordered parts from current/next onwards - * 2. Accumulates expected durations until the anchor part is reached - * 3. Updates estimateState with the calculated duration - * 4. Sets the estimate as running if we're progressing, or paused if pushing (overrunning) + * 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 @@ -243,76 +247,113 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl } const now = getCurrentTime() - let accumulatedDuration = 0 - // Calculate remaining time for current part - // If not started, treat as if it starts now (elapsed = 0, remaining = full duration) - // Account for playOffset (e.g., from play-from-anywhere feature) + // Initialize accumulators + let totalAccumulator = 0 + let segmentAccumulator = 0 let isPushing = false + let currentSegmentId: SegmentId | undefined = undefined + + // Handle current part/segment if (currentPartInstance) { - 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 - accumulatedDuration = Math.max(0, remaining) - - // Schedule next recalculation for when current part ends (if not pushing and no autoNext) - if (!isPushing && !currentPartInstance.part.autoNext) { - const delay = remaining + 5 // 5ms buffer - const timeoutId = setTimeout(() => { - context.queueStudioJob(StudioJobs.RecalculateTTimerEstimates, undefined, undefined).catch((err) => { - logger.error(`Failed to queue T-Timer recalculation: ${stringifyError(err)}`) - }) - }, delay) - activeTimeouts.set(playlist.studioId, timeoutId) + 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 } } + + // Schedule next recalculation + if (!isPushing && !currentPartInstance.part.autoNext) { + const delay = totalAccumulator + 5 + const timeoutId = setTimeout(() => { + context.queueStudioJob(StudioJobs.RecalculateTTimerEstimates, undefined, undefined).catch((err) => { + logger.error(`Failed to queue T-Timer recalculation: ${stringifyError(err)}`) + }) + }, delay) + activeTimeouts.set(playlist.studioId, timeoutId) + } } + // Single pass through parts for (const part of playablePartsSlice) { - // Add this part's expected duration to the accumulator - const partDuration = part.expectedDurationWithTransition ?? part.expectedDuration ?? 0 - accumulatedDuration += partDuration + // 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 for any timer + // 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] - // Update the timer's estimate const estimateState: TimerState = isPushing ? literal({ paused: true, - duration: accumulatedDuration, + duration: anchorTime, }) : literal({ paused: false, - zeroTime: now + accumulatedDuration, + zeroTime: now + anchorTime, }) playoutModel.updateTTimer({ ...timer, estimateState }) } - // Remove this anchor since we've processed it + timerAnchors.delete(part._id) } - // Early exit if we've resolved all timers - if (timerAnchors.size === 0) { - break - } + // Accumulate this part's duration + const partDuration = part.expectedDurationWithTransition ?? part.expectedDuration ?? 0 + segmentAccumulator += partDuration } - // Clear estimates for any timers whose anchors weren't found (e.g., anchor is in the past or removed) - // Any remaining entries in timerAnchors are anchors that weren't reached - for (const timerIndices of timerAnchors.values()) { + // Clear estimates for unresolved anchors + for (const [, timerIndices] of timerAnchors.entries()) { for (const timerIndex of timerIndices) { const timer = tTimers[timerIndex - 1] playoutModel.updateTTimer({ ...timer, estimateState: undefined }) From 8f5a103509eb5a40ab728bab2fc96240ec29ea80 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Fri, 20 Feb 2026 10:51:13 +0000 Subject: [PATCH 231/291] Fix test by adding missing mocks --- .../src/ingest/__tests__/syncChangesToPartInstance.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts b/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts index 3f63fe88589..6fd99f48620 100644 --- a/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts +++ b/packages/job-worker/src/ingest/__tests__/syncChangesToPartInstance.test.ts @@ -118,6 +118,9 @@ describe('SyncChangesToPartInstancesWorker', () => { { findPart: jest.fn(() => undefined), getGlobalPieces: jest.fn(() => []), + getAllOrderedParts: jest.fn(() => []), + getOrderedSegments: jest.fn(() => []), + findAdlibPiece: jest.fn(() => undefined), }, mockOptions ) From 3c21f9d0084c95b56f87a787841c3cade444be79 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 25 Feb 2026 10:37:56 +0000 Subject: [PATCH 232/291] feat(T-Timers): Add convenience method to set estimate anchor part by externalId --- .../blueprints-integration/src/context/tTimersContext.ts | 9 +++++++++ .../src/blueprints/context/services/TTimersService.ts | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/blueprints-integration/src/context/tTimersContext.ts b/packages/blueprints-integration/src/context/tTimersContext.ts index cce8ca198dc..28e03b8ad60 100644 --- a/packages/blueprints-integration/src/context/tTimersContext.ts +++ b/packages/blueprints-integration/src/context/tTimersContext.ts @@ -88,6 +88,15 @@ export interface IPlaylistTTimer { */ setEstimateAnchorPart(partId: string): void + /** + * Set the anchor part for automatic estimate 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 estimate set via setEstimateTime/setEstimateDuration. + * @param externalId The externalId of the part to use as timing anchor + */ + setEstimateAnchorPartByExternalId(externalId: string): void + /** * Manually set the estimate as an absolute timestamp * Use this when you have custom logic for calculating when you expect to reach a timing point. diff --git a/packages/job-worker/src/blueprints/context/services/TTimersService.ts b/packages/job-worker/src/blueprints/context/services/TTimersService.ts index 9d92ff4886d..5f79b7417ff 100644 --- a/packages/job-worker/src/blueprints/context/services/TTimersService.ts +++ b/packages/job-worker/src/blueprints/context/services/TTimersService.ts @@ -5,7 +5,7 @@ import type { 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 } from '@sofie-automation/corelib/dist/protectedString' +import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString' import type { PlayoutModel } from '../../../playout/model/PlayoutModel.js' import { ReadonlyDeep } from 'type-fest' import { @@ -212,6 +212,13 @@ export class PlaylistTTimerImpl implements IPlaylistTTimer { recalculateTTimerEstimates(this.#jobContext, this.#playoutModel) } + setEstimateAnchorPartByExternalId(externalId: string): void { + const part = this.#playoutModel.getAllOrderedParts().find((p) => p.externalId === externalId) + if (!part) return + + this.setEstimateAnchorPart(unprotectString(part._id)) + } + setEstimateTime(time: number, paused: boolean = false): void { const estimateState: TimerState = paused ? literal({ paused: true, duration: time - getCurrentTime() }) From 060ef95bb0b695d92d0e654fb838763f52b58182 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 18 Feb 2026 17:01:13 +0000 Subject: [PATCH 233/291] feat(T-Timers): Add pauseTime field to timer estimates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional pauseTime field to TimerState type to indicate when a timer should automatically pause (when current part ends and overrun begins). Benefits: - Client can handle running→paused transition locally without server update - Reduces latency in state transitions - Server still triggers recalculation on Take/part changes - More declarative timing ("pause at this time" vs "set paused now") Implementation: - When not pushing: pauseTime = now + currentPartRemainingTime - When already pushing: pauseTime = null - Client should display timer as paused when now >= pauseTime --- packages/corelib/src/dataModel/RundownPlaylist.ts | 5 +++++ packages/job-worker/src/playout/tTimers.ts | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index b6863b53758..0f84e83aa19 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -130,6 +130,7 @@ export interface RundownTTimerModeTimeOfDay { * 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). */ export type TimerState = | { @@ -137,12 +138,16 @@ export type TimerState = 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 } export type RundownTTimerIndex = 1 | 2 | 3 diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index b1c9b6192ea..e843ed40c04 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -301,6 +301,9 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl } } + // Save remaining current part time for pauseTime calculation + const currentPartRemainingTime = totalAccumulator + // Single pass through parts for (const part of playablePartsSlice) { // Detect segment boundary @@ -335,10 +338,12 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl ? 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, estimateState }) From 22abc9787beafec5e6a5ae78c2787faeff3ce78b Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 19 Feb 2026 11:21:56 +0000 Subject: [PATCH 234/291] Remove timeout based update of T-Timer now we have pauseTime --- packages/job-worker/src/playout/tTimers.ts | 29 +--------------------- 1 file changed, 1 insertion(+), 28 deletions(-) diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index e843ed40c04..917aa310279 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -8,20 +8,11 @@ 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, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { PartId, SegmentId } from '@sofie-automation/corelib/dist/dataModel/Ids' import { JobContext } from '../jobs/index.js' import { PlayoutModel } from './model/PlayoutModel.js' -import { StudioJobs } from '@sofie-automation/corelib/dist/worker/studio' -import { logger } from '../logging.js' -import { stringifyError } from '@sofie-automation/shared-lib/dist/lib/stringifyError' import { getOrderedPartsAfterPlayhead } from './lookahead/util.js' -/** - * Map of active setTimeout timeouts by studioId - * Used to clear previous timeout when recalculation is triggered before the timeout fires - */ -const activeTimeouts = new Map() - export function validateTTimerIndex(index: number): asserts index is RundownTTimerIndex { if (isNaN(index) || index < 1 || index > 3) throw new Error(`T-timer index out of range: ${index}`) } @@ -203,13 +194,6 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl const playlist = playoutModel.playlist - // Clear any existing timeout for this studio - const existingTimeout = activeTimeouts.get(playlist.studioId) - if (existingTimeout) { - clearTimeout(existingTimeout) - activeTimeouts.delete(playlist.studioId) - } - const tTimers = playlist.tTimers // Find which timers have anchors that need calculation @@ -288,17 +272,6 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl totalAccumulator = currentSegmentBudget } } - - // Schedule next recalculation - if (!isPushing && !currentPartInstance.part.autoNext) { - const delay = totalAccumulator + 5 - const timeoutId = setTimeout(() => { - context.queueStudioJob(StudioJobs.RecalculateTTimerEstimates, undefined, undefined).catch((err) => { - logger.error(`Failed to queue T-Timer recalculation: ${stringifyError(err)}`) - }) - }, delay) - activeTimeouts.set(playlist.studioId, timeoutId) - } } // Save remaining current part time for pauseTime calculation From 825e3673733302d5b8e85e4f7c5b12236a54f263 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 19 Feb 2026 11:23:33 +0000 Subject: [PATCH 235/291] docs(T-Timers): Add client rendering logic for pauseTime Document the client-side logic for rendering timer states with pauseTime support: - paused === true: use frozen duration - pauseTime && now >= pauseTime: use zeroTime - pauseTime (auto-pause) - otherwise: use zeroTime - now (running normally) --- packages/corelib/src/dataModel/RundownPlaylist.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index 0f84e83aa19..6bc6bfa8430 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -131,6 +131,20 @@ export interface RundownTTimerModeTimeOfDay { * 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 = | { From 9a60bdb3c80748aa97c4bb744c6d8fd206a6bf1a Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 19 Feb 2026 13:20:36 +0000 Subject: [PATCH 236/291] feat(T-Timers): Add timerStateToDuration helper function Add timerStateToDuration() function to calculate current timer duration from TimerState, handling all three cases: - Manually paused or already pushing - Auto-pause at overrun (pauseTime) - Running normally Also rename "currentTime" to "currentDuration" in "calculateTTimerDiff" method --- .../corelib/src/dataModel/RundownPlaylist.ts | 21 +++++++++++++++++++ packages/webui/src/client/lib/tTimerUtils.ts | 16 +++++++------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index 6bc6bfa8430..e3eb8fae8c3 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -164,6 +164,27 @@ export type TimerState = 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 { diff --git a/packages/webui/src/client/lib/tTimerUtils.ts b/packages/webui/src/client/lib/tTimerUtils.ts index 08ec4f19e2a..de7377a5b0f 100644 --- a/packages/webui/src/client/lib/tTimerUtils.ts +++ b/packages/webui/src/client/lib/tTimerUtils.ts @@ -1,4 +1,4 @@ -import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownTTimer, timerStateToDuration } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' /** * Calculate the display diff for a T-Timer. @@ -11,19 +11,19 @@ export function calculateTTimerDiff(timer: RundownTTimer, now: number): number { } // Get current time: either frozen duration or calculated from zeroTime - const currentTime = timer.state.paused ? timer.state.duration : timer.state.zeroTime - now + const currentDuration = timerStateToDuration(timer.state, now) // Free run counts up, so negate to get positive elapsed time if (timer.mode?.type === 'freeRun') { - return -currentTime + return -currentDuration } // Apply stopAtZero if configured - if (timer.mode?.stopAtZero && currentTime < 0) { + if (timer.mode?.stopAtZero && currentDuration < 0) { return 0 } - return currentTime + return currentDuration } /** @@ -40,10 +40,8 @@ export function calculateTTimerOverUnder(timer: RundownTTimer, now: number): num return undefined } - const duration = timer.state.paused ? timer.state.duration : timer.state.zeroTime - now - const estimateDuration = timer.estimateState.paused - ? timer.estimateState.duration - : timer.estimateState.zeroTime - now + const duration = timerStateToDuration(timer.state, now) + const estimateDuration = timerStateToDuration(timer.estimateState, now) return duration - estimateDuration } From 05ee3bf7d8b2734a2da0b1d18ae706915aa8f103 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Fri, 6 Mar 2026 13:29:35 +0000 Subject: [PATCH 237/291] Fix sign of over/under calculation If the estimate is big, the output should be positive for over. --- packages/webui/src/client/lib/tTimerUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/lib/tTimerUtils.ts b/packages/webui/src/client/lib/tTimerUtils.ts index de7377a5b0f..8b5a0938ea5 100644 --- a/packages/webui/src/client/lib/tTimerUtils.ts +++ b/packages/webui/src/client/lib/tTimerUtils.ts @@ -43,7 +43,7 @@ export function calculateTTimerOverUnder(timer: RundownTTimer, now: number): num const duration = timerStateToDuration(timer.state, now) const estimateDuration = timerStateToDuration(timer.estimateState, now) - return duration - estimateDuration + return estimateDuration - duration } // TODO: remove this mock From 2914087e7e938ba6309e2043256e3a7659eb3e4f Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Fri, 6 Mar 2026 13:55:45 +0000 Subject: [PATCH 238/291] Include next part in calculation --- packages/job-worker/src/playout/tTimers.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index 917aa310279..5bd5f04c05e 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -277,6 +277,14 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl // 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 From 0ac6ff169799fc619f77b625de8fdf821da26ee7 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Fri, 6 Mar 2026 14:42:53 +0000 Subject: [PATCH 239/291] Don't fetch all parts just to get a max length Just use infininty. The total length may not even be long enough in certain edge cases, for example if you requeue the first segment while later in the showl. --- packages/job-worker/src/playout/tTimers.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index 5bd5f04c05e..bb005e52b70 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -216,8 +216,7 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl // Get ordered parts after playhead (excludes previous, current, and next) // Use ignoreQuickLoop=true to count parts linearly without loop-back behavior - const orderedParts = playoutModel.getAllOrderedParts() - const playablePartsSlice = getOrderedPartsAfterPlayhead(context, playoutModel, orderedParts.length, true) + const playablePartsSlice = getOrderedPartsAfterPlayhead(context, playoutModel, Infinity, true) if (playablePartsSlice.length === 0 && !currentPartInstance) { // No parts to iterate through, clear estimates From a20cc05f0d9f82962c97dbdaaef03af120a046fb Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Fri, 6 Mar 2026 14:43:25 +0000 Subject: [PATCH 240/291] Ensure we recalculate timings when we queue segments --- packages/job-worker/src/playout/setNext.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index 172497909a5..57574949452 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -533,6 +533,10 @@ export async function queueNextSegment( } else { playoutModel.setQueuedSegment(null) } + + // Recalculate timer estimates as the queued segment affects what comes after next + recalculateTTimerEstimates(context, playoutModel) + span?.end() return { queuedSegmentId: queuedSegment?.segment?._id ?? null } } From 03d2937943efdc0b3185d4a3e6b551dbd14dc93c Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Mon, 9 Mar 2026 17:55:44 +0000 Subject: [PATCH 241/291] Fix linting issue --- .../ui/RundownView/RundownHeader/HeaderFreezeFrameIcon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/HeaderFreezeFrameIcon.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/HeaderFreezeFrameIcon.tsx index 18318ac74fd..7a6328d03e8 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/HeaderFreezeFrameIcon.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/HeaderFreezeFrameIcon.tsx @@ -5,7 +5,7 @@ import { useTracker } from '../../../lib/ReactMeteorData/ReactMeteorData' import { PartInstances, PieceInstances } from '../../../collections' import { VTContent } from '@sofie-automation/blueprints-integration' -export function HeaderFreezeFrameIcon({ partInstanceId }: { partInstanceId: PartInstanceId }) { +export function HeaderFreezeFrameIcon({ partInstanceId }: { partInstanceId: PartInstanceId }): JSX.Element | null { const timingDurations = useTiming(TimingTickResolution.Synced, TimingDataResolution.Synced) const freezeFrameIcon = useTracker( From db1ba21f75127f0e3351c7dc75a1c307e3e0adb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Thu, 12 Mar 2026 12:14:18 +0100 Subject: [PATCH 242/291] chore: changes wording for Rem. Dur (was Est. Dur) --- .../ui/RundownView/RundownHeader/RundownHeaderDurations.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx index 2018192cd90..c543acea2e8 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx @@ -47,7 +47,7 @@ export function RundownHeaderDurations({ ) : null} {!simplified && estDuration != null ? ( - + {RundownUtils.formatDiffToTimecode(estDuration, false, true, true, true, true)} ) : null} From 7014cd4dd18a8c40bfcb8a1715545316cd382b79 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Wed, 11 Mar 2026 15:32:13 +0100 Subject: [PATCH 243/291] chore: Added missing font variant variable. --- .../src/client/ui/RundownView/RundownHeader/RundownHeader.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index ee26862bfb3..ebf6ce9fff3 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -341,6 +341,7 @@ 'wdth' 25, 'wght' 600, 'slnt' 0, + 'GRAD' 0, 'opsz' 14, 'XOPQ' 96, 'XTRA' 468, From 759b77f5e8b2a978a7608214fd2f3d59b22225c2 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 12 Mar 2026 11:26:42 +0100 Subject: [PATCH 244/291] chore: Removed unused style for timeOfDay T-timer counters, as T-timers are never shown as time-of-day. --- .../ui/RundownView/RundownHeader/RundownHeader.scss | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index ebf6ce9fff3..493b8c50f7d 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -282,14 +282,6 @@ white-space: nowrap; line-height: 1.25; - &.countdown--timeofday { - .countdown__digit, - .countdown__sep { - font-style: italic; - font-weight: 300; - color: #40b8fa; - } - } .countdown__label { @extend .rundown-header__hoverable-label; margin-left: 0; From 0c432a2bf2b54d7a47e425ac0a73f1edf7d14ae8 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 12 Mar 2026 11:36:06 +0100 Subject: [PATCH 245/291] chore: Show only the playlist name if the playlist contains more than one rundown, otherwise show only the rundown name. --- .../client/ui/RundownView/RundownHeader/RundownHeader.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index e95cdb9c3e4..52dd6add17c 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -90,8 +90,11 @@ export function RundownHeader({
- {(currentRundown ?? firstRundown)?.name} - {rundownCount > 1 && {playlist.name}} + {rundownCount > 1 ? ( + {playlist.name} + ) : ( + {(currentRundown ?? firstRundown)?.name} + )}
From f61cf24ef898eb6a2edf4739f78ccbe1343c450c Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 12 Mar 2026 12:18:12 +0100 Subject: [PATCH 246/291] chore: Tweaked width of time of day counters so that their total width matches the width of a counter with proceeding - sign. --- .../src/client/ui/RundownView/RundownHeader/Countdown.scss | 2 +- .../src/client/ui/RundownView/RundownHeader/RundownHeader.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index c4d4ffcd265..d9cace106b8 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -47,7 +47,7 @@ font-size: 1.3em; letter-spacing: 0.02em; font-variation-settings: - 'wdth' 70, + 'wdth' 85, 'wght' 400, 'slnt' -5, 'GRAD' 0, diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 493b8c50f7d..7575cd85886 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -84,7 +84,7 @@ &.time-now { font-size: 1.8em; font-variation-settings: - 'wdth' 70, + 'wdth' 85, 'wght' 400, 'slnt' -5, 'GRAD' 0, From 34e1dba9fc2b7b7b7e9540c2709efbcc5b5012f3 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 12 Mar 2026 12:19:07 +0100 Subject: [PATCH 247/291] chore: Added label before the counter to/from "Planned Start" in Detailed Mode of the Top Bar. --- .../ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx index f764b037fe7..a9ea781de12 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx @@ -28,7 +28,7 @@ export function RundownHeaderPlannedStart({ (playlist.startedPlayback ? ( ) : ( - + {diff >= 0 && '-'} {RundownUtils.formatDiffToTimecode(Math.abs(diff), false, false, true, true, true)} From 24d290e11113c623ac443b42e4c220453700f0aa Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 12 Mar 2026 13:31:37 +0100 Subject: [PATCH 248/291] chore: Removed erraneous margin of some of the counter colons. --- .../src/client/ui/RundownView/RundownHeader/Countdown.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index d9cace106b8..d0335ff432a 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -85,7 +85,7 @@ } &__sep { - margin: 0 0.05em; + margin: 0 0em; &--dimmed { opacity: 0.4; From 5e3f90373977c61e2a57fd69f095181076d572c2 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 12 Mar 2026 15:32:48 +0100 Subject: [PATCH 249/291] chore: Made all timer labels center-aligned. --- .../src/client/ui/RundownView/RundownHeader/Countdown.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index d0335ff432a..6fdf4e253ba 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -12,7 +12,7 @@ @extend %hoverable-label; white-space: nowrap; position: relative; - top: -0.51em; /* Visually push the label up to align with cap height */ + top: -0.3em; /* Visually place the label to vertically align */ margin-left: auto; text-align: right; width: 100%; From 6aca4a33fb3869330f262998393054054dc55879 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 12 Mar 2026 13:09:48 +0000 Subject: [PATCH 250/291] remove RundownHeader_old folder left behind by mistake --- .../RundownHeader_old/RundownHeaderTimers.tsx | 97 --- .../RundownHeader_old/RundownHeader_old.tsx | 235 ------ .../RundownReloadResponse.ts | 177 ----- .../RundownHeader_old/TimingDisplay.tsx | 97 --- .../useRundownPlaylistOperations.tsx | 693 ------------------ 5 files changed, 1299 deletions(-) delete mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeaderTimers.tsx delete mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeader_old.tsx delete mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownReloadResponse.ts delete mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx delete mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader_old/useRundownPlaylistOperations.tsx diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeaderTimers.tsx deleted file mode 100644 index 132963d6968..00000000000 --- a/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeaderTimers.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react' -import { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { useTiming } from '../RundownTiming/withTiming' -import { RundownUtils } from '../../../lib/rundown' -import classNames from 'classnames' -import { getCurrentTime } from '../../../lib/systemTime' - -interface IProps { - tTimers: [RundownTTimer, RundownTTimer, RundownTTimer] -} - -export const RundownHeaderTimers: React.FC = ({ tTimers }) => { - useTiming() - - const activeTimers = tTimers.filter((t) => t.mode) - - if (activeTimers.length == 0) return null - - return ( -
- {activeTimers.map((timer) => ( - - ))} -
- ) -} - -interface ISingleTimerProps { - timer: RundownTTimer -} - -function SingleTimer({ timer }: ISingleTimerProps) { - const now = getCurrentTime() - - const isRunning = !!timer.state && !timer.state.paused - - const diff = calculateDiff(timer, now) - const timeStr = RundownUtils.formatDiffToTimecode(Math.abs(diff), false, true, true, false, true) - const parts = timeStr.split(':') - - const timerSign = diff >= 0 ? '+' : '-' - - const isCountingDown = timer.mode?.type === 'countdown' && diff < 0 && isRunning - - return ( -
- {timer.label} -
- {timerSign} - {parts.map((p, i) => ( - - - {p} - - {i < parts.length - 1 && :} - - ))} -
-
- ) -} - -function calculateDiff(timer: RundownTTimer, now: number): number { - if (!timer.state || timer.state.paused === undefined) { - return 0 - } - - // Get current time: either frozen duration or calculated from zeroTime - const currentTime = timer.state.paused ? timer.state.duration : timer.state.zeroTime - now - - // Free run counts up, so negate to get positive elapsed time - if (timer.mode?.type === 'freeRun') { - return -currentTime - } - - // Apply stopAtZero if configured - if (timer.mode?.stopAtZero && currentTime < 0) { - return 0 - } - - return currentTime -} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeader_old.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeader_old.tsx deleted file mode 100644 index e235cb792f4..00000000000 --- a/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownHeader_old.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react' -import { useTranslation } from 'react-i18next' -import * as CoreIcon from '@nrk/core-icons/jsx' -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 { 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' - -interface IRundownHeaderProps { - playlist: DBRundownPlaylist - showStyleBase: UIShowStyleBase - showStyleVariant: DBShowStyleVariant - currentRundown: Rundown | undefined - studio: UIStudio - rundownIds: RundownId[] - firstRundown: Rundown | undefined - onActivate?: (isRehearsal: boolean) => void - inActiveRundownView?: boolean - layout: RundownLayoutRundownHeader | undefined -} - -export function RundownHeader_old({ - playlist, - showStyleBase, - showStyleVariant, - currentRundown, - studio, - rundownIds, - firstRundown, - inActiveRundownView, - layout, -}: IRundownHeaderProps): JSX.Element { - const { t } = useTranslation() - - const userPermissions = useContext(UserPermissionsContext) - - const [selectedPiece, setSelectedPiece] = useState(undefined) - const [shouldQueueAdlibs, setShouldQueueAdlibs] = useState(false) - - const operations = useRundownPlaylistOperations() - - 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]) - - const canClearQuickLoop = - !!studio.settings.enableQuickLoop && - !RundownResolver.isLoopLocked(playlist) && - RundownResolver.isAnyLoopMarkerDefined(playlist) - - const rundownTimesInfo = checkRundownTimes(playlist.timing) - - useEffect(() => { - console.debug(`Rundown T-Timers Info: `, JSON.stringify(playlist.tTimers, undefined, 2)) - }, [playlist.tTimers]) - - 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')} - - )} -
-
- - - - noResetOnActivate ? operations.activateRundown(e) : operations.resetAndActivateRundown(e) - } - /> -
-
-
- -
- -
-
- {layout && RundownLayoutsAPI.isDashboardLayout(layout) ? ( - - ) : ( - <> - - - - )} -
-
- - - -
-
-
- - - - ) -} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownReloadResponse.ts b/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownReloadResponse.ts deleted file mode 100644 index a858db9caf2..00000000000 --- a/packages/webui/src/client/ui/RundownView/RundownHeader_old/RundownReloadResponse.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { RundownPlaylists, Rundowns } from '../../../collections' -import { - ReloadRundownPlaylistResponse, - TriggerReloadDataResponse, -} from '@sofie-automation/meteor-lib/dist/api/userActions' -import _ from 'underscore' -import { RundownPlaylistCollectionUtil } from '../../../collections/rundownPlaylistUtil' -import * as i18next from 'i18next' -import { UserPermissions } from '../../UserPermissions' -import { NoticeLevel, Notification, NotificationCenter } from '../../../lib/notifications/notifications' -import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { getRundownNrcsName } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { UserAction } from '@sofie-automation/meteor-lib/dist/userAction' -import { Tracker } from 'meteor/tracker' -import { doUserAction } from '../../../lib/clientUserAction' -import { MeteorCall } from '../../../lib/meteorApi' -import { doModalDialog } from '../../../lib/ModalDialog' - -export function handleRundownPlaylistReloadResponse( - t: i18next.TFunction, - userPermissions: Readonly, - result: ReloadRundownPlaylistResponse -): boolean { - const rundownsInNeedOfHandling = result.rundownsResponses.filter( - (r) => r.response === TriggerReloadDataResponse.MISSING - ) - const firstRundownId = _.first(rundownsInNeedOfHandling)?.rundownId - let allRundownsAffected = false - - if (firstRundownId) { - const firstRundown = Rundowns.findOne(firstRundownId) - const playlist = RundownPlaylists.findOne(firstRundown?.playlistId) - const allRundownIds = playlist ? RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) : [] - if ( - allRundownIds.length > 0 && - _.difference( - allRundownIds, - rundownsInNeedOfHandling.map((r) => r.rundownId) - ).length === 0 - ) { - allRundownsAffected = true - } - } - - const actionsTaken: RundownReloadResponseUserAction[] = [] - function onActionTaken(action: RundownReloadResponseUserAction): void { - actionsTaken.push(action) - if (actionsTaken.length === rundownsInNeedOfHandling.length) { - // the user has taken action on all of the missing rundowns - if (allRundownsAffected && actionsTaken.filter((actionTaken) => actionTaken !== 'removed').length === 0) { - // all rundowns in the playlist were affected and all of them were removed - // we redirect to the Lobby - window.location.assign('/') - } - } - } - - const handled = rundownsInNeedOfHandling.map((r) => - handleRundownReloadResponse(t, userPermissions, r.rundownId, r.response, onActionTaken) - ) - return handled.reduce((previousValue, value) => previousValue || value, false) -} - -export type RundownReloadResponseUserAction = 'removed' | 'unsynced' | 'error' - -export function handleRundownReloadResponse( - t: i18next.TFunction, - userPermissions: Readonly, - rundownId: RundownId, - result: TriggerReloadDataResponse, - clb?: (action: RundownReloadResponseUserAction) => void -): boolean { - let hasDoneSomething = false - - if (result === TriggerReloadDataResponse.MISSING) { - const rundown = Rundowns.findOne(rundownId) - const playlist = RundownPlaylists.findOne(rundown?.playlistId) - - hasDoneSomething = true - const notification = new Notification( - undefined, - NoticeLevel.CRITICAL, - t( - 'Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced or remove the rundown from Sofie. What do you want to do?', - { - nrcsName: getRundownNrcsName(rundown), - rundownName: rundown?.name || t('(Unknown rundown)'), - playlistName: playlist?.name || t('(Unknown playlist)'), - } - ), - 'userAction', - undefined, - true, - [ - // actions: - { - label: t('Leave Unsynced'), - type: 'default', - disabled: !userPermissions.studio, - action: () => { - doUserAction( - t, - 'Missing rundown action', - UserAction.UNSYNC_RUNDOWN, - async (e, ts) => MeteorCall.userAction.unsyncRundown(e, ts, rundownId), - (err) => { - if (!err) { - notificationHandle.stop() - clb?.('unsynced') - } else { - clb?.('error') - } - } - ) - }, - }, - { - label: t('Remove'), - type: 'default', - action: () => { - doModalDialog({ - title: t('Remove rundown'), - message: t( - 'Do you really want to remove just the rundown "{{rundownName}}" in the playlist {{playlistName}} from Sofie? \n\nThis cannot be undone!', - { - rundownName: rundown?.name || 'N/A', - playlistName: playlist?.name || 'N/A', - } - ), - onAccept: () => { - // nothing - doUserAction( - t, - 'Missing rundown action', - UserAction.REMOVE_RUNDOWN, - async (e, ts) => MeteorCall.userAction.removeRundown(e, ts, rundownId), - (err) => { - if (!err) { - notificationHandle.stop() - clb?.('removed') - } else { - clb?.('error') - } - } - ) - }, - }) - }, - }, - ] - ) - const notificationHandle = NotificationCenter.push(notification) - - if (rundown) { - // This allows the semi-modal dialog above to be closed automatically, once the rundown stops existing - // for whatever reason - const comp = Tracker.autorun(() => { - const rundown = Rundowns.findOne(rundownId, { - fields: { - _id: 1, - orphaned: 1, - }, - }) - // we should hide the message - if (!rundown || !rundown.orphaned) { - notificationHandle.stop() - } - }) - notification.on('dropped', () => { - // clean up the reactive computation above when the notification is closed. Will be also executed by - // the notificationHandle.stop() above, so the Tracker.autorun will clean up after itself as well. - comp.stop() - }) - } - } - return hasDoneSomething -} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx deleted file mode 100644 index 809c544fff4..00000000000 --- a/packages/webui/src/client/ui/RundownView/RundownHeader_old/TimingDisplay.tsx +++ /dev/null @@ -1,97 +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 '../RundownHeader/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' -import { RundownHeaderTimers } from './RundownHeaderTimers' - -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_old/useRundownPlaylistOperations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader_old/useRundownPlaylistOperations.tsx deleted file mode 100644 index 8f29d6e7ce0..00000000000 --- a/packages/webui/src/client/ui/RundownView/RundownHeader_old/useRundownPlaylistOperations.tsx +++ /dev/null @@ -1,693 +0,0 @@ -import { SerializedUserError, UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' -import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' -import { UserAction } from '@sofie-automation/meteor-lib/dist/userAction' -import { doUserAction } from '../../../lib/clientUserAction' -import { MeteorCall } from '../../../lib/meteorApi' -import { doModalDialog } from '../../../lib/ModalDialog' -import { useTranslation } from 'react-i18next' -import React, { useContext, useEffect, useMemo } from 'react' -import { UserPermissions, UserPermissionsContext } from '../../UserPermissions' -import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' -import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { logger } from '../../../lib/logging' -import * as i18next from 'i18next' -import { NoticeLevel, Notification, NotificationCenter } from '../../../lib/notifications/notifications' -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 { scrollToPartInstance } from '../../../lib/viewPort' -import { hashSingleUseToken } from '../../../lib/lib' -import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' -import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' -import { getCurrentTime } from '../../../lib/systemTime' -import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' -import { REHEARSAL_MARGIN } from '../WarningDisplay' -import { RundownPlaylistTiming } from '@sofie-automation/blueprints-integration' - -class RundownPlaylistOperationsService { - constructor( - public studio: UIStudio, - public playlist: DBRundownPlaylist, - public currentRundown: Rundown | undefined, - public userPermissions: UserPermissions, - public onActivate?: (isRehearsal: boolean) => void - ) {} - - public executeTake(t: i18next.TFunction, e: EventLike): void { - if (!this.userPermissions.studio) return - - if (!this.playlist.activationId) { - const onSuccess = () => { - if (typeof this.onActivate === 'function') this.onActivate(false) - } - const handleResult = (err: any) => { - if (!err) { - onSuccess() - } else if (ClientAPI.isClientResponseError(err)) { - if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { - this.handleAnotherPlaylistActive(t, this.playlist._id, true, err.error, onSuccess) - return false - } - } - } - // ask to activate - doModalDialog({ - title: t('Failed to execute take'), - message: t( - 'The rundown you are trying to execute a take on is inactive, would you like to activate this rundown?' - ), - acceptOnly: false, - warning: true, - yes: t('Activate "On Air"'), - no: t('Cancel'), - discardAsPrimary: true, - onDiscard: () => { - // Do nothing - }, - actions: [ - { - label: t('Activate "Rehearsal"'), - classNames: 'btn-secondary', - on: (e) => { - doUserAction( - t, - e, - UserAction.DEACTIVATE_OTHER_RUNDOWN_PLAYLIST, - async (e, ts) => MeteorCall.userAction.forceResetAndActivate(e, ts, this.playlist._id, true), - handleResult - ) - }, - }, - ], - onAccept: () => { - // nothing - doUserAction( - t, - e, - UserAction.ACTIVATE_RUNDOWN_PLAYLIST, - async (e, ts) => MeteorCall.userAction.activate(e, ts, this.playlist._id, false), - handleResult - ) - }, - }) - } else { - doUserAction(t, e, UserAction.TAKE, async (e, ts) => - MeteorCall.userAction.take(e, ts, this.playlist._id, this.playlist.currentPartInfo?.partInstanceId ?? null) - ) - } - } - - private handleAnotherPlaylistActive( - t: i18next.TFunction, - playlistId: RundownPlaylistId, - rehersal: boolean, - err: SerializedUserError, - clb?: (response: void) => void - ): void { - function handleResult(err: any, response: void) { - if (!err) { - if (typeof clb === 'function') clb(response) - } else { - logger.error(err) - doModalDialog({ - title: t('Failed to activate'), - message: t('Something went wrong, please contact the system administrator if the problem persists.'), - acceptOnly: true, - warning: true, - yes: t('OK'), - onAccept: () => { - // nothing - }, - }) - } - } - - doModalDialog({ - title: t('Another Rundown is Already Active!'), - message: t( - 'The rundown: "{{rundownName}}" will need to be deactivated in order to activate this one.\n\nAre you sure you want to activate this one anyway?', - { - // TODO: this is a bit of a hack, could a better string sent from the server instead? - rundownName: err.userMessage.args?.names ?? '', - } - ), - yes: t('Activate "On Air"'), - no: t('Cancel'), - discardAsPrimary: true, - actions: [ - { - label: t('Activate "Rehearsal"'), - classNames: 'btn-secondary', - on: (e) => { - doUserAction( - t, - e, - UserAction.DEACTIVATE_OTHER_RUNDOWN_PLAYLIST, - async (e, ts) => MeteorCall.userAction.forceResetAndActivate(e, ts, playlistId, rehersal), - handleResult - ) - }, - }, - ], - warning: true, - onAccept: (e) => { - doUserAction( - t, - e, - UserAction.DEACTIVATE_OTHER_RUNDOWN_PLAYLIST, - async (e, ts) => MeteorCall.userAction.forceResetAndActivate(e, ts, playlistId, false), - handleResult - ) - }, - }) - } - - public executeHold(t: i18next.TFunction, e: EventLike): void { - if (this.userPermissions.studio && this.playlist.activationId) { - doUserAction(t, e, UserAction.ACTIVATE_HOLD, async (e, ts) => - MeteorCall.userAction.activateHold(e, ts, this.playlist._id, false) - ) - } - } - - public executeClearQuickLoop(t: i18next.TFunction, e: EventLike) { - if (this.userPermissions.studio && this.playlist.activationId) { - doUserAction(t, e, UserAction.CLEAR_QUICK_LOOP, async (e, ts) => - MeteorCall.userAction.clearQuickLoop(e, ts, this.playlist._id) - ) - } - } - - public executeActivate(t: i18next.TFunction, e: EventLike) { - if ('persist' in e) e.persist() - - if ( - this.userPermissions.studio && - (!this.playlist.activationId || (this.playlist.activationId && this.playlist.rehearsal)) - ) { - const onSuccess = () => { - this.deferFlushAndRewindSegments() - if (typeof this.onActivate === 'function') this.onActivate(false) - } - const doActivate = () => { - doUserAction( - t, - e, - UserAction.ACTIVATE_RUNDOWN_PLAYLIST, - async (e, ts) => MeteorCall.userAction.activate(e, ts, this.playlist._id, false), - (err) => { - if (!err) { - if (typeof this.onActivate === 'function') this.onActivate(false) - } else if (ClientAPI.isClientResponseError(err)) { - if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { - this.handleAnotherPlaylistActive(t, this.playlist._id, false, err.error, () => { - if (typeof this.onActivate === 'function') this.onActivate(false) - }) - return false - } - } - } - ) - } - - const doActivateAndReset = () => { - this.rewindSegments() - doUserAction( - t, - e, - UserAction.RESET_AND_ACTIVATE_RUNDOWN_PLAYLIST, - async (e, ts) => MeteorCall.userAction.resetAndActivate(e, ts, this.playlist._id), - (err) => { - if (!err) { - onSuccess() - } else if (ClientAPI.isClientResponseError(err)) { - if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { - this.handleAnotherPlaylistActive(t, this.playlist._id, false, err.error, onSuccess) - return false - } - } - } - ) - } - - if (!checkRundownTimes(this.playlist.timing).shouldHaveStarted) { - // The broadcast hasn't started yet - doModalDialog({ - title: 'Activate "On Air"', - message: t('Do you want to activate this Rundown?'), - yes: 'Reset and Activate "On Air"', - no: t('Cancel'), - actions: [ - { - label: 'Activate "On Air"', - classNames: 'btn-secondary', - on: () => { - doActivate() // this one activates without resetting - }, - }, - ], - acceptOnly: false, - onAccept: () => { - doUserAction( - t, - e, - UserAction.RESET_AND_ACTIVATE_RUNDOWN_PLAYLIST, - async (e, ts) => MeteorCall.userAction.resetAndActivate(e, ts, this.playlist._id), - (err) => { - if (!err) { - onSuccess() - } else if (ClientAPI.isClientResponseError(err)) { - if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { - this.handleAnotherPlaylistActive(t, this.playlist._id, false, err.error, onSuccess) - return false - } - } - } - ) - }, - }) - } else if (!checkRundownTimes(this.playlist.timing).shouldHaveEnded) { - // The broadcast has started - doActivate() - } else { - // The broadcast has ended, going into active mode is probably not what you want to do - doModalDialog({ - title: 'Activate "On Air"', - message: t('The planned end time has passed, are you sure you want to activate this Rundown?'), - yes: 'Reset and Activate "On Air"', - no: t('Cancel'), - actions: [ - { - label: 'Activate "On Air"', - classNames: 'btn-secondary', - on: () => { - doActivate() // this one activates without resetting - }, - }, - ], - acceptOnly: false, - onAccept: () => { - doActivateAndReset() - }, - }) - } - } - } - - public executeActivateRehearsal = (t: i18next.TFunction, e: EventLike) => { - if ('persist' in e) e.persist() - - if ( - this.userPermissions.studio && - (!this.playlist.activationId || (this.playlist.activationId && !this.playlist.rehearsal)) - ) { - const onSuccess = () => { - if (typeof this.onActivate === 'function') this.onActivate(false) - } - const doActivateRehersal = () => { - doUserAction( - t, - e, - UserAction.ACTIVATE_RUNDOWN_PLAYLIST, - async (e, ts) => MeteorCall.userAction.activate(e, ts, this.playlist._id, true), - (err) => { - if (!err) { - onSuccess() - } else if (ClientAPI.isClientResponseError(err)) { - if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { - this.handleAnotherPlaylistActive(t, this.playlist._id, true, err.error, onSuccess) - return false - } - } - } - ) - } - if (!checkRundownTimes(this.playlist.timing).shouldHaveStarted) { - // The broadcast hasn't started yet - if (!this.playlist.activationId) { - // inactive, do the full preparation: - doUserAction( - t, - e, - UserAction.PREPARE_FOR_BROADCAST, - async (e, ts) => MeteorCall.userAction.prepareForBroadcast(e, ts, this.playlist._id), - (err) => { - if (!err) { - onSuccess() - } else if (ClientAPI.isClientResponseError(err)) { - if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { - this.handleAnotherPlaylistActive(t, this.playlist._id, true, err.error, onSuccess) - return false - } - } - } - ) - } else if (!this.playlist.rehearsal) { - // Active, and not in rehearsal - doModalDialog({ - title: 'Activate "Rehearsal"', - message: t('Are you sure you want to activate Rehearsal Mode?'), - yes: 'Activate "Rehearsal"', - no: t('Cancel'), - onAccept: () => { - doActivateRehersal() - }, - }) - } else { - // Already in rehearsal, do nothing - } - } else { - // The broadcast has started - if (!checkRundownTimes(this.playlist.timing).shouldHaveEnded) { - // We are in the broadcast - doModalDialog({ - title: 'Activate "Rehearsal"', - message: t('Are you sure you want to activate Rehearsal Mode?'), - yes: 'Activate "Rehearsal"', - no: t('Cancel'), - onAccept: () => { - doActivateRehersal() - }, - }) - } else { - // The broadcast has ended - doActivateRehersal() - } - } - } - } - - public executeDeactivate = (t: i18next.TFunction, e: EventLike) => { - if ('persist' in e) e.persist() - - if (this.userPermissions.studio && this.playlist.activationId) { - if (checkRundownTimes(this.playlist.timing).shouldHaveStarted) { - if (this.playlist.rehearsal) { - // We're in rehearsal mode - doUserAction(t, e, UserAction.DEACTIVATE_RUNDOWN_PLAYLIST, async (e, ts) => - MeteorCall.userAction.deactivate(e, ts, this.playlist._id) - ) - } else { - doModalDialog({ - title: 'Deactivate "On Air"', - message: t('Are you sure you want to deactivate this rundown?\n(This will clear the outputs.)'), - warning: true, - yes: t('Deactivate "On Air"'), - no: t('Cancel'), - onAccept: () => { - doUserAction(t, e, UserAction.DEACTIVATE_RUNDOWN_PLAYLIST, async (e, ts) => - MeteorCall.userAction.deactivate(e, ts, this.playlist._id) - ) - }, - }) - } - } else { - // Do it right away - doUserAction(t, e, UserAction.DEACTIVATE_RUNDOWN_PLAYLIST, async (e, ts) => - MeteorCall.userAction.deactivate(e, ts, this.playlist._id) - ) - } - } - } - - public executeActivateAdlibTesting = (t: i18next.TFunction, e: EventLike) => { - if ('persist' in e) e.persist() - - if ( - this.userPermissions.studio && - this.studio.settings.allowAdlibTestingSegment && - this.playlist.activationId && - this.currentRundown - ) { - const rundownId = this.currentRundown._id - doUserAction(t, e, UserAction.ACTIVATE_ADLIB_TESTING, async (e, ts) => - MeteorCall.userAction.activateAdlibTestingMode(e, ts, this.playlist._id, rundownId) - ) - } - } - - public executeResetRundown = (t: i18next.TFunction, e: EventLike) => { - if ('persist' in e) e.persist() - - const doReset = () => { - this.rewindSegments() // Do a rewind right away - doUserAction( - t, - e, - UserAction.RESET_RUNDOWN_PLAYLIST, - async (e, ts) => MeteorCall.userAction.resetRundownPlaylist(e, ts, this.playlist._id), - () => { - this.deferFlushAndRewindSegments() - } - ) - } - if (this.playlist.activationId && !this.playlist.rehearsal && !this.studio.settings.allowRundownResetOnAir) { - // The rundown is active and not in rehearsal - doModalDialog({ - title: 'Reset Rundown', - message: t('The rundown can not be reset while it is active'), - onAccept: () => { - // nothing - }, - acceptOnly: true, - yes: 'OK', - }) - } else { - doReset() - } - } - - public executeReloadRundownPlaylist = (t: i18next.TFunction, e: EventLike) => { - if (!this.userPermissions.studio) return - - doUserAction( - t, - e, - UserAction.RELOAD_RUNDOWN_PLAYLIST_DATA, - async (e, ts) => MeteorCall.userAction.resyncRundownPlaylist(e, ts, this.playlist._id), - (err, reloadResponse) => { - if (!err && reloadResponse) { - if (!handleRundownPlaylistReloadResponse(t, this.userPermissions, reloadResponse)) { - if (this.playlist && this.playlist.nextPartInfo) { - scrollToPartInstance(this.playlist.nextPartInfo.partInstanceId).catch((error) => { - if (!error.toString().match(/another scroll/)) console.warn(error) - }) - } - } - } - } - ) - } - - public executeTakeRundownSnapshot = (t: i18next.TFunction, e: EventLike) => { - if (!this.userPermissions.studio) return - - const doneMessage = t('A snapshot of the current Running\xa0Order has been created for troubleshooting.') - doUserAction( - t, - e, - UserAction.CREATE_SNAPSHOT_FOR_DEBUG, - async (e, ts) => - MeteorCall.system.generateSingleUseToken().then(async (tokenResponse) => { - if (ClientAPI.isClientResponseError(tokenResponse)) { - throw UserError.fromSerialized(tokenResponse.error) - } else if (!tokenResponse.result) { - throw new Error(`Internal Error: No token.`) - } - return MeteorCall.userAction.storeRundownSnapshot( - e, - ts, - hashSingleUseToken(tokenResponse.result), - this.playlist._id, - 'Taken by user', - false - ) - }), - () => { - NotificationCenter.push( - new Notification( - undefined, - NoticeLevel.NOTIFICATION, - doneMessage, - 'userAction', - undefined, - false, - undefined, - undefined, - 5000 - ) - ) - return false - }, - doneMessage - ) - } - - public executeActivateRundown = (t: i18next.TFunction, e: EventLike) => { - // Called from the ModalDialog, 1 minute before broadcast starts - if (!this.userPermissions.studio) return - - this.rewindSegments() // Do a rewind right away - - doUserAction( - t, - e, - UserAction.ACTIVATE_RUNDOWN_PLAYLIST, - async (e, ts) => MeteorCall.userAction.activate(e, ts, this.playlist._id, false), - (err) => { - if (!err) { - if (typeof this.onActivate === 'function') this.onActivate(false) - } else if (ClientAPI.isClientResponseError(err)) { - if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { - this.handleAnotherPlaylistActive(t, this.playlist._id, false, err.error, () => { - if (typeof this.onActivate === 'function') this.onActivate(false) - }) - return false - } - } - } - ) - } - - public executeResetAndActivateRundown = (t: i18next.TFunction, e: EventLike) => { - // Called from the ModalDialog, 1 minute before broadcast starts - if (!this.userPermissions.studio) return - - this.rewindSegments() // Do a rewind right away - - doUserAction( - t, - e, - UserAction.RESET_AND_ACTIVATE_RUNDOWN_PLAYLIST, - async (e, ts) => MeteorCall.userAction.resetAndActivate(e, ts, this.playlist._id), - (err) => { - if (!err) { - this.deferFlushAndRewindSegments() - if (typeof this.onActivate === 'function') this.onActivate(false) - } - } - ) - } - - private deferFlushAndRewindSegments = () => { - // Do a rewind later, when the UI has updated - Meteor.defer(() => { - Tracker.flush() - Meteor.setTimeout(() => { - this.rewindSegments() - RundownViewEventBus.emit(RundownViewEvents.GO_TO_TOP) - }, 500) - }) - } - - private rewindSegments = () => { - RundownViewEventBus.emit(RundownViewEvents.REWIND_SEGMENTS) - } -} - -export interface RundownPlaylistOperations { - take: (e: EventLike) => void - hold: (e: EventLike) => void - clearQuickLoop: (e: EventLike) => void - activate: (e: EventLike) => void - activateRehearsal: (e: EventLike) => void - deactivate: (e: EventLike) => void - activateAdlibTesting: (e: EventLike) => void - resetRundown: (e: EventLike) => void - reloadRundownPlaylist: (e: EventLike) => void - takeRundownSnapshot: (e: EventLike) => void - activateRundown: (e: EventLike) => void - resetAndActivateRundown: (e: EventLike) => void -} - -const RundownPlaylistOperationsContext = React.createContext(null) - -export function RundownPlaylistOperationsContextProvider({ - children, - currentRundown, - playlist, - studio, - onActivate, -}: React.PropsWithChildren<{ - studio: UIStudio - playlist: DBRundownPlaylist - currentRundown: Rundown | undefined - onActivate?: (isRehearsal: boolean) => void -}>): React.JSX.Element | null { - const { t } = useTranslation() - - const userPermissions = useContext(UserPermissionsContext) - - const service = useMemo( - () => new RundownPlaylistOperationsService(studio, playlist, currentRundown, userPermissions, onActivate), - [] - ) - - useEffect(() => { - service.studio = studio - service.playlist = playlist - service.currentRundown = currentRundown - service.userPermissions = userPermissions - service.onActivate = onActivate - }, [currentRundown, playlist, studio, userPermissions, onActivate]) - - const apiObject = useMemo( - () => - ({ - take: (e) => service.executeTake(t, e), - hold: (e) => service.executeHold(t, e), - clearQuickLoop: (e) => service.executeClearQuickLoop(t, e), - activate: (e) => service.executeActivate(t, e), - activateRehearsal: (e) => service.executeActivateRehearsal(t, e), - deactivate: (e) => service.executeDeactivate(t, e), - activateAdlibTesting: (e) => service.executeActivateAdlibTesting(t, e), - resetRundown: (e) => service.executeResetRundown(t, e), - reloadRundownPlaylist: (e) => service.executeReloadRundownPlaylist(t, e), - takeRundownSnapshot: (e) => service.executeTakeRundownSnapshot(t, e), - activateRundown: (e) => service.executeActivateRundown(t, e), - resetAndActivateRundown: (e) => service.executeResetAndActivateRundown(t, e), - }) satisfies RundownPlaylistOperations, - [service, t] - ) - - return ( - {children} - ) -} - -export function useRundownPlaylistOperations(): RundownPlaylistOperations { - const context = useContext(RundownPlaylistOperationsContext) - - if (!context) - throw new Error('This component must be a child of a `RundownPlaylistOperationsContextProvider` component.') - - return context -} - -interface RundownTimesInfo { - shouldHaveStarted: boolean - willShortlyStart: boolean - shouldHaveEnded: boolean -} - -type EventLike = - | { - persist(): void - } - | {} - -export function checkRundownTimes(playlistTiming: RundownPlaylistTiming): RundownTimesInfo { - const currentTime = getCurrentTime() - - const shouldHaveEnded = - currentTime > - (PlaylistTiming.getExpectedStart(playlistTiming) || 0) + (PlaylistTiming.getExpectedDuration(playlistTiming) || 0) - - return { - shouldHaveStarted: currentTime > (PlaylistTiming.getExpectedStart(playlistTiming) || 0), - willShortlyStart: - !shouldHaveEnded && currentTime > (PlaylistTiming.getExpectedStart(playlistTiming) || 0) - REHEARSAL_MARGIN, - shouldHaveEnded, - } -} From 73042fb13bdca93143ccb557b32b3558d905a010 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 12 Mar 2026 13:59:51 +0000 Subject: [PATCH 251/291] fix: Correct duration calculations in RundownHeader components by using remainingPlaylistDuration --- .../RundownHeader/RundownHeaderDurations.tsx | 21 ++----------------- .../RundownHeaderExpectedEnd.tsx | 18 +++++----------- 2 files changed, 7 insertions(+), 32 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx index c543acea2e8..63cc538125e 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next' import { Countdown } from './Countdown' import { useTiming } from '../RundownTiming/withTiming' import { RundownUtils } from '../../../lib/rundown' -import { getRemainingDurationFromCurrentPart } from './remainingDuration' export function RundownHeaderDurations({ playlist, @@ -18,24 +17,8 @@ export function RundownHeaderDurations({ const expectedDuration = PlaylistTiming.getExpectedDuration(playlist.timing) - const now = timingDurations.currentTime ?? Date.now() - const currentPartInstanceId = playlist.currentPartInfo?.partInstanceId - - let estDuration: number | null = null - if (currentPartInstanceId && timingDurations.partStartsAt && timingDurations.partExpectedDurations) { - const remaining = getRemainingDurationFromCurrentPart( - currentPartInstanceId, - timingDurations.partStartsAt, - timingDurations.partExpectedDurations - ) - if (remaining != null) { - const elapsed = - playlist.startedPlayback == null - ? (timingDurations.asDisplayedPlaylistDuration ?? 0) - : now - playlist.startedPlayback - estDuration = elapsed + remaining - } - } + // Use remainingPlaylistDuration which includes current part's remaining time + const estDuration = timingDurations.remainingPlaylistDuration if (expectedDuration == null && estDuration == null) return null diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx index fe90f5b80ac..5268ad04c66 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -3,7 +3,6 @@ import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTi import { useTranslation } from 'react-i18next' import { Countdown } from './Countdown' import { useTiming } from '../RundownTiming/withTiming' -import { getRemainingDurationFromCurrentPart } from './remainingDuration' export function RundownHeaderExpectedEnd({ playlist, @@ -18,18 +17,11 @@ export function RundownHeaderExpectedEnd({ const expectedEnd = PlaylistTiming.getExpectedEnd(playlist.timing) const now = timingDurations.currentTime ?? Date.now() - let estEnd: number | null = null - const currentPartInstanceId = playlist.currentPartInfo?.partInstanceId - if (currentPartInstanceId && timingDurations.partStartsAt && timingDurations.partExpectedDurations) { - const remaining = getRemainingDurationFromCurrentPart( - currentPartInstanceId, - timingDurations.partStartsAt, - timingDurations.partExpectedDurations - ) - if (remaining != null && remaining > 0) { - estEnd = now + remaining - } - } + // Use remainingPlaylistDuration which includes current part's remaining time + const estEnd = + timingDurations.remainingPlaylistDuration != null && timingDurations.remainingPlaylistDuration > 0 + ? now + timingDurations.remainingPlaylistDuration + : null if (!expectedEnd && !estEnd) return null From 1125a22594a6f6cf6b6ecdeffd482baa756a43bf Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 12 Mar 2026 14:00:17 +0000 Subject: [PATCH 252/291] Linting improvements --- .../ui/RundownView/RundownHeader/RundownHeaderDurations.tsx | 4 ++-- .../ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx index 63cc538125e..f714a7e0255 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx @@ -24,12 +24,12 @@ export function RundownHeaderDurations({ return (
- {expectedDuration != null ? ( + {expectedDuration ? ( {RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, true, true)} ) : null} - {!simplified && estDuration != null ? ( + {!simplified && estDuration ? ( {RundownUtils.formatDiffToTimecode(estDuration, false, true, true, true, true)} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx index 5268ad04c66..ccbc68ccfd5 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -8,8 +8,8 @@ export function RundownHeaderExpectedEnd({ playlist, simplified, }: { - playlist: DBRundownPlaylist - simplified?: boolean + readonly playlist: DBRundownPlaylist + readonly simplified?: boolean }): JSX.Element | null { const { t } = useTranslation() const timingDurations = useTiming() From 92c59fbda95280081458472af4450a4bc26f7649 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Thu, 12 Mar 2026 15:52:28 +0100 Subject: [PATCH 253/291] chore: Changed the Rehearsal background to striped grey, and added labels for Deactivated and Rehearsal. --- .../RundownHeader/RundownHeader.scss | 24 +++++++++++++++++-- .../RundownHeader/RundownHeader.tsx | 5 ++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 7575cd85886..93447f105e8 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -43,7 +43,15 @@ } &.rehearsal { - background: $color-header-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; } } @@ -62,6 +70,18 @@ flex: 1; } + .rundown-header__not-on-air-label { + @extend %hoverable-label; + opacity: 1; + color: #fff; + font-size: 0.8em; + letter-spacing: 0.02em; + + margin-left: 0.075em; + margin-right: 0.25em; + white-space: nowrap; + } + .rundown-header__right { display: flex; align-items: center; @@ -188,7 +208,7 @@ .rundown-header__clocks-diff__label { @extend .rundown-header__hoverable-label; - font-size: 0.7em; + font-size: 0.75em; opacity: 0.6; font-variation-settings: 'wdth' 25, diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index 52dd6add17c..a7770f743b4 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -64,6 +64,11 @@ export function RundownHeader({
+ {playlist.activationId && playlist.rehearsal && ( + {t('REHEARSAL')} + )} + {!playlist.activationId && {t('DEACTIVATED')}} {playlist.currentPartInfo && (
Date: Thu, 12 Mar 2026 16:10:22 +0100 Subject: [PATCH 254/291] fix: more explicit truthy check allow keeping 0 dur timers visible --- .../ui/RundownView/RundownHeader/RundownHeaderDurations.tsx | 4 ++-- .../ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx | 4 ++-- .../RundownView/RundownHeader/RundownHeaderPlannedStart.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx index f714a7e0255..ab981c7f958 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx @@ -24,12 +24,12 @@ export function RundownHeaderDurations({ return (
- {expectedDuration ? ( + {expectedDuration !== undefined ? ( {RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, true, true)} ) : null} - {!simplified && estDuration ? ( + {!simplified && estDuration !== undefined ? ( {RundownUtils.formatDiffToTimecode(estDuration, false, true, true, true, true)} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx index ccbc68ccfd5..077f3129a7f 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -27,10 +27,10 @@ export function RundownHeaderExpectedEnd({ return (
- {expectedEnd ? ( + {expectedEnd !== undefined ? ( ) : null} - {!simplified && estEnd ? ( + {!simplified && 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 index a9ea781de12..af24aa612b5 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx @@ -25,7 +25,7 @@ export function RundownHeaderPlannedStart({
{!simplified && - (playlist.startedPlayback ? ( + (playlist.startedPlayback !== undefined ? ( ) : ( From 4afed0e0723c7ed1d70d224eee5bd96632e0e7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Thu, 12 Mar 2026 16:10:44 +0100 Subject: [PATCH 255/291] chore: cleanup --- packages/webui/src/client/lib/rundownTiming.ts | 2 +- .../src/client/ui/RundownView/RundownHeader/RundownHeader.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/lib/rundownTiming.ts b/packages/webui/src/client/lib/rundownTiming.ts index a273b072ed2..81277ec7399 100644 --- a/packages/webui/src/client/lib/rundownTiming.ts +++ b/packages/webui/src/client/lib/rundownTiming.ts @@ -474,7 +474,7 @@ export class RundownTimingCalculator { // partExpectedDuration is affected by displayGroups, and if it hasn't played yet then it shouldn't // add any duration to the "remaining" time pool remainingRundownDuration += - calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0 + calculatePartInstanceExpectedDurationWithTransition(partInstance) || 0 // item is onAir right now, and it's is currently shorter than expectedDuration } else if ( lastStartedPlayback && diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index a7770f743b4..d0dc8a69971 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -98,7 +98,7 @@ export function RundownHeader({ {rundownCount > 1 ? ( {playlist.name} ) : ( - {(currentRundown ?? firstRundown)?.name} + {currentRundown?.name} )}
From 0192c01bfa3cee3c34abad86fd5f7b099cefddac Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:27:05 +0100 Subject: [PATCH 256/291] WIP: add open state to menu icon in top bar --- .../RundownHeader/RundownContextMenu.tsx | 70 ++++++++----- .../RundownHeader/RundownHeader.scss | 6 ++ .../RundownHeader/RundownHeader.tsx | 98 +++++++++++-------- .../RundownHeaderPlannedStart.tsx | 2 +- 4 files changed, 106 insertions(+), 70 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx index 3941357cffd..d564976e6c9 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx @@ -3,10 +3,10 @@ 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 } from '@jstarpl/react-contextmenu' +import { ContextMenu, MenuItem, ContextMenuTrigger, hideMenu, showMenu } from '@jstarpl/react-contextmenu' import { contextMenuHoldToDisplayTime, useRundownViewEventBusListener } from '../../../lib/lib' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faBars } from '@fortawesome/free-solid-svg-icons' +import { faBars, faTimes } from '@fortawesome/free-solid-svg-icons' import { ActivateRundownPlaylistEvent, DeactivateRundownPlaylistEvent, @@ -25,6 +25,8 @@ interface RundownContextMenuProps { playlist: DBRundownPlaylist studio: UIStudio firstRundown: Rundown | undefined + onShow?: () => void + onHide?: () => void } /** @@ -32,7 +34,13 @@ interface RundownContextMenuProps { * 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 }: Readonly): JSX.Element { +export function RundownContextMenu({ + playlist, + studio, + firstRundown, + onShow, + onHide, +}: Readonly): JSX.Element { const { t } = useTranslation() const userPermissions = useContext(UserPermissionsContext) const operations = useRundownPlaylistOperations() @@ -77,7 +85,7 @@ export function RundownContextMenu({ playlist, studio, firstRundown }: Readonly< return ( - +
{playlist && playlist.name}
{userPermissions.studio ? ( @@ -147,33 +155,43 @@ export function RundownHeaderContextMenuTrigger({ children }: Readonly void }>): JSX.Element { const { t } = useTranslation() const buttonRef = useRef(null) - const handleClick = useCallback((e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() - - // Dispatch a custom contextmenu event - if (buttonRef.current) { - const rect = buttonRef.current.getBoundingClientRect() - const event = new MouseEvent('contextmenu', { - view: globalThis as unknown as Window, - bubbles: true, - cancelable: true, - clientX: rect.left, - clientY: rect.bottom + 5, - button: 2, - buttons: 2, - }) - buttonRef.current.dispatchEvent(event) - } - }, []) + const handleClick = useCallback( + (e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() + + 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, + }) + } + }, + [isOpen] + ) return ( - ) } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 93447f105e8..214dd9da69a 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -68,6 +68,12 @@ display: flex; align-items: center; flex: 1; + + .rundown-header__left-context-menu-wrapper { + display: flex; + align-items: center; + height: 100%; + } } .rundown-header__not-on-air-label { diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx index d0dc8a69971..d1ae385e2e1 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import ClassNames from 'classnames' @@ -46,10 +46,19 @@ export function RundownHeader({ }: IRundownHeaderProps): JSX.Element { const { t } = useTranslation() const [simplified, setSimplified] = useState(false) + const [isMenuOpen, setIsMenuOpen] = useState(false) + + const onMenuClose = useCallback(() => setIsMenuOpen(false), [setIsMenuOpen]) return ( <> - + setIsMenuOpen(true)} + onHide={() => setIsMenuOpen(false)} + /> - -
-
- - {playlist.activationId && playlist.rehearsal && ( - {t('REHEARSAL')} - )} - {!playlist.activationId && {t('DEACTIVATED')}} - {playlist.currentPartInfo && ( -
- - - {t('On Air')} - +
+ + +
+ {playlist.activationId && playlist.rehearsal && ( + {t('REHEARSAL')} + )} + {!playlist.activationId && {t('DEACTIVATED')}} + {playlist.currentPartInfo && ( +
+ - - -
- )} - -
+ + {t('On Air')} + + + +
+ )} + +
+ +
+
@@ -98,28 +110,28 @@ export function RundownHeader({ {rundownCount > 1 ? ( {playlist.name} ) : ( - {currentRundown?.name} + {(currentRundown ?? firstRundown)?.name} )}
+
-
- - - - -
+
+ + + +
- +
) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx index af24aa612b5..e3384f4de14 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx @@ -28,7 +28,7 @@ export function RundownHeaderPlannedStart({ (playlist.startedPlayback !== undefined ? ( ) : ( - + {diff >= 0 && '-'} {RundownUtils.formatDiffToTimecode(Math.abs(diff), false, false, true, true, true)} From fce270b9d104df74ba5d85c2dcb21723e741efaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Thu, 12 Mar 2026 16:31:09 +0100 Subject: [PATCH 257/291] fix: allow Plan. end to show even at the end of the show --- .../ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx index 077f3129a7f..df93d376c63 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -19,7 +19,7 @@ export function RundownHeaderExpectedEnd({ // Use remainingPlaylistDuration which includes current part's remaining time const estEnd = - timingDurations.remainingPlaylistDuration != null && timingDurations.remainingPlaylistDuration > 0 + timingDurations.remainingPlaylistDuration !== undefined ? now + timingDurations.remainingPlaylistDuration : null From a72005567bae8756b09d2a38ad890d76b33651cd Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 12 Mar 2026 15:35:20 +0000 Subject: [PATCH 258/291] Remove no longer needed file --- .../RundownHeader/remainingDuration.ts | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/remainingDuration.ts diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/remainingDuration.ts b/packages/webui/src/client/ui/RundownView/RundownHeader/remainingDuration.ts deleted file mode 100644 index b54bb6c74fc..00000000000 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/remainingDuration.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { PartInstanceId } from '@sofie-automation/corelib/dist/dataModel/Ids' -import { unprotectString } from '@sofie-automation/corelib/dist/protectedString' - -/** - * Compute the sum of expected durations of all parts after the current part. - * Uses partStartsAt to determine ordering and partExpectedDurations for the values. - * Returns 0 if the current part can't be found or there are no future parts. - */ -export function getRemainingDurationFromCurrentPart( - currentPartInstanceId: PartInstanceId, - partStartsAt: Record, - partExpectedDurations: Record -): number | null { - const currentKey = unprotectString(currentPartInstanceId) - const currentStartsAt = partStartsAt[currentKey] - - if (currentStartsAt == null) return null - - let remaining = 0 - for (const [partId, startsAt] of Object.entries(partStartsAt)) { - if (startsAt > currentStartsAt) { - remaining += partExpectedDurations[partId] ?? 0 - } - } - return remaining -} From b4d9871b83ec823a5dd044c4abc8598924f4b312 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 12 Mar 2026 15:35:58 +0000 Subject: [PATCH 259/291] Fix overdeletion of _old files --- .../webui/src/client/ui/RundownList/util.ts | 2 +- .../RundownHeader/RundownContextMenu.tsx | 2 +- .../RundownHeader/RundownReloadResponse.ts | 177 +++++ .../useRundownPlaylistOperations.tsx | 693 ++++++++++++++++++ .../client/ui/RundownView/RundownNotifier.tsx | 2 +- .../RundownViewContextProviders.tsx | 2 +- 6 files changed, 874 insertions(+), 4 deletions(-) create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/RundownReloadResponse.ts create mode 100644 packages/webui/src/client/ui/RundownView/RundownHeader/useRundownPlaylistOperations.tsx diff --git a/packages/webui/src/client/ui/RundownList/util.ts b/packages/webui/src/client/ui/RundownList/util.ts index 7a492e91f33..cec30f2fd8d 100644 --- a/packages/webui/src/client/ui/RundownList/util.ts +++ b/packages/webui/src/client/ui/RundownList/util.ts @@ -4,7 +4,7 @@ import { doModalDialog } from '../../lib/ModalDialog.js' import { doUserAction, UserAction } from '../../lib/clientUserAction.js' import { MeteorCall } from '../../lib/meteorApi.js' import { TFunction } from 'i18next' -import { handleRundownReloadResponse } from '../RundownView/RundownHeader_old/RundownReloadResponse.js' +import { handleRundownReloadResponse } from '../RundownView/RundownHeader/RundownReloadResponse.js' import { RundownId, RundownLayoutId, diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx index d564976e6c9..1d2acbd09ba 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx @@ -16,7 +16,7 @@ import { import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' import { UserPermissionsContext } from '../../UserPermissions' import * as RundownResolver from '../../../lib/RundownResolver' -import { checkRundownTimes, useRundownPlaylistOperations } from '../RundownHeader_old/useRundownPlaylistOperations' +import { checkRundownTimes, useRundownPlaylistOperations } from './useRundownPlaylistOperations.js' import { reloadRundownPlaylistClick } from '../RundownNotifier' export const RUNDOWN_CONTEXT_MENU_ID = 'rundown-context-menu' diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownReloadResponse.ts b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownReloadResponse.ts new file mode 100644 index 00000000000..a858db9caf2 --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownReloadResponse.ts @@ -0,0 +1,177 @@ +import { RundownPlaylists, Rundowns } from '../../../collections' +import { + ReloadRundownPlaylistResponse, + TriggerReloadDataResponse, +} from '@sofie-automation/meteor-lib/dist/api/userActions' +import _ from 'underscore' +import { RundownPlaylistCollectionUtil } from '../../../collections/rundownPlaylistUtil' +import * as i18next from 'i18next' +import { UserPermissions } from '../../UserPermissions' +import { NoticeLevel, Notification, NotificationCenter } from '../../../lib/notifications/notifications' +import { RundownId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { getRundownNrcsName } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { UserAction } from '@sofie-automation/meteor-lib/dist/userAction' +import { Tracker } from 'meteor/tracker' +import { doUserAction } from '../../../lib/clientUserAction' +import { MeteorCall } from '../../../lib/meteorApi' +import { doModalDialog } from '../../../lib/ModalDialog' + +export function handleRundownPlaylistReloadResponse( + t: i18next.TFunction, + userPermissions: Readonly, + result: ReloadRundownPlaylistResponse +): boolean { + const rundownsInNeedOfHandling = result.rundownsResponses.filter( + (r) => r.response === TriggerReloadDataResponse.MISSING + ) + const firstRundownId = _.first(rundownsInNeedOfHandling)?.rundownId + let allRundownsAffected = false + + if (firstRundownId) { + const firstRundown = Rundowns.findOne(firstRundownId) + const playlist = RundownPlaylists.findOne(firstRundown?.playlistId) + const allRundownIds = playlist ? RundownPlaylistCollectionUtil.getRundownUnorderedIDs(playlist) : [] + if ( + allRundownIds.length > 0 && + _.difference( + allRundownIds, + rundownsInNeedOfHandling.map((r) => r.rundownId) + ).length === 0 + ) { + allRundownsAffected = true + } + } + + const actionsTaken: RundownReloadResponseUserAction[] = [] + function onActionTaken(action: RundownReloadResponseUserAction): void { + actionsTaken.push(action) + if (actionsTaken.length === rundownsInNeedOfHandling.length) { + // the user has taken action on all of the missing rundowns + if (allRundownsAffected && actionsTaken.filter((actionTaken) => actionTaken !== 'removed').length === 0) { + // all rundowns in the playlist were affected and all of them were removed + // we redirect to the Lobby + window.location.assign('/') + } + } + } + + const handled = rundownsInNeedOfHandling.map((r) => + handleRundownReloadResponse(t, userPermissions, r.rundownId, r.response, onActionTaken) + ) + return handled.reduce((previousValue, value) => previousValue || value, false) +} + +export type RundownReloadResponseUserAction = 'removed' | 'unsynced' | 'error' + +export function handleRundownReloadResponse( + t: i18next.TFunction, + userPermissions: Readonly, + rundownId: RundownId, + result: TriggerReloadDataResponse, + clb?: (action: RundownReloadResponseUserAction) => void +): boolean { + let hasDoneSomething = false + + if (result === TriggerReloadDataResponse.MISSING) { + const rundown = Rundowns.findOne(rundownId) + const playlist = RundownPlaylists.findOne(rundown?.playlistId) + + hasDoneSomething = true + const notification = new Notification( + undefined, + NoticeLevel.CRITICAL, + t( + 'Rundown {{rundownName}} in Playlist {{playlistName}} is missing in the data from {{nrcsName}}. You can either leave it in Sofie and mark it as Unsynced or remove the rundown from Sofie. What do you want to do?', + { + nrcsName: getRundownNrcsName(rundown), + rundownName: rundown?.name || t('(Unknown rundown)'), + playlistName: playlist?.name || t('(Unknown playlist)'), + } + ), + 'userAction', + undefined, + true, + [ + // actions: + { + label: t('Leave Unsynced'), + type: 'default', + disabled: !userPermissions.studio, + action: () => { + doUserAction( + t, + 'Missing rundown action', + UserAction.UNSYNC_RUNDOWN, + async (e, ts) => MeteorCall.userAction.unsyncRundown(e, ts, rundownId), + (err) => { + if (!err) { + notificationHandle.stop() + clb?.('unsynced') + } else { + clb?.('error') + } + } + ) + }, + }, + { + label: t('Remove'), + type: 'default', + action: () => { + doModalDialog({ + title: t('Remove rundown'), + message: t( + 'Do you really want to remove just the rundown "{{rundownName}}" in the playlist {{playlistName}} from Sofie? \n\nThis cannot be undone!', + { + rundownName: rundown?.name || 'N/A', + playlistName: playlist?.name || 'N/A', + } + ), + onAccept: () => { + // nothing + doUserAction( + t, + 'Missing rundown action', + UserAction.REMOVE_RUNDOWN, + async (e, ts) => MeteorCall.userAction.removeRundown(e, ts, rundownId), + (err) => { + if (!err) { + notificationHandle.stop() + clb?.('removed') + } else { + clb?.('error') + } + } + ) + }, + }) + }, + }, + ] + ) + const notificationHandle = NotificationCenter.push(notification) + + if (rundown) { + // This allows the semi-modal dialog above to be closed automatically, once the rundown stops existing + // for whatever reason + const comp = Tracker.autorun(() => { + const rundown = Rundowns.findOne(rundownId, { + fields: { + _id: 1, + orphaned: 1, + }, + }) + // we should hide the message + if (!rundown || !rundown.orphaned) { + notificationHandle.stop() + } + }) + notification.on('dropped', () => { + // clean up the reactive computation above when the notification is closed. Will be also executed by + // the notificationHandle.stop() above, so the Tracker.autorun will clean up after itself as well. + comp.stop() + }) + } + } + return hasDoneSomething +} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/useRundownPlaylistOperations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/useRundownPlaylistOperations.tsx new file mode 100644 index 00000000000..c4d5a3b64db --- /dev/null +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/useRundownPlaylistOperations.tsx @@ -0,0 +1,693 @@ +import { SerializedUserError, UserError, UserErrorMessage } from '@sofie-automation/corelib/dist/error' +import { ClientAPI } from '@sofie-automation/meteor-lib/dist/api/client' +import { UserAction } from '@sofie-automation/meteor-lib/dist/userAction' +import { doUserAction } from '../../../lib/clientUserAction' +import { MeteorCall } from '../../../lib/meteorApi' +import { doModalDialog } from '../../../lib/ModalDialog' +import { useTranslation } from 'react-i18next' +import React, { useContext, useEffect, useMemo } from 'react' +import { UserPermissions, UserPermissionsContext } from '../../UserPermissions' +import { DBRundownPlaylist } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist' +import { RundownPlaylistId } from '@sofie-automation/corelib/dist/dataModel/Ids' +import { logger } from '../../../lib/logging' +import * as i18next from 'i18next' +import { NoticeLevel, Notification, NotificationCenter } from '../../../lib/notifications/notifications' +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.js' +import { scrollToPartInstance } from '../../../lib/viewPort' +import { hashSingleUseToken } from '../../../lib/lib' +import { Rundown } from '@sofie-automation/corelib/dist/dataModel/Rundown' +import { UIStudio } from '@sofie-automation/meteor-lib/dist/api/studios' +import { getCurrentTime } from '../../../lib/systemTime' +import { PlaylistTiming } from '@sofie-automation/corelib/dist/playout/rundownTiming' +import { REHEARSAL_MARGIN } from '../WarningDisplay' +import { RundownPlaylistTiming } from '@sofie-automation/blueprints-integration' + +class RundownPlaylistOperationsService { + constructor( + public studio: UIStudio, + public playlist: DBRundownPlaylist, + public currentRundown: Rundown | undefined, + public userPermissions: UserPermissions, + public onActivate?: (isRehearsal: boolean) => void + ) {} + + public executeTake(t: i18next.TFunction, e: EventLike): void { + if (!this.userPermissions.studio) return + + if (!this.playlist.activationId) { + const onSuccess = () => { + if (typeof this.onActivate === 'function') this.onActivate(false) + } + const handleResult = (err: any) => { + if (!err) { + onSuccess() + } else if (ClientAPI.isClientResponseError(err)) { + if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { + this.handleAnotherPlaylistActive(t, this.playlist._id, true, err.error, onSuccess) + return false + } + } + } + // ask to activate + doModalDialog({ + title: t('Failed to execute take'), + message: t( + 'The rundown you are trying to execute a take on is inactive, would you like to activate this rundown?' + ), + acceptOnly: false, + warning: true, + yes: t('Activate "On Air"'), + no: t('Cancel'), + discardAsPrimary: true, + onDiscard: () => { + // Do nothing + }, + actions: [ + { + label: t('Activate "Rehearsal"'), + classNames: 'btn-secondary', + on: (e) => { + doUserAction( + t, + e, + UserAction.DEACTIVATE_OTHER_RUNDOWN_PLAYLIST, + async (e, ts) => MeteorCall.userAction.forceResetAndActivate(e, ts, this.playlist._id, true), + handleResult + ) + }, + }, + ], + onAccept: () => { + // nothing + doUserAction( + t, + e, + UserAction.ACTIVATE_RUNDOWN_PLAYLIST, + async (e, ts) => MeteorCall.userAction.activate(e, ts, this.playlist._id, false), + handleResult + ) + }, + }) + } else { + doUserAction(t, e, UserAction.TAKE, async (e, ts) => + MeteorCall.userAction.take(e, ts, this.playlist._id, this.playlist.currentPartInfo?.partInstanceId ?? null) + ) + } + } + + private handleAnotherPlaylistActive( + t: i18next.TFunction, + playlistId: RundownPlaylistId, + rehersal: boolean, + err: SerializedUserError, + clb?: (response: void) => void + ): void { + function handleResult(err: any, response: void) { + if (!err) { + if (typeof clb === 'function') clb(response) + } else { + logger.error(err) + doModalDialog({ + title: t('Failed to activate'), + message: t('Something went wrong, please contact the system administrator if the problem persists.'), + acceptOnly: true, + warning: true, + yes: t('OK'), + onAccept: () => { + // nothing + }, + }) + } + } + + doModalDialog({ + title: t('Another Rundown is Already Active!'), + message: t( + 'The rundown: "{{rundownName}}" will need to be deactivated in order to activate this one.\n\nAre you sure you want to activate this one anyway?', + { + // TODO: this is a bit of a hack, could a better string sent from the server instead? + rundownName: err.userMessage.args?.names ?? '', + } + ), + yes: t('Activate "On Air"'), + no: t('Cancel'), + discardAsPrimary: true, + actions: [ + { + label: t('Activate "Rehearsal"'), + classNames: 'btn-secondary', + on: (e) => { + doUserAction( + t, + e, + UserAction.DEACTIVATE_OTHER_RUNDOWN_PLAYLIST, + async (e, ts) => MeteorCall.userAction.forceResetAndActivate(e, ts, playlistId, rehersal), + handleResult + ) + }, + }, + ], + warning: true, + onAccept: (e) => { + doUserAction( + t, + e, + UserAction.DEACTIVATE_OTHER_RUNDOWN_PLAYLIST, + async (e, ts) => MeteorCall.userAction.forceResetAndActivate(e, ts, playlistId, false), + handleResult + ) + }, + }) + } + + public executeHold(t: i18next.TFunction, e: EventLike): void { + if (this.userPermissions.studio && this.playlist.activationId) { + doUserAction(t, e, UserAction.ACTIVATE_HOLD, async (e, ts) => + MeteorCall.userAction.activateHold(e, ts, this.playlist._id, false) + ) + } + } + + public executeClearQuickLoop(t: i18next.TFunction, e: EventLike) { + if (this.userPermissions.studio && this.playlist.activationId) { + doUserAction(t, e, UserAction.CLEAR_QUICK_LOOP, async (e, ts) => + MeteorCall.userAction.clearQuickLoop(e, ts, this.playlist._id) + ) + } + } + + public executeActivate(t: i18next.TFunction, e: EventLike) { + if ('persist' in e) e.persist() + + if ( + this.userPermissions.studio && + (!this.playlist.activationId || (this.playlist.activationId && this.playlist.rehearsal)) + ) { + const onSuccess = () => { + this.deferFlushAndRewindSegments() + if (typeof this.onActivate === 'function') this.onActivate(false) + } + const doActivate = () => { + doUserAction( + t, + e, + UserAction.ACTIVATE_RUNDOWN_PLAYLIST, + async (e, ts) => MeteorCall.userAction.activate(e, ts, this.playlist._id, false), + (err) => { + if (!err) { + if (typeof this.onActivate === 'function') this.onActivate(false) + } else if (ClientAPI.isClientResponseError(err)) { + if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { + this.handleAnotherPlaylistActive(t, this.playlist._id, false, err.error, () => { + if (typeof this.onActivate === 'function') this.onActivate(false) + }) + return false + } + } + } + ) + } + + const doActivateAndReset = () => { + this.rewindSegments() + doUserAction( + t, + e, + UserAction.RESET_AND_ACTIVATE_RUNDOWN_PLAYLIST, + async (e, ts) => MeteorCall.userAction.resetAndActivate(e, ts, this.playlist._id), + (err) => { + if (!err) { + onSuccess() + } else if (ClientAPI.isClientResponseError(err)) { + if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { + this.handleAnotherPlaylistActive(t, this.playlist._id, false, err.error, onSuccess) + return false + } + } + } + ) + } + + if (!checkRundownTimes(this.playlist.timing).shouldHaveStarted) { + // The broadcast hasn't started yet + doModalDialog({ + title: 'Activate "On Air"', + message: t('Do you want to activate this Rundown?'), + yes: 'Reset and Activate "On Air"', + no: t('Cancel'), + actions: [ + { + label: 'Activate "On Air"', + classNames: 'btn-secondary', + on: () => { + doActivate() // this one activates without resetting + }, + }, + ], + acceptOnly: false, + onAccept: () => { + doUserAction( + t, + e, + UserAction.RESET_AND_ACTIVATE_RUNDOWN_PLAYLIST, + async (e, ts) => MeteorCall.userAction.resetAndActivate(e, ts, this.playlist._id), + (err) => { + if (!err) { + onSuccess() + } else if (ClientAPI.isClientResponseError(err)) { + if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { + this.handleAnotherPlaylistActive(t, this.playlist._id, false, err.error, onSuccess) + return false + } + } + } + ) + }, + }) + } else if (!checkRundownTimes(this.playlist.timing).shouldHaveEnded) { + // The broadcast has started + doActivate() + } else { + // The broadcast has ended, going into active mode is probably not what you want to do + doModalDialog({ + title: 'Activate "On Air"', + message: t('The planned end time has passed, are you sure you want to activate this Rundown?'), + yes: 'Reset and Activate "On Air"', + no: t('Cancel'), + actions: [ + { + label: 'Activate "On Air"', + classNames: 'btn-secondary', + on: () => { + doActivate() // this one activates without resetting + }, + }, + ], + acceptOnly: false, + onAccept: () => { + doActivateAndReset() + }, + }) + } + } + } + + public executeActivateRehearsal = (t: i18next.TFunction, e: EventLike) => { + if ('persist' in e) e.persist() + + if ( + this.userPermissions.studio && + (!this.playlist.activationId || (this.playlist.activationId && !this.playlist.rehearsal)) + ) { + const onSuccess = () => { + if (typeof this.onActivate === 'function') this.onActivate(false) + } + const doActivateRehersal = () => { + doUserAction( + t, + e, + UserAction.ACTIVATE_RUNDOWN_PLAYLIST, + async (e, ts) => MeteorCall.userAction.activate(e, ts, this.playlist._id, true), + (err) => { + if (!err) { + onSuccess() + } else if (ClientAPI.isClientResponseError(err)) { + if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { + this.handleAnotherPlaylistActive(t, this.playlist._id, true, err.error, onSuccess) + return false + } + } + } + ) + } + if (!checkRundownTimes(this.playlist.timing).shouldHaveStarted) { + // The broadcast hasn't started yet + if (!this.playlist.activationId) { + // inactive, do the full preparation: + doUserAction( + t, + e, + UserAction.PREPARE_FOR_BROADCAST, + async (e, ts) => MeteorCall.userAction.prepareForBroadcast(e, ts, this.playlist._id), + (err) => { + if (!err) { + onSuccess() + } else if (ClientAPI.isClientResponseError(err)) { + if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { + this.handleAnotherPlaylistActive(t, this.playlist._id, true, err.error, onSuccess) + return false + } + } + } + ) + } else if (!this.playlist.rehearsal) { + // Active, and not in rehearsal + doModalDialog({ + title: 'Activate "Rehearsal"', + message: t('Are you sure you want to activate Rehearsal Mode?'), + yes: 'Activate "Rehearsal"', + no: t('Cancel'), + onAccept: () => { + doActivateRehersal() + }, + }) + } else { + // Already in rehearsal, do nothing + } + } else { + // The broadcast has started + if (!checkRundownTimes(this.playlist.timing).shouldHaveEnded) { + // We are in the broadcast + doModalDialog({ + title: 'Activate "Rehearsal"', + message: t('Are you sure you want to activate Rehearsal Mode?'), + yes: 'Activate "Rehearsal"', + no: t('Cancel'), + onAccept: () => { + doActivateRehersal() + }, + }) + } else { + // The broadcast has ended + doActivateRehersal() + } + } + } + } + + public executeDeactivate = (t: i18next.TFunction, e: EventLike) => { + if ('persist' in e) e.persist() + + if (this.userPermissions.studio && this.playlist.activationId) { + if (checkRundownTimes(this.playlist.timing).shouldHaveStarted) { + if (this.playlist.rehearsal) { + // We're in rehearsal mode + doUserAction(t, e, UserAction.DEACTIVATE_RUNDOWN_PLAYLIST, async (e, ts) => + MeteorCall.userAction.deactivate(e, ts, this.playlist._id) + ) + } else { + doModalDialog({ + title: 'Deactivate "On Air"', + message: t('Are you sure you want to deactivate this rundown?\n(This will clear the outputs.)'), + warning: true, + yes: t('Deactivate "On Air"'), + no: t('Cancel'), + onAccept: () => { + doUserAction(t, e, UserAction.DEACTIVATE_RUNDOWN_PLAYLIST, async (e, ts) => + MeteorCall.userAction.deactivate(e, ts, this.playlist._id) + ) + }, + }) + } + } else { + // Do it right away + doUserAction(t, e, UserAction.DEACTIVATE_RUNDOWN_PLAYLIST, async (e, ts) => + MeteorCall.userAction.deactivate(e, ts, this.playlist._id) + ) + } + } + } + + public executeActivateAdlibTesting = (t: i18next.TFunction, e: EventLike) => { + if ('persist' in e) e.persist() + + if ( + this.userPermissions.studio && + this.studio.settings.allowAdlibTestingSegment && + this.playlist.activationId && + this.currentRundown + ) { + const rundownId = this.currentRundown._id + doUserAction(t, e, UserAction.ACTIVATE_ADLIB_TESTING, async (e, ts) => + MeteorCall.userAction.activateAdlibTestingMode(e, ts, this.playlist._id, rundownId) + ) + } + } + + public executeResetRundown = (t: i18next.TFunction, e: EventLike) => { + if ('persist' in e) e.persist() + + const doReset = () => { + this.rewindSegments() // Do a rewind right away + doUserAction( + t, + e, + UserAction.RESET_RUNDOWN_PLAYLIST, + async (e, ts) => MeteorCall.userAction.resetRundownPlaylist(e, ts, this.playlist._id), + () => { + this.deferFlushAndRewindSegments() + } + ) + } + if (this.playlist.activationId && !this.playlist.rehearsal && !this.studio.settings.allowRundownResetOnAir) { + // The rundown is active and not in rehearsal + doModalDialog({ + title: 'Reset Rundown', + message: t('The rundown can not be reset while it is active'), + onAccept: () => { + // nothing + }, + acceptOnly: true, + yes: 'OK', + }) + } else { + doReset() + } + } + + public executeReloadRundownPlaylist = (t: i18next.TFunction, e: EventLike) => { + if (!this.userPermissions.studio) return + + doUserAction( + t, + e, + UserAction.RELOAD_RUNDOWN_PLAYLIST_DATA, + async (e, ts) => MeteorCall.userAction.resyncRundownPlaylist(e, ts, this.playlist._id), + (err, reloadResponse) => { + if (!err && reloadResponse) { + if (!handleRundownPlaylistReloadResponse(t, this.userPermissions, reloadResponse)) { + if (this.playlist && this.playlist.nextPartInfo) { + scrollToPartInstance(this.playlist.nextPartInfo.partInstanceId).catch((error) => { + if (!error.toString().match(/another scroll/)) console.warn(error) + }) + } + } + } + } + ) + } + + public executeTakeRundownSnapshot = (t: i18next.TFunction, e: EventLike) => { + if (!this.userPermissions.studio) return + + const doneMessage = t('A snapshot of the current Running\xa0Order has been created for troubleshooting.') + doUserAction( + t, + e, + UserAction.CREATE_SNAPSHOT_FOR_DEBUG, + async (e, ts) => + MeteorCall.system.generateSingleUseToken().then(async (tokenResponse) => { + if (ClientAPI.isClientResponseError(tokenResponse)) { + throw UserError.fromSerialized(tokenResponse.error) + } else if (!tokenResponse.result) { + throw new Error(`Internal Error: No token.`) + } + return MeteorCall.userAction.storeRundownSnapshot( + e, + ts, + hashSingleUseToken(tokenResponse.result), + this.playlist._id, + 'Taken by user', + false + ) + }), + () => { + NotificationCenter.push( + new Notification( + undefined, + NoticeLevel.NOTIFICATION, + doneMessage, + 'userAction', + undefined, + false, + undefined, + undefined, + 5000 + ) + ) + return false + }, + doneMessage + ) + } + + public executeActivateRundown = (t: i18next.TFunction, e: EventLike) => { + // Called from the ModalDialog, 1 minute before broadcast starts + if (!this.userPermissions.studio) return + + this.rewindSegments() // Do a rewind right away + + doUserAction( + t, + e, + UserAction.ACTIVATE_RUNDOWN_PLAYLIST, + async (e, ts) => MeteorCall.userAction.activate(e, ts, this.playlist._id, false), + (err) => { + if (!err) { + if (typeof this.onActivate === 'function') this.onActivate(false) + } else if (ClientAPI.isClientResponseError(err)) { + if (err.error.key === UserErrorMessage.RundownAlreadyActiveNames) { + this.handleAnotherPlaylistActive(t, this.playlist._id, false, err.error, () => { + if (typeof this.onActivate === 'function') this.onActivate(false) + }) + return false + } + } + } + ) + } + + public executeResetAndActivateRundown = (t: i18next.TFunction, e: EventLike) => { + // Called from the ModalDialog, 1 minute before broadcast starts + if (!this.userPermissions.studio) return + + this.rewindSegments() // Do a rewind right away + + doUserAction( + t, + e, + UserAction.RESET_AND_ACTIVATE_RUNDOWN_PLAYLIST, + async (e, ts) => MeteorCall.userAction.resetAndActivate(e, ts, this.playlist._id), + (err) => { + if (!err) { + this.deferFlushAndRewindSegments() + if (typeof this.onActivate === 'function') this.onActivate(false) + } + } + ) + } + + private deferFlushAndRewindSegments = () => { + // Do a rewind later, when the UI has updated + Meteor.defer(() => { + Tracker.flush() + Meteor.setTimeout(() => { + this.rewindSegments() + RundownViewEventBus.emit(RundownViewEvents.GO_TO_TOP) + }, 500) + }) + } + + private rewindSegments = () => { + RundownViewEventBus.emit(RundownViewEvents.REWIND_SEGMENTS) + } +} + +export interface RundownPlaylistOperations { + take: (e: EventLike) => void + hold: (e: EventLike) => void + clearQuickLoop: (e: EventLike) => void + activate: (e: EventLike) => void + activateRehearsal: (e: EventLike) => void + deactivate: (e: EventLike) => void + activateAdlibTesting: (e: EventLike) => void + resetRundown: (e: EventLike) => void + reloadRundownPlaylist: (e: EventLike) => void + takeRundownSnapshot: (e: EventLike) => void + activateRundown: (e: EventLike) => void + resetAndActivateRundown: (e: EventLike) => void +} + +const RundownPlaylistOperationsContext = React.createContext(null) + +export function RundownPlaylistOperationsContextProvider({ + children, + currentRundown, + playlist, + studio, + onActivate, +}: React.PropsWithChildren<{ + studio: UIStudio + playlist: DBRundownPlaylist + currentRundown: Rundown | undefined + onActivate?: (isRehearsal: boolean) => void +}>): React.JSX.Element | null { + const { t } = useTranslation() + + const userPermissions = useContext(UserPermissionsContext) + + const service = useMemo( + () => new RundownPlaylistOperationsService(studio, playlist, currentRundown, userPermissions, onActivate), + [] + ) + + useEffect(() => { + service.studio = studio + service.playlist = playlist + service.currentRundown = currentRundown + service.userPermissions = userPermissions + service.onActivate = onActivate + }, [currentRundown, playlist, studio, userPermissions, onActivate]) + + const apiObject = useMemo( + () => + ({ + take: (e) => service.executeTake(t, e), + hold: (e) => service.executeHold(t, e), + clearQuickLoop: (e) => service.executeClearQuickLoop(t, e), + activate: (e) => service.executeActivate(t, e), + activateRehearsal: (e) => service.executeActivateRehearsal(t, e), + deactivate: (e) => service.executeDeactivate(t, e), + activateAdlibTesting: (e) => service.executeActivateAdlibTesting(t, e), + resetRundown: (e) => service.executeResetRundown(t, e), + reloadRundownPlaylist: (e) => service.executeReloadRundownPlaylist(t, e), + takeRundownSnapshot: (e) => service.executeTakeRundownSnapshot(t, e), + activateRundown: (e) => service.executeActivateRundown(t, e), + resetAndActivateRundown: (e) => service.executeResetAndActivateRundown(t, e), + }) satisfies RundownPlaylistOperations, + [service, t] + ) + + return ( + {children} + ) +} + +export function useRundownPlaylistOperations(): RundownPlaylistOperations { + const context = useContext(RundownPlaylistOperationsContext) + + if (!context) + throw new Error('This component must be a child of a `RundownPlaylistOperationsContextProvider` component.') + + return context +} + +interface RundownTimesInfo { + shouldHaveStarted: boolean + willShortlyStart: boolean + shouldHaveEnded: boolean +} + +type EventLike = + | { + persist(): void + } + | {} + +export function checkRundownTimes(playlistTiming: RundownPlaylistTiming): RundownTimesInfo { + const currentTime = getCurrentTime() + + const shouldHaveEnded = + currentTime > + (PlaylistTiming.getExpectedStart(playlistTiming) || 0) + (PlaylistTiming.getExpectedDuration(playlistTiming) || 0) + + return { + shouldHaveStarted: currentTime > (PlaylistTiming.getExpectedStart(playlistTiming) || 0), + willShortlyStart: + !shouldHaveEnded && currentTime > (PlaylistTiming.getExpectedStart(playlistTiming) || 0) - REHEARSAL_MARGIN, + shouldHaveEnded, + } +} diff --git a/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx b/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx index ccaa737756d..906a22a3eb2 100644 --- a/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownNotifier.tsx @@ -24,7 +24,7 @@ import { doUserAction, UserAction } from '../../lib/clientUserAction.js' 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_old/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' diff --git a/packages/webui/src/client/ui/RundownView/RundownViewContextProviders.tsx b/packages/webui/src/client/ui/RundownView/RundownViewContextProviders.tsx index 552f54afeb9..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_old/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' From 5244cdc48b666ce21c8320aa66ce32f61262a443 Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Fri, 13 Mar 2026 04:46:31 +0100 Subject: [PATCH 260/291] Top bar: fix hamburger menu not closing in some cases --- .../RundownView/RundownHeader/RundownContextMenu.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx index 1d2acbd09ba..45bb25eedbf 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx @@ -162,7 +162,7 @@ export function RundownHamburgerButton({ const { t } = useTranslation() const buttonRef = useRef(null) - const handleClick = useCallback( + const handleToggle = useCallback( (e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() @@ -181,14 +181,19 @@ export function RundownHamburgerButton({ }) } }, - [isOpen] + [isOpen, onClose] ) return ( ) } From 08450fb1d26fefc9b2965a6bb50f6ee3bd13a805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Fri, 13 Mar 2026 13:45:19 +0100 Subject: [PATCH 273/291] fix: Correct prefix for Start In when passing the planned start time --- .../ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx index 923570f3192..de7d132bced 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx @@ -29,7 +29,7 @@ export function RundownHeaderPlannedStart({ ) : ( - {diff >= 0 && '-'} + {diff >= 0 && '+'} {RundownUtils.formatDiffToTimecode(Math.abs(diff), false, false, true, true, true)} ))} From 0a5497cc1b7ffb5ba70cd28888fe1d3ae456c7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Fri, 13 Mar 2026 13:55:22 +0100 Subject: [PATCH 274/291] fix: more robust falsy checks that allow 0 values --- .../ui/RundownView/RundownHeader/RundownHeaderDurations.tsx | 2 +- .../ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx index 881d32f788f..e949f45111a 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx @@ -20,7 +20,7 @@ export function RundownHeaderDurations({ // Use remainingPlaylistDuration which includes current part's remaining time const estDuration = timingDurations.remainingPlaylistDuration - if (expectedDuration == null && estDuration == null) return null + if (expectedDuration == undefined && estDuration == undefined) return null return (
diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx index df93d376c63..52af4a76e78 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -23,7 +23,7 @@ export function RundownHeaderExpectedEnd({ ? now + timingDurations.remainingPlaylistDuration : null - if (!expectedEnd && !estEnd) return null + if (expectedEnd === undefined && estEnd === null) return null return (
From 533a9c0b1e978cb1bc69bca9c32abe3f2e75ff3e Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Fri, 13 Mar 2026 14:11:31 +0100 Subject: [PATCH 275/291] chore: Tweaks to the menu wording and menu styling. Added visible menu dividers. --- .../webui/src/client/styles/contextMenu.scss | 19 ++++++++++++++---- .../RundownHeader/RundownContextMenu.tsx | 20 +++++++++++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/webui/src/client/styles/contextMenu.scss b/packages/webui/src/client/styles/contextMenu.scss index c3f21ff1e78..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,14 +36,16 @@ 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; @@ -60,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/ui/RundownView/RundownHeader/RundownContextMenu.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx index f5389a3b24e..e7958a1fc7e 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx @@ -100,16 +100,21 @@ export function RundownContextMenu({ {t('Activate (Rehearsal)')} ) ) : ( - {t('Activate (On-Air)')} + {t('Activate On Air')} )} {rundownTimesInfo.willShortlyStart && !playlist.activationId && ( - {t('Activate (On-Air)')} + {t('Activate On Air')} )} - {playlist.activationId ? {t('Deactivate')} : null} + {playlist.activationId ? {t('Deactivate Studio')} : null} {studio.settings.allowAdlibTestingSegment && playlist.activationId ? ( {t('AdLib Testing')} ) : null} - {playlist.activationId ? {t('Take')} : null} + {playlist.activationId ? ( + <> + + {t('Take')} + + ) : null} {studio.settings.allowHold && playlist.activationId ? ( {t('Hold')} ) : null} @@ -117,7 +122,10 @@ export function RundownContextMenu({ {t('Clear QuickLoop')} ) : null} {!(playlist.activationId && !playlist.rehearsal && !studio.settings.allowRundownResetOnAir) ? ( - {t('Reset Rundown')} + <> + + {t('Reset Rundown')} + ) : null} {t('Reload {{nrcsName}} Data', { @@ -126,7 +134,7 @@ export function RundownContextMenu({ {t('Store Snapshot')} - history.push('/')}>{t('Close')} + history.push('/')}>{t('Close Rundown')} ) : ( From d265fac2f45f3ecd2c383f4af6107c615fd4bbb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20St=C3=A6rk=C3=A6r?= Date: Fri, 13 Mar 2026 14:29:03 +0100 Subject: [PATCH 276/291] fix: swaps the timers in the simple view mode --- .../ui/RundownView/RundownHeader/RundownHeaderDurations.tsx | 4 ++-- .../ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx | 4 ++-- .../RundownView/RundownHeader/RundownHeaderPlannedStart.tsx | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx index e949f45111a..b49e21f09b7 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderDurations.tsx @@ -24,12 +24,12 @@ export function RundownHeaderDurations({ return (
- {expectedDuration ? ( + {!simplified && expectedDuration ? ( {RundownUtils.formatDiffToTimecode(expectedDuration, false, true, true, true, true)} ) : null} - {!simplified && estDuration !== undefined ? ( + {estDuration !== undefined ? ( {RundownUtils.formatDiffToTimecode(estDuration, false, true, true, true, true)} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx index 52af4a76e78..4ce2497d832 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -27,10 +27,10 @@ export function RundownHeaderExpectedEnd({ return (
- {expectedEnd !== undefined ? ( + {!simplified && expectedEnd !== undefined ? ( ) : null} - {!simplified && estEnd !== 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 index de7d132bced..48002e0282e 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx @@ -23,8 +23,10 @@ export function RundownHeaderPlannedStart({ return (
+ {!simplified && expectedStart !== undefined ? ( - {!simplified && + ) : null} + { (playlist.startedPlayback !== undefined ? ( ) : ( From 128f28175f179f228cefc1d54b80f99f976ea2ad Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Fri, 13 Mar 2026 14:35:51 +0100 Subject: [PATCH 277/291] chore: Made sure the menu icon only changed state and was clickable when the menu was initiated from the icon. --- .../RundownHeader/RundownContextMenu.tsx | 12 +++++++++--- .../RundownView/RundownHeader/RundownHeader.scss | 7 ++++++- .../RundownView/RundownHeader/RundownHeader.tsx | 15 ++++++++++++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx index e7958a1fc7e..3ab85520cc5 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx @@ -169,8 +169,10 @@ export function RundownHeaderContextMenuTrigger({ children }: Readonly void }>): JSX.Element { + onOpen, +}: Readonly<{ isOpen?: boolean; disabled?: boolean; onClose: () => void; onOpen?: () => void }>): JSX.Element { const { t } = useTranslation() const buttonRef = useRef(null) @@ -179,6 +181,8 @@ export function RundownHamburgerButton({ e.preventDefault() e.stopPropagation() + if (disabled) return + if (isOpen) { hideMenu({ id: RUNDOWN_CONTEXT_MENU_ID }) onClose() @@ -191,15 +195,17 @@ export function RundownHamburgerButton({ position: { x: rect.left, y: rect.bottom + 5 }, id: RUNDOWN_CONTEXT_MENU_ID, }) + if (onOpen) onOpen() } }, - [isOpen, onClose] + [isOpen, disabled, onClose, onOpen] ) return ( - - - +
+ + + + +
-
+ ) From 96382360ed58f98324c86f381ff4f796b2fbc711 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Mon, 16 Mar 2026 09:07:23 +0100 Subject: [PATCH 281/291] chore: Added visual hover indication on the menu and close buttons. --- .../RundownHeader/RundownHeader.scss | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 4f1c7d0c413..1f285bb3223 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -383,11 +383,36 @@ justify-content: center; height: 100%; font-size: 1.2em; - transition: color 0.2s; + 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)); + } } } @@ -567,7 +592,27 @@ color: #40b8fa; opacity: 0; flex-shrink: 0; - transition: opacity 0.2s; + 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 { From 5f039b77812b9a7230fbdeba5734f757f148d1e4 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Mon, 16 Mar 2026 15:43:37 +0100 Subject: [PATCH 282/291] chore: Added a Bootstrap CSS customization to get around the faulty size rendering of the entire GUI only when the URL parameter 'zoom' is set to '100' or no localStorage variable 'uiZoomLevel' exists, that occurs when the user has a local, non-default font size rendering. --- packages/webui/src/client/styles/bootstrap-customize.scss | 1 + 1 file changed, 1 insertion(+) 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}; From a2308dbf7162e59e1042049970ab2e9ea71586d0 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Mon, 16 Mar 2026 15:59:21 +0000 Subject: [PATCH 283/291] Lint fixes And fix the yarn lint:fix command --- package.json | 2 +- .../src/context/onSetAsNextContext.ts | 3 ++- .../src/context/rundownContext.ts | 5 +---- .../context/services/TTimersService.ts | 6 +++++- packages/openapi/run_server_tests.mjs | 2 +- .../RundownHeader/RundownContextMenu.tsx | 4 +++- .../RundownHeaderExpectedEnd.tsx | 4 +--- .../RundownHeaderPlannedStart.tsx | 19 +++++++++---------- 8 files changed, 23 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index f011d29dd3d..f929601411d 100644 --- a/package.json +++ b/package.json @@ -25,7 +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", + "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", diff --git a/packages/blueprints-integration/src/context/onSetAsNextContext.ts b/packages/blueprints-integration/src/context/onSetAsNextContext.ts index 6a209b11af6..114ef0f47ca 100644 --- a/packages/blueprints-integration/src/context/onSetAsNextContext.ts +++ b/packages/blueprints-integration/src/context/onSetAsNextContext.ts @@ -19,7 +19,8 @@ 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, ITriggerIngestChangeContext, ITTimersContext { +export interface IOnSetAsNextContext + extends IShowStyleUserContext, IEventContext, ITriggerIngestChangeContext, ITTimersContext { /** Information about the current loop, if there is one */ readonly quickLoopInfo: BlueprintQuickLookInfo | null diff --git a/packages/blueprints-integration/src/context/rundownContext.ts b/packages/blueprints-integration/src/context/rundownContext.ts index cf3a30e332c..cb57cbd5692 100644 --- a/packages/blueprints-integration/src/context/rundownContext.ts +++ b/packages/blueprints-integration/src/context/rundownContext.ts @@ -15,10 +15,7 @@ export interface IRundownContext extends IShowStyleContext { export interface IRundownUserContext extends IUserNotesContext, IRundownContext {} export interface IRundownActivationContext - extends IRundownContext, - IExecuteTSRActionsContext, - IDataStoreMethods, - ITTimersContext { + 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/job-worker/src/blueprints/context/services/TTimersService.ts b/packages/job-worker/src/blueprints/context/services/TTimersService.ts index 5f79b7417ff..f3cab65238f 100644 --- a/packages/job-worker/src/blueprints/context/services/TTimersService.ts +++ b/packages/job-worker/src/blueprints/context/services/TTimersService.ts @@ -2,7 +2,11 @@ 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 { + 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' 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/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx index 3ab85520cc5..7f7800f9dcd 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownContextMenu.tsx @@ -105,7 +105,9 @@ export function RundownContextMenu({ {rundownTimesInfo.willShortlyStart && !playlist.activationId && ( {t('Activate On Air')} )} - {playlist.activationId ? {t('Deactivate Studio')} : null} + {playlist.activationId ? ( + {t('Deactivate Studio')} + ) : null} {studio.settings.allowAdlibTestingSegment && playlist.activationId ? ( {t('AdLib Testing')} ) : null} diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx index 4ce2497d832..8ab25709827 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderExpectedEnd.tsx @@ -19,9 +19,7 @@ export function RundownHeaderExpectedEnd({ // Use remainingPlaylistDuration which includes current part's remaining time const estEnd = - timingDurations.remainingPlaylistDuration !== undefined - ? now + timingDurations.remainingPlaylistDuration - : null + timingDurations.remainingPlaylistDuration !== undefined ? now + timingDurations.remainingPlaylistDuration : null if (expectedEnd === undefined && estEnd === null) return null diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx index 48002e0282e..9d0324413f9 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderPlannedStart.tsx @@ -24,17 +24,16 @@ export function RundownHeaderPlannedStart({ return (
{!simplified && expectedStart !== undefined ? ( - + ) : null} - { - (playlist.startedPlayback !== undefined ? ( - - ) : ( - - {diff >= 0 && '+'} - {RundownUtils.formatDiffToTimecode(Math.abs(diff), false, false, true, true, true)} - - ))} + {playlist.startedPlayback !== undefined ? ( + + ) : ( + + {diff >= 0 && '+'} + {RundownUtils.formatDiffToTimecode(Math.abs(diff), false, false, true, true, true)} + + )}
) } From 088b9585d7653cb9d007d7207ebc6e1eb52293b0 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Tue, 17 Mar 2026 11:02:03 +0000 Subject: [PATCH 284/291] Remove mock timer --- packages/webui/src/client/lib/tTimerUtils.ts | 34 ++------------------ 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/packages/webui/src/client/lib/tTimerUtils.ts b/packages/webui/src/client/lib/tTimerUtils.ts index 8b5a0938ea5..216e04a4c98 100644 --- a/packages/webui/src/client/lib/tTimerUtils.ts +++ b/packages/webui/src/client/lib/tTimerUtils.ts @@ -46,36 +46,6 @@ export function calculateTTimerOverUnder(timer: RundownTTimer, now: number): num return estimateDuration - duration } -// TODO: remove this mock -let mockTimer: RundownTTimer | undefined - -export function getDefaultTTimer(_tTimers: [RundownTTimer, RundownTTimer, RundownTTimer]): RundownTTimer | undefined { - // FORCE MOCK: - /* - const active = tTimers.find((t) => t.mode) - if (active) return active - */ - - if (!mockTimer) { - const now = Date.now() - mockTimer = { - index: 0, - label: 'MOCK TIMER', - mode: { - type: 'countdown', - }, - state: { - zeroTime: now + 60 * 60 * 1000, // 1 hour - duration: 0, - paused: false, - }, - estimateState: { - zeroTime: now + 65 * 60 * 1000, // 65 mins -> 5 mins over - duration: 0, - paused: false, - }, - } as any - } - - return mockTimer +export function getDefaultTTimer(tTimers: [RundownTTimer, RundownTTimer, RundownTTimer]): RundownTTimer | undefined { + return tTimers.find((t) => t.mode) } From 2299440ef174f242d181601f874dab8740258f32 Mon Sep 17 00:00:00 2001 From: Jonas Hummelstrand Date: Tue, 17 Mar 2026 15:43:27 +0100 Subject: [PATCH 285/291] chore: Vertically aligned the Over/Under and Clock. --- .../src/client/ui/RundownView/RundownHeader/Countdown.scss | 2 +- .../client/ui/RundownView/RundownHeader/RundownHeader.scss | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss index 4a0270766f1..24b03a89301 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/Countdown.scss @@ -3,7 +3,7 @@ .countdown { display: flex; - align-items: baseline; + align-items: center; justify-content: space-between; gap: 0.3em; transition: color 0.2s; diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss index 1f285bb3223..a0fce01c1ab 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeader.scss @@ -155,11 +155,11 @@ max-width: 40em; color: #fff; max-height: 0; - padding-top: 0.55em; + padding-top: 0; overflow: hidden; transition: max-height 0.2s ease, - padding 0.2s ease; + padding-top 0.2s ease; .rundown-name, .playlist-name { @@ -638,6 +638,7 @@ .rundown-header__clocks-clock-group { .rundown-header__clocks-playlist-name { max-height: 2em; + padding-top: 0em; } } } From 620eb0025f94021f9c7dfa19f0a92c82c30f0b6b Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Tue, 17 Mar 2026 12:41:00 +0000 Subject: [PATCH 286/291] Improve empty t-timers object in test --- meteor/server/__tests__/cronjobs.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/meteor/server/__tests__/cronjobs.test.ts b/meteor/server/__tests__/cronjobs.test.ts index 2d45733ccc7..b5f6ce1d78e 100644 --- a/meteor/server/__tests__/cronjobs.test.ts +++ b/meteor/server/__tests__/cronjobs.test.ts @@ -622,7 +622,11 @@ describe('cronjobs', () => { type: PlaylistTimingType.None, }, activationId: protectString(''), - tTimers: [] as any, + tTimers: [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, + ], }) return { From 3c7d1bef5c07287f657cbab7859034372e96f74c Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Tue, 17 Mar 2026 14:07:54 +0000 Subject: [PATCH 287/291] Simplify validating index and add more tests --- packages/job-worker/src/playout/__tests__/tTimers.test.ts | 5 +++++ packages/job-worker/src/playout/tTimers.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/job-worker/src/playout/__tests__/tTimers.test.ts b/packages/job-worker/src/playout/__tests__/tTimers.test.ts index bea1a2c92b3..d323e939abd 100644 --- a/packages/job-worker/src/playout/__tests__/tTimers.test.ts +++ b/packages/job-worker/src/playout/__tests__/tTimers.test.ts @@ -43,6 +43,11 @@ describe('tTimers utils', () => { 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', () => { diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index bb005e52b70..83af3a093eb 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -14,7 +14,7 @@ import { PlayoutModel } from './model/PlayoutModel.js' import { getOrderedPartsAfterPlayhead } from './lookahead/util.js' export function validateTTimerIndex(index: number): asserts index is RundownTTimerIndex { - if (isNaN(index) || index < 1 || index > 3) throw new Error(`T-timer index out of range: ${index}`) + if (![1, 2, 3].includes(index)) throw new Error(`T-timer index out of range: ${index}`) } /** From 518d406b76af49bd7c23e95557a5c4c2d50f4588 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Tue, 17 Mar 2026 14:44:06 +0000 Subject: [PATCH 288/291] Change T-Timer estimate to projection --- .../src/context/tTimersContext.ts | 40 +++++------ .../corelib/src/dataModel/RundownPlaylist.ts | 12 ++-- packages/corelib/src/worker/studio.ts | 8 +-- .../context/services/TTimersService.ts | 30 ++++---- .../services/__tests__/TTimersService.test.ts | 72 +++++++++---------- packages/job-worker/src/ingest/commit.ts | 10 +-- .../src/playout/__tests__/tTimersJobs.test.ts | 12 ++-- packages/job-worker/src/playout/setNext.ts | 10 +-- packages/job-worker/src/playout/tTimers.ts | 18 ++--- .../job-worker/src/playout/tTimersJobs.ts | 10 +-- .../job-worker/src/workers/studio/child.ts | 2 +- .../job-worker/src/workers/studio/jobs.ts | 4 +- packages/webui/src/client/lib/tTimerUtils.ts | 10 +-- 13 files changed, 119 insertions(+), 119 deletions(-) diff --git a/packages/blueprints-integration/src/context/tTimersContext.ts b/packages/blueprints-integration/src/context/tTimersContext.ts index 28e03b8ad60..7b00d9258a1 100644 --- a/packages/blueprints-integration/src/context/tTimersContext.ts +++ b/packages/blueprints-integration/src/context/tTimersContext.ts @@ -73,49 +73,49 @@ export interface IPlaylistTTimer { restart(): boolean /** - * Clear any estimate (manual or anchor-based) for this timer - * This removes both manual estimates set via setEstimateTime/setEstimateDuration - * and automatic estimates based on anchor parts set via setEstimateAnchorPart. + * 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. */ - clearEstimate(): void + clearProjected(): void /** - * Set the anchor part for automatic estimate calculation + * 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 estimate accordingly. - * Clears any manual estimate set via setEstimateTime/setEstimateDuration. + * 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 */ - setEstimateAnchorPart(partId: string): void + setProjectedAnchorPart(partId: string): void /** - * Set the anchor part for automatic estimate calculation, looked up by its externalId. + * 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 estimate set via setEstimateTime/setEstimateDuration. + * Clears any manual projection set via setProjectedTime/setProjectedDuration. * @param externalId The externalId of the part to use as timing anchor */ - setEstimateAnchorPartByExternalId(externalId: string): void + setProjectedAnchorPartByExternalId(externalId: string): void /** - * Manually set the estimate as an absolute timestamp + * 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 (estimate won't update with time passing). - * If false (default), we're progressing normally (estimate counts down in real-time). + * @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). */ - setEstimateTime(time: number, paused?: boolean): void + setProjectedTime(time: number, paused?: boolean): void /** - * Manually set the estimate as a relative duration from now - * Use this when you want to express the estimate as "X milliseconds from now". + * 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 (estimate won't update with time passing). - * If false (default), we're progressing normally (estimate counts down in real-time). + * @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). */ - setEstimateDuration(duration: number, paused?: boolean): void + setProjectedDuration(duration: number, paused?: boolean): void } export type IPlaylistTTimerState = diff --git a/packages/corelib/src/dataModel/RundownPlaylist.ts b/packages/corelib/src/dataModel/RundownPlaylist.ts index e3eb8fae8c3..06cf6d3ff5f 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist.ts @@ -205,23 +205,23 @@ export interface RundownTTimer { */ state: TimerState | null - /** The estimated time when we expect to reach the anchor part, for calculating over/under diff. + /** 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 estimate and the timer's target (state.zeroTime). + * 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 (estimate moves with real time) + * 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. */ - estimateState?: TimerState + 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 estimateState based on when we expect to reach this part. - * If not set, estimateState is not calculated automatically but can still be set manually by a blueprint. + * 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 diff --git a/packages/corelib/src/worker/studio.ts b/packages/corelib/src/worker/studio.ts index bcbde0b94ad..961c7b7dfd9 100644 --- a/packages/corelib/src/worker/studio.ts +++ b/packages/corelib/src/worker/studio.ts @@ -127,10 +127,10 @@ export enum StudioJobs { OnTimelineTriggerTime = 'onTimelineTriggerTime', /** - * Recalculate T-Timer estimates based on current playlist state - * Called after setNext, takes, and ingest changes to update timing anchor estimates + * Recalculate T-Timer projections based on current playlist state + * Called after setNext, takes, and ingest changes to update timing anchor projections */ - RecalculateTTimerEstimates = 'recalculateTTimerEstimates', + RecalculateTTimerProjections = 'recalculateTTimerProjections', /** * Update the timeline with a regenerated Studio Baseline @@ -423,7 +423,7 @@ export type StudioJobFunc = { [StudioJobs.OnPlayoutPlaybackChanged]: (data: OnPlayoutPlaybackChangedProps) => void [StudioJobs.OnTimelineTriggerTime]: (data: OnTimelineTriggerTimeProps) => void - [StudioJobs.RecalculateTTimerEstimates]: () => void + [StudioJobs.RecalculateTTimerProjections]: () => void [StudioJobs.UpdateStudioBaseline]: () => string | false [StudioJobs.CleanupEmptyPlaylists]: () => void diff --git a/packages/job-worker/src/blueprints/context/services/TTimersService.ts b/packages/job-worker/src/blueprints/context/services/TTimersService.ts index f3cab65238f..ab0a67452da 100644 --- a/packages/job-worker/src/blueprints/context/services/TTimersService.ts +++ b/packages/job-worker/src/blueprints/context/services/TTimersService.ts @@ -20,7 +20,7 @@ import { restartTTimer, resumeTTimer, validateTTimerIndex, - recalculateTTimerEstimates, + recalculateTTimerProjections, } from '../../../playout/tTimers.js' import { getCurrentTime } from '../../../lib/index.js' import type { JobContext } from '../../../jobs/index.js' @@ -195,56 +195,56 @@ export class PlaylistTTimerImpl implements IPlaylistTTimer { return true } - clearEstimate(): void { + clearProjected(): void { this.#timer = { ...this.#timer, anchorPartId: undefined, - estimateState: undefined, + projectedState: undefined, } this.#emitChange(this.#timer) } - setEstimateAnchorPart(partId: string): void { + setProjectedAnchorPart(partId: string): void { this.#timer = { ...this.#timer, anchorPartId: protectString(partId), - estimateState: undefined, // Clear manual estimate + projectedState: undefined, // Clear manual projection } this.#emitChange(this.#timer) - // Recalculate estimates immediately since we already have the playout model - recalculateTTimerEstimates(this.#jobContext, this.#playoutModel) + // Recalculate projections immediately since we already have the playout model + recalculateTTimerProjections(this.#jobContext, this.#playoutModel) } - setEstimateAnchorPartByExternalId(externalId: string): void { + setProjectedAnchorPartByExternalId(externalId: string): void { const part = this.#playoutModel.getAllOrderedParts().find((p) => p.externalId === externalId) if (!part) return - this.setEstimateAnchorPart(unprotectString(part._id)) + this.setProjectedAnchorPart(unprotectString(part._id)) } - setEstimateTime(time: number, paused: boolean = false): void { - const estimateState: TimerState = paused + 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 - estimateState, + projectedState, } this.#emitChange(this.#timer) } - setEstimateDuration(duration: number, paused: boolean = false): void { - const estimateState: TimerState = paused + 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 - estimateState, + projectedState, } this.#emitChange(this.#timer) } 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 index 8922d386ccc..72236e2d51b 100644 --- a/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts +++ b/packages/job-worker/src/blueprints/context/services/__tests__/TTimersService.test.ts @@ -843,17 +843,17 @@ describe('PlaylistTTimerImpl', () => { }) }) - describe('clearEstimate', () => { - it('should clear both anchorPartId and estimateState', () => { + describe('clearProjected', () => { + it('should clear both anchorPartId and projectedState', () => { const tTimers = createEmptyTTimers() tTimers[0].anchorPartId = 'part1' as any - tTimers[0].estimateState = { paused: false, zeroTime: 50000 } + 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.clearEstimate() + timer.clearProjected() expect(updateFn).toHaveBeenCalledWith({ index: 1, @@ -861,18 +861,18 @@ describe('PlaylistTTimerImpl', () => { mode: null, state: null, anchorPartId: undefined, - estimateState: undefined, + projectedState: undefined, }) }) - it('should work when estimates are already cleared', () => { + 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.clearEstimate() + timer.clearProjected() expect(updateFn).toHaveBeenCalledWith({ index: 1, @@ -880,21 +880,21 @@ describe('PlaylistTTimerImpl', () => { mode: null, state: null, anchorPartId: undefined, - estimateState: undefined, + projectedState: undefined, }) }) }) - describe('setEstimateAnchorPart', () => { - it('should set anchorPartId and clear estimateState', () => { + describe('setProjectedAnchorPart', () => { + it('should set anchorPartId and clear projectedState', () => { const tTimers = createEmptyTTimers() - tTimers[0].estimateState = { paused: false, zeroTime: 50000 } + 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.setEstimateAnchorPart('part123') + timer.setProjectedAnchorPart('part123') expect(updateFn).toHaveBeenCalledWith({ index: 1, @@ -902,7 +902,7 @@ describe('PlaylistTTimerImpl', () => { mode: null, state: null, anchorPartId: 'part123', - estimateState: undefined, + projectedState: undefined, }) }) @@ -914,22 +914,22 @@ describe('PlaylistTTimerImpl', () => { const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) // Should not throw - expect(() => timer.setEstimateAnchorPart('part456')).not.toThrow() + expect(() => timer.setProjectedAnchorPart('part456')).not.toThrow() // Job queue should not be called (recalculate is called directly) expect(mockJobContext.queueStudioJob).not.toHaveBeenCalled() }) }) - describe('setEstimateTime', () => { - it('should set estimateState with absolute time (not paused)', () => { + 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.setEstimateTime(50000, false) + timer.setProjectedTime(50000, false) expect(updateFn).toHaveBeenCalledWith({ index: 1, @@ -937,18 +937,18 @@ describe('PlaylistTTimerImpl', () => { mode: null, state: null, anchorPartId: undefined, - estimateState: { paused: false, zeroTime: 50000 }, + projectedState: { paused: false, zeroTime: 50000 }, }) }) - it('should set estimateState with absolute time (paused)', () => { + 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.setEstimateTime(50000, true) + timer.setProjectedTime(50000, true) expect(updateFn).toHaveBeenCalledWith({ index: 1, @@ -956,11 +956,11 @@ describe('PlaylistTTimerImpl', () => { mode: null, state: null, anchorPartId: undefined, - estimateState: { paused: true, duration: 40000 }, // 50000 - 10000 (current time) + projectedState: { paused: true, duration: 40000 }, // 50000 - 10000 (current time) }) }) - it('should clear anchorPartId when setting manual estimate', () => { + it('should clear anchorPartId when setting manual projection', () => { const tTimers = createEmptyTTimers() tTimers[0].anchorPartId = 'part1' as any const updateFn = jest.fn() @@ -968,7 +968,7 @@ describe('PlaylistTTimerImpl', () => { const mockJobContext = createMockJobContext() const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - timer.setEstimateTime(50000) + timer.setProjectedTime(50000) expect(updateFn).toHaveBeenCalledWith( expect.objectContaining({ @@ -984,25 +984,25 @@ describe('PlaylistTTimerImpl', () => { const mockJobContext = createMockJobContext() const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - timer.setEstimateTime(50000) + timer.setProjectedTime(50000) expect(updateFn).toHaveBeenCalledWith( expect.objectContaining({ - estimateState: { paused: false, zeroTime: 50000 }, + projectedState: { paused: false, zeroTime: 50000 }, }) ) }) }) - describe('setEstimateDuration', () => { - it('should set estimateState with relative duration (not paused)', () => { + 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.setEstimateDuration(30000, false) + timer.setProjectedDuration(30000, false) expect(updateFn).toHaveBeenCalledWith({ index: 1, @@ -1010,18 +1010,18 @@ describe('PlaylistTTimerImpl', () => { mode: null, state: null, anchorPartId: undefined, - estimateState: { paused: false, zeroTime: 40000 }, // 10000 (current) + 30000 (duration) + projectedState: { paused: false, zeroTime: 40000 }, // 10000 (current) + 30000 (duration) }) }) - it('should set estimateState with relative duration (paused)', () => { + 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.setEstimateDuration(30000, true) + timer.setProjectedDuration(30000, true) expect(updateFn).toHaveBeenCalledWith({ index: 1, @@ -1029,11 +1029,11 @@ describe('PlaylistTTimerImpl', () => { mode: null, state: null, anchorPartId: undefined, - estimateState: { paused: true, duration: 30000 }, + projectedState: { paused: true, duration: 30000 }, }) }) - it('should clear anchorPartId when setting manual estimate', () => { + it('should clear anchorPartId when setting manual projection', () => { const tTimers = createEmptyTTimers() tTimers[0].anchorPartId = 'part1' as any const updateFn = jest.fn() @@ -1041,7 +1041,7 @@ describe('PlaylistTTimerImpl', () => { const mockJobContext = createMockJobContext() const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - timer.setEstimateDuration(30000) + timer.setProjectedDuration(30000) expect(updateFn).toHaveBeenCalledWith( expect.objectContaining({ @@ -1057,11 +1057,11 @@ describe('PlaylistTTimerImpl', () => { const mockJobContext = createMockJobContext() const timer = new PlaylistTTimerImpl(tTimers[0], updateFn, mockPlayoutModel, mockJobContext) - timer.setEstimateDuration(30000) + timer.setProjectedDuration(30000) expect(updateFn).toHaveBeenCalledWith( expect.objectContaining({ - estimateState: { paused: false, zeroTime: 40000 }, + projectedState: { paused: false, zeroTime: 40000 }, }) ) }) diff --git a/packages/job-worker/src/ingest/commit.ts b/packages/job-worker/src/ingest/commit.ts index 93e1f4d5a21..18c981dc4be 100644 --- a/packages/job-worker/src/ingest/commit.ts +++ b/packages/job-worker/src/ingest/commit.ts @@ -29,7 +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 { recalculateTTimerEstimates } from '../playout/tTimers.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' @@ -242,8 +242,8 @@ export async function CommitIngestOperation( // Note: This should trigger a timeline update, one is already queued in the `deferAfterSave` above await ensureNextPartIsValid(context, playoutModel) - // Recalculate T-Timer estimates after ingest changes - recalculateTTimerEstimates(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 @@ -617,8 +617,8 @@ export async function updatePlayoutAfterChangingRundownInPlaylist( const shouldUpdateTimeline = await ensureNextPartIsValid(context, playoutModel) - // Recalculate T-Timer estimates after playlist changes - recalculateTTimerEstimates(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/playout/__tests__/tTimersJobs.test.ts b/packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts index e6623a952b7..6704e8255ed 100644 --- a/packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts +++ b/packages/job-worker/src/playout/__tests__/tTimersJobs.test.ts @@ -1,5 +1,5 @@ import { setupDefaultJobEnvironment, MockJobContext } from '../../__mocks__/context.js' -import { handleRecalculateTTimerEstimates } from '../tTimersJobs.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' @@ -12,7 +12,7 @@ describe('tTimersJobs', () => { context = setupDefaultJobEnvironment() }) - describe('handleRecalculateTTimerEstimates', () => { + describe('handleRecalculateTTimerProjections', () => { it('should handle studio with active playlists', async () => { // Create an active playlist const playlistId = protectString('playlist1') @@ -59,7 +59,7 @@ describe('tTimersJobs', () => { ) // Should complete without errors - await expect(handleRecalculateTTimerEstimates(context)).resolves.toBeUndefined() + await expect(handleRecalculateTTimerProjections(context)).resolves.toBeUndefined() }) it('should handle studio with no active playlists', async () => { @@ -108,7 +108,7 @@ describe('tTimersJobs', () => { ) // Should complete without errors (just does nothing) - await expect(handleRecalculateTTimerEstimates(context)).resolves.toBeUndefined() + await expect(handleRecalculateTTimerProjections(context)).resolves.toBeUndefined() }) it('should handle multiple active playlists', async () => { @@ -199,13 +199,13 @@ describe('tTimersJobs', () => { ) // Should complete without errors, processing both playlists - await expect(handleRecalculateTTimerEstimates(context)).resolves.toBeUndefined() + 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(handleRecalculateTTimerEstimates(context)).resolves.toBeUndefined() + await expect(handleRecalculateTTimerProjections(context)).resolves.toBeUndefined() }) }) }) diff --git a/packages/job-worker/src/playout/setNext.ts b/packages/job-worker/src/playout/setNext.ts index 57574949452..2fb38067aed 100644 --- a/packages/job-worker/src/playout/setNext.ts +++ b/packages/job-worker/src/playout/setNext.ts @@ -36,7 +36,7 @@ import { PersistentPlayoutStateStore } from '../blueprints/context/services/Pers import { DBPartInstance } from '@sofie-automation/corelib/dist/dataModel/PartInstance' import { PlayoutPartInstanceModelImpl } from './model/implementation/PlayoutPartInstanceModelImpl.js' import { QuickLoopService } from './model/services/QuickLoopService.js' -import { recalculateTTimerEstimates } from './tTimers.js' +import { recalculateTTimerProjections } from './tTimers.js' /** * Set or clear the nexted part, from a given PartInstance, or SelectNextPartResult @@ -100,8 +100,8 @@ export async function setNextPart( await cleanupOrphanedItems(context, playoutModel) - // Recalculate T-Timer estimates based on the new next part - recalculateTTimerEstimates(context, playoutModel) + // Recalculate T-Timer projections based on the new next part + recalculateTTimerProjections(context, playoutModel) if (span) span.end() } @@ -534,8 +534,8 @@ export async function queueNextSegment( playoutModel.setQueuedSegment(null) } - // Recalculate timer estimates as the queued segment affects what comes after next - recalculateTTimerEstimates(context, playoutModel) + // Recalculate timer projections as the queued segment affects what comes after next + recalculateTTimerProjections(context, playoutModel) span?.end() return { queuedSegmentId: queuedSegment?.segment?._id ?? null } diff --git a/packages/job-worker/src/playout/tTimers.ts b/packages/job-worker/src/playout/tTimers.ts index 83af3a093eb..dc6d9524a05 100644 --- a/packages/job-worker/src/playout/tTimers.ts +++ b/packages/job-worker/src/playout/tTimers.ts @@ -174,7 +174,7 @@ export function calculateNextTimeOfDayTarget(targetTime: string | number): numbe } /** - * Recalculate T-Timer estimates based on timing anchors using segment budget timing. + * 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 @@ -189,8 +189,8 @@ export function calculateNextTimeOfDayTarget(targetTime: string | number): numbe * @param context Job context * @param playoutModel The playout model containing the playlist and parts */ -export function recalculateTTimerEstimates(context: JobContext, playoutModel: PlayoutModel): void { - const span = context.startSpan('recalculateTTimerEstimates') +export function recalculateTTimerProjections(context: JobContext, playoutModel: PlayoutModel): void { + const span = context.startSpan('recalculateTTimerProjections') const playlist = playoutModel.playlist @@ -219,10 +219,10 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl const playablePartsSlice = getOrderedPartsAfterPlayhead(context, playoutModel, Infinity, true) if (playablePartsSlice.length === 0 && !currentPartInstance) { - // No parts to iterate through, clear estimates + // No parts to iterate through, clear projections for (const timer of tTimers) { if (timer.anchorPartId) { - playoutModel.updateTTimer({ ...timer, estimateState: undefined }) + playoutModel.updateTTimer({ ...timer, projectedState: undefined }) } } if (span) span.end() @@ -314,7 +314,7 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl for (const timerIndex of timersForThisPart) { const timer = tTimers[timerIndex - 1] - const estimateState: TimerState = isPushing + const projectedState: TimerState = isPushing ? literal({ paused: true, duration: anchorTime, @@ -326,7 +326,7 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl pauseTime: now + currentPartRemainingTime, // When current part ends and pushing begins }) - playoutModel.updateTTimer({ ...timer, estimateState }) + playoutModel.updateTTimer({ ...timer, projectedState }) } timerAnchors.delete(part._id) @@ -337,11 +337,11 @@ export function recalculateTTimerEstimates(context: JobContext, playoutModel: Pl segmentAccumulator += partDuration } - // Clear estimates for unresolved anchors + // Clear projections for unresolved anchors for (const [, timerIndices] of timerAnchors.entries()) { for (const timerIndex of timerIndices) { const timer = tTimers[timerIndex - 1] - playoutModel.updateTTimer({ ...timer, estimateState: undefined }) + playoutModel.updateTTimer({ ...timer, projectedState: undefined }) } } diff --git a/packages/job-worker/src/playout/tTimersJobs.ts b/packages/job-worker/src/playout/tTimersJobs.ts index b1fede76426..a639ea1db04 100644 --- a/packages/job-worker/src/playout/tTimersJobs.ts +++ b/packages/job-worker/src/playout/tTimersJobs.ts @@ -1,13 +1,13 @@ import { JobContext } from '../jobs/index.js' -import { recalculateTTimerEstimates } from './tTimers.js' +import { recalculateTTimerProjections } from './tTimers.js' import { runWithPlayoutModel, runWithPlaylistLock } from './lock.js' /** - * Handle RecalculateTTimerEstimates job - * This is called after setNext, takes, and ingest changes to update T-Timer estimates + * 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 handleRecalculateTTimerEstimates(context: JobContext): Promise { +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( { @@ -37,7 +37,7 @@ export async function handleRecalculateTTimerEstimates(context: JobContext): Pro } await runWithPlayoutModel(context, playlist, lock, null, async (playoutModel) => { - recalculateTTimerEstimates(context, playoutModel) + recalculateTTimerProjections(context, playoutModel) }) }) } diff --git a/packages/job-worker/src/workers/studio/child.ts b/packages/job-worker/src/workers/studio/child.ts index 138bfd10d0d..e01783a4ef7 100644 --- a/packages/job-worker/src/workers/studio/child.ts +++ b/packages/job-worker/src/workers/studio/child.ts @@ -80,7 +80,7 @@ export class StudioWorkerChild { // Queue initial T-Timer recalculation to set up timers after startup this.#queueJob( getStudioQueueName(this.#studioId), - StudioJobs.RecalculateTTimerEstimates, + StudioJobs.RecalculateTTimerProjections, undefined, undefined ).catch((err) => { diff --git a/packages/job-worker/src/workers/studio/jobs.ts b/packages/job-worker/src/workers/studio/jobs.ts index 7b66526a4d4..89928fd3b9f 100644 --- a/packages/job-worker/src/workers/studio/jobs.ts +++ b/packages/job-worker/src/workers/studio/jobs.ts @@ -49,7 +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 { handleRecalculateTTimerEstimates } from '../../playout/tTimersJobs.js' +import { handleRecalculateTTimerProjections } from '../../playout/tTimersJobs.js' type ExecutableFunction = ( context: JobContext, @@ -88,7 +88,7 @@ export const studioJobHandlers: StudioJobHandlers = { [StudioJobs.OnPlayoutPlaybackChanged]: handleOnPlayoutPlaybackChanged, [StudioJobs.OnTimelineTriggerTime]: handleTimelineTriggerTime, - [StudioJobs.RecalculateTTimerEstimates]: handleRecalculateTTimerEstimates, + [StudioJobs.RecalculateTTimerProjections]: handleRecalculateTTimerProjections, [StudioJobs.UpdateStudioBaseline]: handleUpdateStudioBaseline, [StudioJobs.CleanupEmptyPlaylists]: handleRemoveEmptyPlaylists, diff --git a/packages/webui/src/client/lib/tTimerUtils.ts b/packages/webui/src/client/lib/tTimerUtils.ts index 216e04a4c98..637c8d75e54 100644 --- a/packages/webui/src/client/lib/tTimerUtils.ts +++ b/packages/webui/src/client/lib/tTimerUtils.ts @@ -28,22 +28,22 @@ export function calculateTTimerDiff(timer: RundownTTimer, now: number): number { /** * Calculate the over/under difference between the timer's current value - * and its estimate. + * 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 estimate is available. + * Returns undefined if no projection is available. */ export function calculateTTimerOverUnder(timer: RundownTTimer, now: number): number | undefined { - if (!timer.state || !timer.estimateState) { + if (!timer.state || !timer.projectedState) { return undefined } const duration = timerStateToDuration(timer.state, now) - const estimateDuration = timerStateToDuration(timer.estimateState, now) + const projectedDuration = timerStateToDuration(timer.projectedState, now) - return estimateDuration - duration + return projectedDuration - duration } export function getDefaultTTimer(tTimers: [RundownTTimer, RundownTTimer, RundownTTimer]): RundownTTimer | undefined { From 95fd1b79cd3f956e79a1e08704c26215cc6c889c Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Thu, 29 Jan 2026 18:54:58 +0100 Subject: [PATCH 289/291] Add t-timers info to LSG --- .../activePlaylistEvent-example.yaml | 15 ++ .../activePlaylistEvent.yaml | 9 +- .../api/components/tTimers/tTimerIndex.yaml | 6 + .../tTimers/tTimerMode/tTimerMode.yaml | 55 ++++ .../tTimerModeCountdown-example.yaml | 5 + .../tTimerMode/tTimerModeFreeRun-example.yaml | 3 + .../tTimerStatus/tTimerStatus-example.yaml | 9 + .../tTimers/tTimerStatus/tTimerStatus.yaml | 23 ++ .../src/generated/asyncapi.yaml | 243 +++++++++++++----- .../src/generated/schema.ts | 77 ++++++ .../topics/__tests__/activePlaylist.spec.ts | 96 +++++++ .../src/topics/__tests__/utils.ts | 6 +- .../src/topics/activePlaylistTopic.ts | 67 +++++ 13 files changed, 552 insertions(+), 62 deletions(-) create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerIndex.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml create mode 100644 packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml 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 21e7277c149..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 @@ -46,7 +46,14 @@ $defs: $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..12c29a0740a --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml @@ -0,0 +1,55 @@ +$defs: + tTimerModeCountdown: + type: object + title: TTimerModeCountdown + description: Countdown timer mode - counts down from a duration + properties: + type: + type: string + const: countdown + startTime: + description: Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + type: number + pauseTime: + description: Unix timestamp when paused (milliseconds), or null if running + oneOf: + - type: number + - type: 'null' + durationMs: + description: Total countdown duration in milliseconds + type: number + stopAtZero: + description: Whether timer stops at zero or continues into negative values + type: boolean + required: [type, startTime, pauseTime, 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 + startTime: + description: Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + type: number + pauseTime: + description: Unix timestamp when paused (milliseconds), or null if running + oneOf: + - type: number + - type: 'null' + required: [type, startTime, pauseTime] + 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..3f76f9cfdf8 --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeCountdown-example.yaml @@ -0,0 +1,5 @@ +type: countdown +startTime: 1706371800000 +pauseTime: null +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..34cf8b5671a --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerModeFreeRun-example.yaml @@ -0,0 +1,3 @@ +type: freeRun +startTime: 1706371900000 +pauseTime: 1706372000000 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..6a95e8daa1c --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus-example.yaml @@ -0,0 +1,9 @@ +index: 1 +label: 'Segment Timer' +configured: true +mode: + type: countdown + startTime: 1706371800000 + pauseTime: null + durationMs: 120000 + stopAtZero: true 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..31007f3f86b --- /dev/null +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml @@ -0,0 +1,23 @@ +$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' + required: [index, label, configured] + additionalProperties: false + examples: + - $ref: './tTimerStatus-example.yaml' diff --git a/packages/live-status-gateway-api/src/generated/asyncapi.yaml b/packages/live-status-gateway-api/src/generated/asyncapi.yaml index 71d5675007d..1d6857aff88 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: @@ -537,7 +537,7 @@ channels: - type: object title: CurrentSegment allOf: - - &a32 + - &a34 title: SegmentBase type: object properties: @@ -556,7 +556,7 @@ channels: title: CurrentSegmentTiming description: Timing information about the current segment allOf: - - &a33 + - &a35 type: object title: SegmentTiming properties: @@ -739,6 +739,115 @@ 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 + startTime: + description: Unix timestamp when timer started (milliseconds). May be adjusted + when pausing/resuming. + type: number + pauseTime: + description: Unix timestamp when paused (milliseconds), or null if running + oneOf: + - type: number + - type: "null" + durationMs: + description: Total countdown duration in milliseconds + type: number + stopAtZero: + description: Whether timer stops at zero or continues into negative values + type: boolean + required: + - type + - startTime + - pauseTime + - durationMs + - stopAtZero + additionalProperties: false + examples: + - &a29 + type: countdown + startTime: 1706371800000 + pauseTime: null + durationMs: 120000 + stopAtZero: true + - type: object + title: TTimerModeFreeRun + description: Free-running timer mode - counts up from start time + properties: + type: + type: string + const: freeRun + startTime: + description: Unix timestamp when timer started (milliseconds). May be adjusted + when pausing/resuming. + type: number + pauseTime: + description: Unix timestamp when paused (milliseconds), or null if running + oneOf: + - type: number + - type: "null" + required: + - type + - startTime + - pauseTime + additionalProperties: false + examples: + - &a30 + type: freeRun + startTime: 1706371900000 + pauseTime: 1706372000000 + required: + - index + - label + - configured + additionalProperties: false + examples: + - index: 1 + label: Segment Timer + configured: true + mode: + type: countdown + startTime: 1706371800000 + pauseTime: null + durationMs: 120000 + stopAtZero: true + minItems: 3 + maxItems: 3 required: - event - id @@ -749,9 +858,10 @@ channels: - currentSegment - nextPart - timing + - tTimers additionalProperties: false examples: - - &a29 + - &a31 event: activePlaylist id: OKAgZmZ0Buc99lE_2uPPSKVbMrQ_ externalId: 1ZIYVYL1aEkNEJbeGsmRXr5s8wtkyxfPRjNSTxZfcoEI @@ -765,8 +875,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: @@ -791,20 +914,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: @@ -833,7 +956,7 @@ channels: type: object title: Segment allOf: - - *a32 + - *a34 - type: object title: Segment properties: @@ -847,7 +970,7 @@ channels: name: description: Name of the segment type: string - timing: *a33 + timing: *a35 publicData: description: Optional arbitrary data required: @@ -860,7 +983,7 @@ channels: - name - timing examples: - - &a34 + - &a36 identifier: Segment 0 identifier rundownId: y9HauyWkcxQS3XaAOsW40BRLLsI_ name: Segment 0 @@ -876,13 +999,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: @@ -912,7 +1035,7 @@ channels: items: title: AdLibStatus allOf: - - &a40 + - &a42 title: AdLibBase type: object properties: @@ -947,7 +1070,7 @@ channels: - label additionalProperties: false examples: - - &a36 + - &a38 name: pvw label: Preview tags: @@ -967,15 +1090,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 @@ -1007,15 +1130,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 @@ -1039,9 +1162,9 @@ channels: items: title: GlobalAdLibStatus allOf: - - *a40 + - *a42 examples: - - *a41 + - *a43 required: - event - rundownPlaylistId @@ -1049,15 +1172,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 @@ -1165,7 +1288,7 @@ channels: - pieceOrAdLibId additionalProperties: false examples: - - &a44 + - &a46 packageName: MV000123.mxf status: ok rundownId: y9HauyWkcxQS3XaAOsW40BRLLsI_ @@ -1183,7 +1306,7 @@ channels: - event: packages rundownPlaylistId: y9HauyWkcxQS3XaAOsW40BRLLsI_ packages: - - *a44 + - *a46 buckets: description: Buckets schema for websocket subscriptions subscribe: @@ -1219,7 +1342,7 @@ channels: items: title: BucketAdLibStatus allOf: - - *a40 + - *a42 - type: object title: BucketAdLibStatus properties: @@ -1230,14 +1353,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 @@ -1261,22 +1384,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: @@ -1340,7 +1463,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: &a48 + enum: &a50 - rundown - playlist - partInstance @@ -1352,7 +1475,7 @@ channels: type: string additionalProperties: false examples: - - &a49 + - &a51 type: rundown studioId: studio01 rundownId: rd123 @@ -1368,14 +1491,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 @@ -1392,7 +1515,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 studioId: type: string rundownId: @@ -1401,7 +1524,7 @@ channels: type: string additionalProperties: false examples: - - &a51 + - &a53 type: partInstance studioId: studio01 rundownId: rd123 @@ -1420,7 +1543,7 @@ channels: type: string title: NotificationTargetType description: Possible NotificationTarget types - enum: *a48 + enum: *a50 studioId: type: string rundownId: @@ -1431,7 +1554,7 @@ channels: type: string additionalProperties: false examples: - - &a52 + - &a54 type: pieceInstance studioId: studio01 rundownId: rd123 @@ -1447,17 +1570,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 @@ -1468,11 +1591,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: @@ -1480,9 +1603,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 dca47bd84fd..9231fd183ed 100644 --- a/packages/live-status-gateway-api/src/generated/schema.ts +++ b/packages/live-status-gateway-api/src/generated/schema.ts @@ -190,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 { @@ -477,6 +481,75 @@ 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 +} + +/** + * 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' + /** + * Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + */ + startTime: number + /** + * Unix timestamp when paused (milliseconds), or null if running + */ + pauseTime: number | null + /** + * Total countdown duration in milliseconds + */ + 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' + /** + * Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + */ + startTime: number + /** + * Unix timestamp when paused (milliseconds), or null if running + */ + pauseTime: number | null +} + interface ActivePiecesEvent { event: 'activePieces' /** @@ -948,6 +1021,10 @@ export { ActivePlaylistQuickLoop, QuickLoopMarker, QuickLoopMarkerType, + TTimerStatus, + TTimerIndex, + TTimerModeCountdown, + TTimerModeFreeRun, ActivePiecesEvent, SegmentsEvent, Segment, 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..8e08f69837a 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 }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: 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 }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: 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 }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + ], } // eslint-disable-next-line @typescript-eslint/unbound-method @@ -278,4 +294,84 @@ 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', + startTime: 1600000000000, + pauseTime: null, + duration: 60000, + stopAtZero: true, + }, + }, + { + index: 2, + label: 'Paused FreeRun', + mode: { + type: 'freeRun', + startTime: 1600000010000, + pauseTime: 1600000020000, + }, + }, + { index: 3, label: '', mode: 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 countdown timer transformation + expect(receivedStatus.tTimers[0]).toEqual({ + index: TTimerIndex.NUMBER_1, + label: 'Countdown Timer', + configured: true, + mode: { + type: 'countdown', + startTime: 1600000000000, + pauseTime: null, + durationMs: 60000, + stopAtZero: true, + }, + }) + + // Verify paused freeRun timer transformation + expect(receivedStatus.tTimers[1]).toEqual({ + index: TTimerIndex.NUMBER_2, + label: 'Paused FreeRun', + configured: true, + mode: { + type: 'freeRun', + startTime: 1600000010000, + pauseTime: 1600000020000, + }, + }) + + // Verify unconfigured timer + expect(receivedStatus.tTimers[2]).toEqual({ + index: TTimerIndex.NUMBER_3, + label: '', + configured: false, + mode: null, + }) + }) }) diff --git a/packages/live-status-gateway/src/topics/__tests__/utils.ts b/packages/live-status-gateway/src/topics/__tests__/utils.ts index 23b70507c10..444acd33856 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -34,7 +34,11 @@ export function makeTestPlaylist(id?: string): DBRundownPlaylist { studioId: protectString('STUDIO_1'), timing: { type: PlaylistTimingType.None }, publicData: { a: 'b' }, - tTimers: [] as any, + tTimers: [ + { index: 1, label: '', mode: null }, + { index: 2, label: '', mode: null }, + { index: 3, label: '', mode: null }, + ], } } diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index aa4b0093292..4e2fe65a99d 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,6 +31,8 @@ import { ActivePlaylistQuickLoop, QuickLoopMarker as QuickLoopMarkerStatus, QuickLoopMarkerType as QuickLoopMarkerStatusType, + TTimerStatus, + TTimerIndex, } from '@sofie-automation/live-status-gateway-api' import { CollectionHandlers } from '../liveStatusServer.js' @@ -51,6 +54,7 @@ const PLAYLIST_KEYS = [ 'timing', 'startedPlayback', 'quickLoop', + 'tTimers', ] as const type Playlist = PickKeys @@ -172,6 +176,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket ? this._activePlaylist.timing.expectedEnd : undefined, }, + tTimers: this.transformTTimers(this._activePlaylist.tTimers), }) : literal>({ event: 'activePlaylist', @@ -188,6 +193,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket timing: { timingMode: ActivePlaylistTimingMode.NONE, }, + tTimers: this.transformTTimers(undefined), }) this.sendMessage(subscribers, message) @@ -207,6 +213,67 @@ 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 }, + { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, + { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: 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 + + if (!timer.mode) { + return { + index, + label: timer.label, + configured: false, + mode: null, + } + } + + if (timer.mode.type === 'countdown') { + return { + index, + label: timer.label, + configured: true, + mode: { + type: 'countdown', + startTime: timer.mode.startTime, + pauseTime: timer.mode.pauseTime, + durationMs: timer.mode.duration, + stopAtZero: timer.mode.stopAtZero, + }, + } + } else { + return { + index, + label: timer.label, + configured: true, + mode: { + type: 'freeRun', + startTime: timer.mode.startTime, + pauseTime: timer.mode.pauseTime, + }, + } + } + } + private transformQuickLoopMarkerStatus(marker: QuickLoopMarker | undefined): QuickLoopMarkerStatus | undefined { if (!marker) return undefined From 9d92b26ebc5217815a0206c402e58b8eb81d418a Mon Sep 17 00:00:00 2001 From: Maciek Antek Papiewski <41971218+anteeek@users.noreply.github.com> Date: Tue, 10 Feb 2026 05:24:11 +0100 Subject: [PATCH 290/291] SOFIE-261 | add projected state info to LSG topic about t-timers --- .../tTimers/tTimerMode/tTimerMode.yaml | 44 +++++---- .../tTimerModeCountdown-example.yaml | 4 +- .../tTimerMode/tTimerModeFreeRun-example.yaml | 4 +- .../tTimerStatus/tTimerStatus-example.yaml | 8 +- .../tTimers/tTimerStatus/tTimerStatus.yaml | 28 ++++++ .../src/generated/asyncapi.yaml | 89 +++++++++++++------ .../src/generated/schema.ts | 53 +++++++++-- .../topics/__tests__/activePlaylist.spec.ts | 49 ++++++---- .../src/topics/__tests__/utils.ts | 6 +- .../src/topics/activePlaylistTopic.ts | 79 ++++++++++++---- 10 files changed, 267 insertions(+), 97 deletions(-) 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 index 12c29a0740a..1cb36d05a7a 100644 --- a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml @@ -7,21 +7,26 @@ $defs: type: type: string const: countdown - startTime: - description: Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + 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 + remainingMs: + description: >- + Frozen remaining duration in milliseconds. + Present when paused is true. type: number - pauseTime: - description: Unix timestamp when paused (milliseconds), or null if running - oneOf: - - type: number - - type: 'null' durationMs: - description: Total countdown duration in milliseconds + 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, startTime, pauseTime, durationMs, stopAtZero] + required: [type, paused, durationMs, stopAtZero] additionalProperties: false examples: - $ref: './tTimerModeCountdown-example.yaml' @@ -34,15 +39,20 @@ $defs: type: type: string const: freeRun - startTime: - description: Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + 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 + elapsedMs: + description: >- + Frozen elapsed time in milliseconds. + Present when paused is true. type: number - pauseTime: - description: Unix timestamp when paused (milliseconds), or null if running - oneOf: - - type: number - - type: 'null' - required: [type, startTime, pauseTime] + required: [type, paused] additionalProperties: false examples: - $ref: './tTimerModeFreeRun-example.yaml' 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 index 3f76f9cfdf8..bcc642bbe7f 100644 --- 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 @@ -1,5 +1,5 @@ type: countdown -startTime: 1706371800000 -pauseTime: null +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 index 34cf8b5671a..1cad209ada8 100644 --- 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 @@ -1,3 +1,3 @@ type: freeRun -startTime: 1706371900000 -pauseTime: 1706372000000 +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 index 6a95e8daa1c..94e3027369d 100644 --- 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 @@ -3,7 +3,11 @@ label: 'Segment Timer' configured: true mode: type: countdown - startTime: 1706371800000 - pauseTime: null + 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 index 31007f3f86b..10c9f17645b 100644 --- a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml @@ -17,6 +17,34 @@ $defs: 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 + 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: diff --git a/packages/live-status-gateway-api/src/generated/asyncapi.yaml b/packages/live-status-gateway-api/src/generated/asyncapi.yaml index 1d6857aff88..4d1359f4d6f 100644 --- a/packages/live-status-gateway-api/src/generated/asyncapi.yaml +++ b/packages/live-status-gateway-api/src/generated/asyncapi.yaml @@ -776,33 +776,37 @@ channels: type: type: string const: countdown - startTime: - description: Unix timestamp when timer started (milliseconds). May be adjusted - when pausing/resuming. + 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 + remainingMs: + description: Frozen remaining duration in milliseconds. Present when paused is + true. type: number - pauseTime: - description: Unix timestamp when paused (milliseconds), or null if running - oneOf: - - type: number - - type: "null" durationMs: - description: Total countdown duration in milliseconds + 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 - - startTime - - pauseTime + - paused - durationMs - stopAtZero additionalProperties: false examples: - &a29 type: countdown - startTime: 1706371800000 - pauseTime: null + paused: false + zeroTime: 1706371920000 durationMs: 120000 stopAtZero: true - type: object @@ -812,25 +816,52 @@ channels: type: type: string const: freeRun - startTime: - description: Unix timestamp when timer started (milliseconds). May be adjusted - when pausing/resuming. + 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 + elapsedMs: + description: Frozen elapsed time in milliseconds. Present when paused is true. type: number - pauseTime: - description: Unix timestamp when paused (milliseconds), or null if running - oneOf: - - type: number - - type: "null" required: - type - - startTime - - pauseTime + - paused additionalProperties: false examples: - &a30 type: freeRun - startTime: 1706371900000 - pauseTime: 1706372000000 + 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 + 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 @@ -842,10 +873,14 @@ channels: configured: true mode: type: countdown - startTime: 1706371800000 - pauseTime: null + paused: false + zeroTime: 1706371920000 durationMs: 120000 stopAtZero: true + projected: + paused: false + zeroTime: 1706371920000 + anchorPartId: part_break_1 minItems: 3 maxItems: 3 required: diff --git a/packages/live-status-gateway-api/src/generated/schema.ts b/packages/live-status-gateway-api/src/generated/schema.ts index 9231fd183ed..2a250e965f0 100644 --- a/packages/live-status-gateway-api/src/generated/schema.ts +++ b/packages/live-status-gateway-api/src/generated/schema.ts @@ -501,6 +501,14 @@ interface TTimerStatus { * 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 } /** @@ -518,15 +526,19 @@ enum TTimerIndex { interface TTimerModeCountdown { type: 'countdown' /** - * Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + * Whether the timer is currently paused */ - startTime: number + paused: boolean /** - * Unix timestamp when paused (milliseconds), or null if running + * Unix timestamp (ms) when the timer reaches/reached zero. Present when paused is false. The client calculates remaining time as zeroTime - Date.now(). */ - pauseTime: number | null + zeroTime?: number /** - * Total countdown duration in milliseconds + * Frozen remaining duration in milliseconds. Present when paused is true. + */ + remainingMs?: number + /** + * Total countdown duration in milliseconds (the original configured duration) */ durationMs: number /** @@ -541,13 +553,35 @@ interface TTimerModeCountdown { interface TTimerModeFreeRun { type: 'freeRun' /** - * Unix timestamp when timer started (milliseconds). May be adjusted when pausing/resuming. + * Whether the timer is currently paused */ - startTime: number + 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 + /** + * 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 when paused (milliseconds), or null if running + * Frozen remaining duration projection in milliseconds */ - pauseTime: number | null + durationMs?: number } interface ActivePiecesEvent { @@ -1025,6 +1059,7 @@ export { TTimerIndex, TTimerModeCountdown, TTimerModeFreeRun, + TTimerProjected, ActivePiecesEvent, SegmentsEvent, Segment, 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 8e08f69837a..96bb540d775 100644 --- a/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts +++ b/packages/live-status-gateway/src/topics/__tests__/activePlaylist.spec.ts @@ -65,9 +65,9 @@ describe('ActivePlaylistTopic', () => { }, quickLoop: undefined, tTimers: [ - { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + { 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 }, ], } @@ -171,9 +171,9 @@ describe('ActivePlaylistTopic', () => { }, quickLoop: undefined, tTimers: [ - { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + { 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 }, ], } @@ -282,9 +282,9 @@ describe('ActivePlaylistTopic', () => { }, quickLoop: undefined, tTimers: [ - { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + { 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 }, ], } @@ -309,22 +309,23 @@ describe('ActivePlaylistTopic', () => { label: 'Countdown Timer', mode: { type: 'countdown', - startTime: 1600000000000, - pauseTime: null, 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', - startTime: 1600000010000, - pauseTime: 1600000020000, }, + state: { paused: true, duration: 10000 }, + projectedState: { paused: true, duration: 5000 }, }, - { index: 3, label: '', mode: null }, + { index: 3, label: '', mode: null, state: null }, ] handlers.playlistHandler.notify(playlist) @@ -340,18 +341,23 @@ describe('ActivePlaylistTopic', () => { expect(mockSubscriber.send).toHaveBeenCalledTimes(1) const receivedStatus = JSON.parse(mockSubscriber.send.mock.calls[0][0] as string) as ActivePlaylistEvent - // Verify countdown timer transformation + // Verify running countdown timer transformation expect(receivedStatus.tTimers[0]).toEqual({ index: TTimerIndex.NUMBER_1, label: 'Countdown Timer', configured: true, mode: { type: 'countdown', - startTime: 1600000000000, - pauseTime: null, + paused: false, + zeroTime: 1600000060000, durationMs: 60000, stopAtZero: true, }, + projected: { + paused: false, + zeroTime: 1600000060000, + }, + anchorPartId: 'PART_BREAK', }) // Verify paused freeRun timer transformation @@ -361,8 +367,12 @@ describe('ActivePlaylistTopic', () => { configured: true, mode: { type: 'freeRun', - startTime: 1600000010000, - pauseTime: 1600000020000, + paused: true, + elapsedMs: 10000, + }, + projected: { + paused: true, + durationMs: 5000, }, }) @@ -372,6 +382,7 @@ describe('ActivePlaylistTopic', () => { 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 444acd33856..7afa37e1104 100644 --- a/packages/live-status-gateway/src/topics/__tests__/utils.ts +++ b/packages/live-status-gateway/src/topics/__tests__/utils.ts @@ -35,9 +35,9 @@ export function makeTestPlaylist(id?: string): DBRundownPlaylist { timing: { type: PlaylistTimingType.None }, publicData: { a: 'b' }, tTimers: [ - { index: 1, label: '', mode: null }, - { index: 2, label: '', mode: null }, - { index: 3, label: '', mode: null }, + { 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/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index 4e2fe65a99d..1894ab14ec9 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -32,6 +32,9 @@ import { QuickLoopMarker as QuickLoopMarkerStatus, QuickLoopMarkerType as QuickLoopMarkerStatusType, TTimerStatus, + TTimerProjected, + TTimerModeCountdown, + TTimerModeFreeRun, TTimerIndex, } from '@sofie-automation/live-status-gateway-api' @@ -222,9 +225,9 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket if (!tTimers) { // Return 3 unconfigured timers when no playlist is active return [ - { index: TTimerIndex.NUMBER_1, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_2, label: '', configured: false, mode: null }, - { index: TTimerIndex.NUMBER_3, label: '', configured: false, mode: null }, + { 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 }, ] } @@ -238,38 +241,82 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket const index = timer.index === 1 ? TTimerIndex.NUMBER_1 : timer.index === 2 ? TTimerIndex.NUMBER_2 : TTimerIndex.NUMBER_3 - if (!timer.mode) { + 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, + durationMs: timer.mode.duration, + stopAtZero: timer.mode.stopAtZero, + } + : { + type: 'countdown', + paused: false, + zeroTime: timer.state.zeroTime, + durationMs: timer.mode.duration, + stopAtZero: timer.mode.stopAtZero, + } return { index, label: timer.label, configured: true, - mode: { - type: 'countdown', - startTime: timer.mode.startTime, - pauseTime: timer.mode.pauseTime, - durationMs: timer.mode.duration, - stopAtZero: timer.mode.stopAtZero, - }, + mode, + projected, + anchorPartId, } } else { + const mode: TTimerModeFreeRun = timer.state.paused + ? { + type: 'freeRun', + paused: true, + elapsedMs: timer.state.duration, + } + : { + type: 'freeRun', + paused: false, + zeroTime: timer.state.zeroTime, + } return { index, label: timer.label, configured: true, - mode: { - type: 'freeRun', - startTime: timer.mode.startTime, - pauseTime: timer.mode.pauseTime, - }, + 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, + } + } else { + return { + paused: false, + zeroTime: projectedState.zeroTime, } } } From dea7a63c0654209244be4b5b4fad8d1937d73171 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Tue, 17 Mar 2026 20:19:54 +0000 Subject: [PATCH 291/291] Add pauseTime to LSG --- .../tTimers/tTimerMode/tTimerMode.yaml | 12 ++++++++++ .../tTimers/tTimerStatus/tTimerStatus.yaml | 5 +++++ .../src/generated/asyncapi.yaml | 22 +++++++++++++++++++ .../src/generated/schema.ts | 12 ++++++++++ .../src/topics/activePlaylistTopic.ts | 6 +++++ 5 files changed, 57 insertions(+) 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 index 1cb36d05a7a..76f7301261c 100644 --- a/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerMode/tTimerMode.yaml @@ -15,6 +15,12 @@ $defs: 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. @@ -47,6 +53,12 @@ $defs: 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. 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 index 10c9f17645b..acd2c344a78 100644 --- a/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml +++ b/packages/live-status-gateway-api/api/components/tTimers/tTimerStatus/tTimerStatus.yaml @@ -35,6 +35,11 @@ $defs: 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 diff --git a/packages/live-status-gateway-api/src/generated/asyncapi.yaml b/packages/live-status-gateway-api/src/generated/asyncapi.yaml index 4d1359f4d6f..133cc509fb9 100644 --- a/packages/live-status-gateway-api/src/generated/asyncapi.yaml +++ b/packages/live-status-gateway-api/src/generated/asyncapi.yaml @@ -785,6 +785,14 @@ channels: 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. @@ -825,6 +833,14 @@ channels: 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 @@ -853,6 +869,12 @@ channels: 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 diff --git a/packages/live-status-gateway-api/src/generated/schema.ts b/packages/live-status-gateway-api/src/generated/schema.ts index 2a250e965f0..60f039b9935 100644 --- a/packages/live-status-gateway-api/src/generated/schema.ts +++ b/packages/live-status-gateway-api/src/generated/schema.ts @@ -533,6 +533,10 @@ interface TTimerModeCountdown { * 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. */ @@ -560,6 +564,10 @@ interface TTimerModeFreeRun { * 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. */ @@ -578,6 +586,10 @@ interface TTimerProjected { * 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 */ diff --git a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts index 1894ab14ec9..506c2d71d5f 100644 --- a/packages/live-status-gateway/src/topics/activePlaylistTopic.ts +++ b/packages/live-status-gateway/src/topics/activePlaylistTopic.ts @@ -261,6 +261,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket type: 'countdown', paused: true, remainingMs: timer.state.duration, + pauseTime: undefined, durationMs: timer.mode.duration, stopAtZero: timer.mode.stopAtZero, } @@ -268,6 +269,7 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket type: 'countdown', paused: false, zeroTime: timer.state.zeroTime, + pauseTime: timer.state.pauseTime ?? undefined, durationMs: timer.mode.duration, stopAtZero: timer.mode.stopAtZero, } @@ -285,11 +287,13 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket 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, @@ -312,11 +316,13 @@ export class ActivePlaylistTopic extends WebSocketTopicBase implements WebSocket return { paused: true, durationMs: projectedState.duration, + pauseTime: projectedState.pauseTime ?? undefined, } } else { return { paused: false, zeroTime: projectedState.zeroTime, + pauseTime: projectedState.pauseTime ?? undefined, } } }