Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions meteor/__mocks__/defaultCollectionObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export function defaultStudio(_id: StudioId): DBStudio {
mediaPreviewsUrl: '',
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
fallbackPartDuration: DEFAULT_FALLBACK_PART_DURATION,
allowHold: false,
allowPieceDirectPlay: false,
}),
_rundownVersionHash: '',
routeSetsWithOverrides: wrapDefaultObject({}),
Expand Down
14 changes: 10 additions & 4 deletions meteor/server/api/rest/v1/typeConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PeripheralDevice, PeripheralDeviceType } from '@sofie-automation/coreli
import { Blueprint } from '@sofie-automation/corelib/dist/dataModel/Blueprint'
import { BucketId, ShowStyleBaseId, ShowStyleVariantId, StudioId } from '@sofie-automation/corelib/dist/dataModel/Ids'
import { DBStudio, IStudioSettings } from '@sofie-automation/corelib/dist/dataModel/Studio'
import { assertNever, getRandomId, literal } from '@sofie-automation/corelib/dist/lib'
import { assertNever, Complete, getRandomId, literal } from '@sofie-automation/corelib/dist/lib'
import { protectString, unprotectString } from '@sofie-automation/corelib/dist/protectedString'
import {
applyAndValidateOverrides,
Expand Down Expand Up @@ -296,7 +296,7 @@ export async function studioFrom(apiStudio: APIStudio, existingId?: StudioId): P
}
}

export function APIStudioFrom(studio: DBStudio): APIStudio {
export function APIStudioFrom(studio: DBStudio): Complete<APIStudio> {
const studioSettings = APIStudioSettingsFrom(applyAndValidateOverrides(studio.settingsWithOverrides).obj)

return {
Expand All @@ -309,7 +309,7 @@ export function APIStudioFrom(studio: DBStudio): APIStudio {
}
}

export function studioSettingsFrom(apiStudioSettings: APIStudioSettings): IStudioSettings {
export function studioSettingsFrom(apiStudioSettings: APIStudioSettings): Complete<IStudioSettings> {
return {
frameRate: apiStudioSettings.frameRate,
mediaPreviewsUrl: apiStudioSettings.mediaPreviewsUrl,
Expand All @@ -325,10 +325,13 @@ export function studioSettingsFrom(apiStudioSettings: APIStudioSettings): IStudi
enableQuickLoop: apiStudioSettings.enableQuickLoop,
forceQuickLoopAutoNext: forceQuickLoopAutoNextFrom(apiStudioSettings.forceQuickLoopAutoNext),
fallbackPartDuration: apiStudioSettings.fallbackPartDuration ?? DEFAULT_FALLBACK_PART_DURATION,
allowAdlibTestingSegment: apiStudioSettings.allowAdlibTestingSegment,
allowHold: apiStudioSettings.allowHold ?? true, // Backwards compatible
allowPieceDirectPlay: apiStudioSettings.allowPieceDirectPlay ?? true, // Backwards compatible
}
}

export function APIStudioSettingsFrom(settings: IStudioSettings): APIStudioSettings {
export function APIStudioSettingsFrom(settings: IStudioSettings): Complete<APIStudioSettings> {
return {
frameRate: settings.frameRate,
mediaPreviewsUrl: settings.mediaPreviewsUrl,
Expand All @@ -344,6 +347,9 @@ export function APIStudioSettingsFrom(settings: IStudioSettings): APIStudioSetti
enableQuickLoop: settings.enableQuickLoop,
forceQuickLoopAutoNext: APIForceQuickLoopAutoNextFrom(settings.forceQuickLoopAutoNext),
fallbackPartDuration: settings.fallbackPartDuration,
allowAdlibTestingSegment: settings.allowAdlibTestingSegment,
allowHold: settings.allowHold,
allowPieceDirectPlay: settings.allowPieceDirectPlay,
}
}

Expand Down
2 changes: 2 additions & 0 deletions meteor/server/api/studio/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export async function insertStudioInner(organizationId: OrganizationId | null, n
frameRate: 25,
mediaPreviewsUrl: '',
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: false,
allowPieceDirectPlay: false,
}),
_rundownVersionHash: '',
routeSetsWithOverrides: wrapDefaultObject({}),
Expand Down
3 changes: 3 additions & 0 deletions meteor/server/lib/rest/v1/studios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,7 @@ export interface APIStudioSettings {
forceQuickLoopAutoNext?: 'disabled' | 'enabled_when_valid_duration' | 'enabled_forcing_min_duration'
minimumTakeSpan?: number
fallbackPartDuration?: number
allowAdlibTestingSegment?: boolean
allowHold?: boolean
allowPieceDirectPlay?: boolean
}
2 changes: 2 additions & 0 deletions meteor/server/migration/0_1_0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const addSteps = addMigrationSteps('0.1.0', [
frameRate: 25,
mediaPreviewsUrl: '',
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: false,
allowPieceDirectPlay: false,
}),
mappingsWithOverrides: wrapDefaultObject({}),
blueprintConfigWithOverrides: wrapDefaultObject({}),
Expand Down
37 changes: 37 additions & 0 deletions meteor/server/migration/X_X_X.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,43 @@ export const addSteps = addMigrationSteps(CURRENT_SYSTEM_VERSION, [
}
},
},

{
id: `add studio settings allowHold & allowPieceDirectPlay`,
canBeRunAutomatically: true,
validate: async () => {
const studios = await Studios.findFetchAsync({
$or: [
{ 'settings.allowHold': { $exists: false } },
{ 'settings.allowPieceDirectPlay': { $exists: false } },
],
})

if (studios.length > 0) {
return 'studios must have settings.allowHold and settings.allowPieceDirectPlay defined'
}

return false
},
migrate: async () => {
const studios = await Studios.findFetchAsync({
$or: [
{ 'settings.allowHold': { $exists: false } },
{ 'settings.allowPieceDirectPlay': { $exists: false } },
],
})

for (const studio of studios) {
// Populate the settings to be backwards compatible
await Studios.updateAsync(studio._id, {
$set: {
'settings.allowHold': true,
'settings.allowPieceDirectPlay': true,
},
})
}
},
},
])

