Skip to content

Commit

Permalink
feat: A/B player Ids as strings
Browse files Browse the repository at this point in the history
  • Loading branch information
sbaudlr committed Oct 18, 2023
1 parent df7ed0c commit a039efe
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 30 deletions.
6 changes: 3 additions & 3 deletions packages/blueprints-integration/src/abPlayback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const AB_MEDIA_PLAYER_AUTO = '__auto__'
* Description of a player in an AB pool
*/
export interface ABPlayerDefinition {
playerId: number
playerId: number | string
}

/**
Expand All @@ -30,7 +30,7 @@ export interface ABTimelineLayerChangeRule {
/** What AB pools can this rule be used for */
acceptedPoolNames: string[]
/** A function to generate the new layer name for a chosen playerId */
newLayerName: (playerId: number) => string
newLayerName: (playerId: number | string) => string
/** Whether this rule can be used for lookaheadObjects */
allowsLookahead: boolean
}
Expand Down Expand Up @@ -60,7 +60,7 @@ export interface ABResolverConfiguration {
customApplyToObject?: (
context: ICommonContext,
poolName: string,
playerId: number,
playerId: number | string,
timelineObject: OnGenerateTimelineObj<TSR.TSRTimelineContent>
) => boolean
}
Expand Down
2 changes: 1 addition & 1 deletion packages/corelib/src/dataModel/RundownPlaylist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface ABSessionInfo {

export interface ABSessionAssignment {
sessionId: string
playerId: number
playerId: number | string
lookahead: boolean // purely informational for debugging
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function resolveAbSessions(
timelineObjs: OnGenerateTimelineObjExt[],
previousAssignmentMap: ABSessionAssignments,
sessionPool: string,
playerIds: number[],
playerIds: Array<number | string>,
now: number
): AssignmentResult {
const sessionRequests = calculateSessionTimeRanges(
Expand Down Expand Up @@ -148,6 +148,78 @@ describe('resolveMediaPlayers', () => {
expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2].instance, 'clip_ghi')
})

test('basic pieces - players with string Ids', () => {
const previousAssignments = {}
const pieces = [
createBasicResolvedPieceInstance('0', 400, 5000, 'abc'),
createBasicResolvedPieceInstance('1', 400, 5000, 'def'),
createBasicResolvedPieceInstance('2', 800, 4000, 'ghi'),
]

mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`)

const assignments = resolveAbSessions(
abSessionHelper,
resolverOptions,
pieces,
[],
previousAssignments,
POOL_NAME,
['player1', 'player2'],
4500
)
expect(assignments.failedRequired).toEqual(['inst_2_clip_ghi'])
expect(assignments.failedOptional).toHaveLength(0)
expect(assignments.requests).toHaveLength(3)
expect(assignments.requests).toEqual([
{ end: 5400, id: 'inst_0_clip_abc', playerId: 'player1', start: 400, optional: false },
{ end: 5400, id: 'inst_1_clip_def', playerId: 'player2', start: 400, optional: false },
{ end: 4800, id: 'inst_2_clip_ghi', playerId: undefined, start: 800, optional: false }, // Massive overlap
])

expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3)
expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0)
expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0], 'clip_abc')
expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1], 'clip_def')
expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2], 'clip_ghi')
})

test('basic pieces - players with number and string Ids', () => {
const previousAssignments = {}
const pieces = [
createBasicResolvedPieceInstance('0', 400, 5000, 'abc'),
createBasicResolvedPieceInstance('1', 400, 5000, 'def'),
createBasicResolvedPieceInstance('2', 800, 4000, 'ghi'),
]

mockGetPieceSessionId.mockImplementation((piece, name) => `${piece._id}_${name}`)

const assignments = resolveAbSessions(
abSessionHelper,
resolverOptions,
pieces,
[],
previousAssignments,
POOL_NAME,
[1, 'player2'],
4500
)
expect(assignments.failedRequired).toEqual(['inst_2_clip_ghi'])
expect(assignments.failedOptional).toHaveLength(0)
expect(assignments.requests).toHaveLength(3)
expect(assignments.requests).toEqual([
{ end: 5400, id: 'inst_0_clip_abc', playerId: 1, start: 400, optional: false },
{ end: 5400, id: 'inst_1_clip_def', playerId: 'player2', start: 400, optional: false },
{ end: 4800, id: 'inst_2_clip_ghi', playerId: undefined, start: 800, optional: false }, // Massive overlap
])

expect(mockGetPieceSessionId).toHaveBeenCalledTimes(3)
expect(mockGetObjectSessionId).toHaveBeenCalledTimes(0)
expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(1, pieces[0], 'clip_abc')
expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(2, pieces[1], 'clip_def')
expect(mockGetPieceSessionId).toHaveBeenNthCalledWith(3, pieces[2], 'clip_ghi')
})

test('Multiple pieces same id', () => {
const previousAssignments = {}
const pieces = [
Expand Down
25 changes: 17 additions & 8 deletions packages/job-worker/src/playout/abPlayback/abPlaybackResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface SessionRequest {
readonly end: number | undefined
readonly optional?: boolean
readonly lookaheadRank?: number
playerId?: number
playerId?: number | string
}

export interface AssignmentResult {
Expand All @@ -21,7 +21,7 @@ export interface AssignmentResult {
}

interface SlotAvailability {
id: number
id: number | string
before: (SessionRequest & { end: number }) | null
after: SessionRequest | null
clashes: SessionRequest[]
Expand Down Expand Up @@ -55,7 +55,7 @@ function safeMin<T>(arr: T[], func: (val: T) => number): T | undefined {
*/
export function resolveAbAssignmentsFromRequests(
resolverOptions: ABResolverOptions,
playerIds: number[],
playerIds: Array<number | string>,
rawRequests: SessionRequest[],
now: number // Current time
): AssignmentResult {
Expand All @@ -80,7 +80,7 @@ export function resolveAbAssignmentsFromRequests(
return res
}

const originalLookaheadAssignments: Record<string, number> = {}
const originalLookaheadAssignments: Record<string, number | string> = {}
for (const req of rawRequests) {
if (req.lookaheadRank !== undefined && req.playerId !== undefined) {
originalLookaheadAssignments[req.id] = req.playerId
Expand All @@ -104,7 +104,7 @@ export function resolveAbAssignmentsFromRequests(
pendingRequests = grouped[undefined as any]

// build map of slots and what they already have assigned
const slots: Map<number, SessionRequest[]> = new Map()
const slots: Map<number | string, SessionRequest[]> = new Map()
_.each(playerIds, (id) => slots.set(id, grouped[id] || []))

const beforeHasGap = (p: SlotAvailability, req: SessionRequest): boolean =>
Expand Down Expand Up @@ -313,7 +313,7 @@ export function resolveAbAssignmentsFromRequests(

// Ensure lookahead gets assigned based on priority not some randomness
// Includes slots which have either no sessions, or the last has a known end time
const lastSessionPerSlot: Record<number, number | undefined> = {} // playerId, end
const lastSessionPerSlot: Record<number | string, number | undefined> = {} // playerId, end
for (const [playerId, sessions] of slots) {
const last = _.last(sessions.filter((s) => s.lookaheadRank === undefined))
if (!last) {
Expand Down Expand Up @@ -359,7 +359,12 @@ export function resolveAbAssignmentsFromRequests(
const req = remainingLookaheads[i]

if (slot) {
req.playerId = Number(slot[0])
// Check if we were originally given a player index rather than a string Id
if (playerIds.find((id) => typeof id === 'number' && id === Number(slot[0]))) {
req.playerId = Number(slot[0])
} else {
req.playerId = slot[0]
}
} else {
delete req.playerId
}
Expand All @@ -368,7 +373,11 @@ export function resolveAbAssignmentsFromRequests(
return res
}

function getAvailability(id: number, thisReq: SessionRequest, orderedRequests: SessionRequest[]): SlotAvailability {
function getAvailability(
id: number | string,
thisReq: SessionRequest,
orderedRequests: SessionRequest[]
): SlotAvailability {
const res: SlotAvailability = {
id,
before: null,
Expand Down
26 changes: 10 additions & 16 deletions packages/job-worker/src/playout/abPlayback/applyAssignments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function applyAbPlayerObjectAssignments(
poolName: string
): ABSessionAssignments {
const newAssignments: ABSessionAssignments = {}
const persistAssignment = (sessionId: string, playerId: number, lookahead: boolean): void => {
const persistAssignment = (sessionId: string, playerId: number | string, lookahead: boolean): void => {
// Track the assignment, so that the next onTimelineGenerate can try to reuse the same session
if (newAssignments[sessionId]) {
// TODO - warn?
Expand Down Expand Up @@ -117,24 +117,18 @@ function updateObjectsToAbPlayer(
context: ICommonContext,
abConfiguration: Pick<ABResolverConfiguration, 'timelineObjectLayerChangeRules' | 'customApplyToObject'>,
poolName: string,
poolIndex: number,
poolId: number | string,
objs: OnGenerateTimelineObj<TSR.TSRTimelineContent>[]
): OnGenerateTimelineObj<TSR.TSRTimelineContent>[] {
const failedObjects: OnGenerateTimelineObj<TSR.TSRTimelineContent>[] = []

for (const obj of objs) {
const updatedKeyframes = applyUpdateToKeyframes(poolName, poolIndex, obj)
const updatedKeyframes = applyUpdateToKeyframes(poolName, poolId, obj)

const updatedLayer = applylayerMoveRule(
abConfiguration.timelineObjectLayerChangeRules,
poolName,
poolIndex,
obj
)
const updatedLayer = applylayerMoveRule(abConfiguration.timelineObjectLayerChangeRules, poolName, poolId, obj)

const updatedCustom =
abConfiguration.customApplyToObject &&
abConfiguration.customApplyToObject(context, poolName, poolIndex, obj)
abConfiguration.customApplyToObject && abConfiguration.customApplyToObject(context, poolName, poolId, obj)

if (!updatedKeyframes && !updatedLayer && !updatedCustom) {
failedObjects.push(obj)
Expand All @@ -146,7 +140,7 @@ function updateObjectsToAbPlayer(

function applyUpdateToKeyframes(
poolName: string,
poolIndex: number,
poolId: number | string,
obj: OnGenerateTimelineObj<TSR.TSRTimelineContent>
): boolean {
if (!obj.keyframes) return false
Expand All @@ -160,7 +154,7 @@ function applyUpdateToKeyframes(
// Preserve from other ab pools
if (kf.abSession.poolName !== poolName) return kf

if (kf.abSession.playerIndex === poolIndex) {
if (kf.abSession.playerId === poolId) {
// Make sure any ab keyframe is active
kf.disabled = false
updated = true
Expand All @@ -178,7 +172,7 @@ function applyUpdateToKeyframes(
function applylayerMoveRule(
timelineObjectLayerChangeRules: ABTimelineLayerChangeRules | undefined,
poolName: string,
poolIndex: number,
poolId: number | string,
obj: OnGenerateTimelineObj<TSR.TSRTimelineContent>
): boolean {
const ruleId = obj.isLookahead ? obj.lookaheadForLayer || obj.layer : obj.layer
Expand All @@ -190,13 +184,13 @@ function applylayerMoveRule(
if (obj.isLookahead && moveRule.allowsLookahead && obj.lookaheadForLayer) {
// This works on the assumption that layer will contain lookaheadForLayer, but not the exact syntax.
// Hopefully this will be durable to any potential future core changes
const newLayer = moveRule.newLayerName(poolIndex)
const newLayer = moveRule.newLayerName(poolId)
obj.layer = (obj.layer + '').replace(obj.lookaheadForLayer + '', newLayer)
obj.lookaheadForLayer = newLayer

return true
} else if (!obj.isLookahead || (obj.isLookahead && !obj.lookaheadForLayer)) {
obj.layer = moveRule.newLayerName(poolIndex)
obj.layer = moveRule.newLayerName(poolId)
return true
}

Expand Down
2 changes: 1 addition & 1 deletion packages/shared-lib/src/core/model/Timeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export interface TimelineKeyframeCoreExt<TContent extends { deviceType: TSR.Devi

abSession?: {
poolName: string
playerIndex: number
playerId: number | string
}
}

Expand Down

0 comments on commit a039efe

Please sign in to comment.