interface PartialOldICoreSystem {
Expand Down
6 changes: 6 additions & 0 deletions meteor/server/migration/__tests__/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ describe('Migrations', () => {
mediaPreviewsUrl: '',
frameRate: 25,
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: true,
allowPieceDirectPlay: true,
}),
mappingsWithOverrides: wrapDefaultObject({}),
blueprintConfigWithOverrides: wrapDefaultObject({}),
Expand Down Expand Up @@ -163,6 +165,8 @@ describe('Migrations', () => {
mediaPreviewsUrl: '',
frameRate: 25,
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: true,
allowPieceDirectPlay: true,
}),
mappingsWithOverrides: wrapDefaultObject({}),
blueprintConfigWithOverrides: wrapDefaultObject({}),
Expand Down Expand Up @@ -201,6 +205,8 @@ describe('Migrations', () => {
mediaPreviewsUrl: '',
frameRate: 25,
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: true,
allowPieceDirectPlay: true,
}),
mappingsWithOverrides: wrapDefaultObject({}),
blueprintConfigWithOverrides: wrapDefaultObject({}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,7 @@ describe('lib/mediaObjects', () => {
test('getAcceptedFormats', () => {
const acceptedFormats = getAcceptedFormats({
supportedMediaFormats: '1920x1080i5000, 1280x720, i5000, i5000tff',
mediaPreviewsUrl: '',
frameRate: 25,
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
})
expect(acceptedFormats).toEqual([
['1920', '1080', 'i', '5000', undefined],
Expand Down Expand Up @@ -251,6 +249,8 @@ describe('lib/mediaObjects', () => {
supportedAudioStreams: '4',
frameRate: 25,
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: false,
allowPieceDirectPlay: false,
}

const mockDefaultStudio = defaultStudio(protectString('studio0'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ export function acceptFormat(format: string, formats: Array<Array<string>>): boo
* [undefined, undefined, i, 5000, tff]
* ]
*/
export function getAcceptedFormats(settings: IStudioSettings | undefined): Array<Array<string>> {
export function getAcceptedFormats(
settings: Pick<IStudioSettings, 'supportedMediaFormats' | 'frameRate'> | undefined
): Array<Array<string>> {
const formatsConfigField = settings ? settings.supportedMediaFormats : ''
const formatsString: string =
(formatsConfigField && formatsConfigField !== '' ? formatsConfigField : '1920x1080i5000') + ''
Expand Down
2 changes: 2 additions & 0 deletions packages/job-worker/src/__mocks__/defaultCollectionObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export function defaultStudio(_id: StudioId): DBStudio {
mediaPreviewsUrl: '',
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowAdlibTestingSegment: true,
allowHold: true,
allowPieceDirectPlay: true,
}),
routeSetsWithOverrides: wrapDefaultObject({}),
routeSetExclusivityGroupsWithOverrides: wrapDefaultObject({}),
Expand Down
4 changes: 4 additions & 0 deletions packages/job-worker/src/blueprints/__tests__/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ describe('Test blueprint config', () => {
mediaPreviewsUrl: '',
frameRate: 25,
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: true,
allowPieceDirectPlay: true,
}),
blueprintConfigWithOverrides: wrapDefaultObject({ sdfsdf: 'one', another: 5 }),
})
Expand All @@ -38,6 +40,8 @@ describe('Test blueprint config', () => {
mediaPreviewsUrl: '',
frameRate: 25,
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: true,
allowPieceDirectPlay: true,
}),
blueprintConfigWithOverrides: wrapDefaultObject({ sdfsdf: 'one', another: 5 }),
})
Expand Down
6 changes: 6 additions & 0 deletions packages/job-worker/src/playout/adlibJobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ import { PlayoutPieceInstanceModel } from './model/PlayoutPieceInstanceModel'
* Play an existing Piece in the Rundown as an AdLib
*/
export async function handleTakePieceAsAdlibNow(context: JobContext, data: TakePieceAsAdlibNowProps): Promise<void> {
if (!context.studio.settings.allowPieceDirectPlay) {
// Piece direct play isn't allowed, making this a noop
logger.debug(`Piece direct play isn't allowed, skipping`)
return
}

return runJobWithPlayoutModel(
context,
data,
Expand Down
9 changes: 9 additions & 0 deletions packages/job-worker/src/playout/holdJobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,18 @@ import { ActivateHoldProps, DeactivateHoldProps } from '@sofie-automation/coreli
import { JobContext } from '../jobs'
import { runJobWithPlayoutModel } from './lock'
import { updateTimeline } from './timeline/generate'
import { logger } from '../logging'

/**
* Activate Hold
*/
export async function handleActivateHold(context: JobContext, data: ActivateHoldProps): Promise<void> {
if (!context.studio.settings.allowHold) {
// Hold isn't allowed, making this a noop
logger.debug(`Hold isn't allowed, skipping`)
return
}

return runJobWithPlayoutModel(
context,
data,
Expand Down Expand Up @@ -59,6 +66,8 @@ export async function handleActivateHold(context: JobContext, data: ActivateHold
* Deactivate Hold
*/
export async function handleDeactivateHold(context: JobContext, data: DeactivateHoldProps): Promise<void> {
// This should be possible even when hold is not allowed, as it is a way to get out of a stuck state

return runJobWithPlayoutModel(
context,
data,
Expand Down
2 changes: 2 additions & 0 deletions packages/job-worker/src/playout/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ export async function handleBlueprintUpgradeForStudio(context: JobContext, _data
frameRate: 25,
mediaPreviewsUrl: '',
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: true,
allowPieceDirectPlay: true,
}

await context.directCollections.Studios.update(context.studioId, {
Expand Down
9 changes: 9 additions & 0 deletions packages/openapi/api/definitions/studios.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,15 @@ components:
fallbackPartDuration:
type: number
description: The duration to apply on too short Parts Within QuickLoop when forceQuickLoopAutoNext is set to `enabled_forcing_min_duration`
allowAdlibTestingSegment:
type: boolean
description: Whether to allow adlib testing mode, before a Part is playing in a Playlist
allowHold:
type: boolean
description: Whether to allow hold operations for Rundowns in this Studio
allowPieceDirectPlay:
type: boolean
description: Whether to allow direct playing of a piece in the rundown

required:
- frameRate
Expand Down
13 changes: 13 additions & 0 deletions packages/shared-lib/src/core/model/StudioSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,17 @@ export interface IStudioSettings {
* Default: 3000
*/
fallbackPartDuration?: number

/**
* Whether to allow hold operations for Rundowns in this Studio
* When disabled, any action-triggers that would normally trigger a hold operation will be silently ignored
* This should only block entering hold, to ensure Sofie doesn't get stuck if it somehow gets into hold
*/
allowHold: boolean

/**
* Whether to allow direct playing of a piece in the rundown
* This behaviour is usally triggered by double-clicking on a piece in the GUI
*/
allowPieceDirectPlay: boolean
}
2 changes: 2 additions & 0 deletions packages/webui/src/__mocks__/defaultCollectionObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export function defaultStudio(_id: StudioId): DBStudio {
frameRate: 25,
mediaPreviewsUrl: '',
minimumTakeSpan: DEFAULT_MINIMUM_TAKE_SPAN,
allowHold: true,
allowPieceDirectPlay: true,
}),
_rundownVersionHash: '',
routeSetsWithOverrides: wrapDefaultObject({}),
Expand Down
5 changes: 3 additions & 2 deletions packages/webui/src/client/ui/RundownView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1050,7 +1050,7 @@ const RundownHeader = withTranslation()(
{this.props.playlist.activationId ? (
<MenuItem onClick={(e) => this.take(e)}>{t('Take')}</MenuItem>
) : null}
{this.props.playlist.activationId ? (
{this.props.studio.settings.allowHold && this.props.playlist.activationId ? (
<MenuItem onClick={(e) => this.hold(e)}>{t('Hold')}</MenuItem>
) : null}
{this.props.playlist.activationId && canClearQuickLoop ? (
Expand Down Expand Up @@ -2252,7 +2252,8 @@ const RundownViewContent = translateWithTracker<IPropsWithReady, IState, ITracke
item &&
item.instance &&
this.props.playlist &&
this.props.playlist.currentPartInfo
this.props.playlist.currentPartInfo &&
this.props.studio?.settings.allowPieceDirectPlay
) {
const idToCopy = item.instance.isTemporary ? item.instance.piece._id : item.instance._id
const playlistId = this.props.playlist._id
Expand Down
20 changes: 20 additions & 0 deletions packages/webui/src/client/ui/Settings/Studio/Generic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,26 @@ function StudioSettings({ studio }: { studio: DBStudio }): JSX.Element {
/>
)}
</LabelAndOverridesForInt>

<LabelAndOverridesForCheckbox
label={t('Allow HOLD mode')}
item={wrappedItem}
itemKey={'allowHold'}
overrideHelper={overrideHelper}
hint={t('When disabled, any HOLD operations will be silently ignored')}
>
{(value, handleUpdate) => <CheckboxControl value={!!value} handleUpdate={handleUpdate} />}
</LabelAndOverridesForCheckbox>

<LabelAndOverridesForCheckbox
label={t('Allow direct playing pieces')}
item={wrappedItem}
itemKey={'allowPieceDirectPlay'}
overrideHelper={overrideHelper}
hint={t('When enabled, double clicking on certain pieces in the GUI will play them as adlibs')}
>
{(value, handleUpdate) => <CheckboxControl value={!!value} handleUpdate={handleUpdate} />}
</LabelAndOverridesForCheckbox>
</>
)
}
Loading