{"version":3,"file":"mediaClockOwnership.mjs","names":[],"sources":["../../../../src/hooks/playback/internal/mediaClockOwnership.ts"],"sourcesContent":["import type {\n  TimelineCommandResult,\n  ActiveLayerSelector,\n  PlaybackOptions,\n  TimelineEngine,\n  TimelineMediaError,\n  TimelineMediaSyncAdapter,\n} from '@techsquidtv/canvas-timeline-core';\nimport {\n  createCancelledMediaPlayResult,\n  createMediaPlayFailure,\n} from '#react/hooks/playback/internal/mediaPlayResult';\nimport type { TimelineMediaPlayResult } from '#react/hooks/playback/internal/mediaPlayResult';\nimport type { MediaSynchronizationQueue } from '#react/hooks/playback/internal/mediaSynchronizationQueue';\nimport { toMediaError, withMediaCauseMessage } from '#react/hooks/playback/mediaError';\nimport { quantizeTimelineTimeToFrame } from '#react/hooks/playback/playbackFrameTime';\nimport { compareRational, rationalEquals } from '@techsquidtv/canvas-timeline-utils';\nimport type { RationalTime, TimecodeFrameRate } from '@techsquidtv/canvas-timeline-utils';\nexport interface PendingMediaPlaybackStart<LayerName extends string, PlayResult> {\n  generation: number;\n  adapter: TimelineMediaSyncAdapter<LayerName>;\n  promise: Promise<PlayResult>;\n}\n\nexport interface MediaClockOwner<LayerName extends string> {\n  generation: number;\n  adapter: TimelineMediaSyncAdapter<LayerName>;\n  identity: object | undefined;\n}\n\nexport class MediaClockOwnership<LayerName extends string, PlayResult> {\n  private barrier: Promise<void> = Promise.resolve();\n  private generation = 0;\n  owner: MediaClockOwner<LayerName> | null = null;\n  pending: PendingMediaPlaybackStart<LayerName, PlayResult> | null = null;\n\n  updateAdapter(adapter: TimelineMediaSyncAdapter<LayerName>, sameIdentity: boolean) {\n    if (!sameIdentity) {\n      return;\n    }\n    if (this.owner !== null) {\n      this.owner.adapter = adapter;\n    }\n    if (this.pending !== null) {\n      this.pending.adapter = adapter;\n    }\n  }\n\n  isCurrentStart(generation: number) {\n    return this.pending?.generation === generation;\n  }\n\n  stopOwnedClock(generation?: number) {\n    const owner = this.owner;\n    if (owner === null || (generation !== undefined && owner.generation !== generation)) {\n      return;\n    }\n    this.owner = null;\n    owner.adapter.stopClock?.();\n  }\n\n  cancelPendingStart(invalidateOperations: () => void) {\n    const pendingStart = this.pending;\n    if (pendingStart === null) {\n      return false;\n    }\n    this.generation += 1;\n    invalidateOperations();\n    this.pending = null;\n    pendingStart.adapter.stopClock?.();\n    return true;\n  }\n\n  queueStart(\n    adapter: TimelineMediaSyncAdapter<LayerName>,\n    start: (generation: number) => Promise<PlayResult>\n  ) {\n    if (this.pending !== null) {\n      return this.pending.promise;\n    }\n    const generation = this.generation + 1;\n    this.generation = generation;\n    const promise = this.barrier.then(() => start(generation));\n    this.barrier = promise.then(\n      () => undefined,\n      () => undefined\n    );\n    this.pending = { generation, adapter, promise };\n    const clearPendingStart = () => {\n      if (this.pending?.promise === promise) {\n        this.pending = null;\n      }\n    };\n    void promise.then(clearPendingStart, clearPendingStart);\n    return promise;\n  }\n\n  clearOwnerWhenIdle(playing: boolean) {\n    if (!playing && this.pending === null) {\n      this.owner = null;\n    }\n  }\n\n  clearOwner() {\n    this.owner = null;\n  }\n}\n\ntype MediaClockStartupResult =\n  | { state: 'cancelled' }\n  | { state: 'competing-clock' }\n  | { state: 'started'; started: boolean };\n\ninterface StartMediaClockPlaybackOptions<LayerName extends string> {\n  engine: TimelineEngine;\n  generation: number;\n  frameRate: TimecodeFrameRate | undefined;\n  layers: Record<LayerName, ActiveLayerSelector>;\n  playbackOptions: Omit<PlaybackOptions, 'clock'> | undefined;\n  ownership: MediaClockOwnership<LayerName, TimelineMediaPlayResult>;\n  operationQueue: MediaSynchronizationQueue;\n  getAdapter: () => TimelineMediaSyncAdapter<LayerName>;\n  getAdapterIdentity: () => object | undefined;\n  isReady: () => boolean;\n  playTimeline: () => Promise<TimelineCommandResult>;\n  onError: ((error: TimelineMediaError) => void) | undefined;\n}\n\nfunction getEarliestTimelineTime(\n  times: readonly (RationalTime | undefined)[]\n): RationalTime | undefined {\n  return times.reduce<RationalTime | undefined>((earliest, time) => {\n    if (time === undefined || (earliest !== undefined && compareRational(earliest, time) <= 0)) {\n      return earliest;\n    }\n    return time;\n  }, undefined);\n}\n\nexport async function startMediaClockPlayback<LayerName extends string>({\n  engine,\n  generation,\n  frameRate,\n  layers,\n  playbackOptions,\n  ownership,\n  operationQueue,\n  getAdapter,\n  getAdapterIdentity,\n  isReady,\n  playTimeline,\n  onError,\n}: StartMediaClockPlaybackOptions<LayerName>): Promise<TimelineMediaPlayResult> {\n  const operationToken = operationQueue.capture(getAdapterIdentity());\n  const isCurrentOperation = () => operationQueue.isCurrent(operationToken, getAdapterIdentity());\n  let startupAdapter = getAdapter();\n  if (!ownership.isCurrentStart(generation)) {\n    return createCancelledMediaPlayResult();\n  }\n  if (!isReady()) {\n    return createMediaPlayFailure('not-ready', 'Media adapter is not ready.', onError);\n  }\n  if (engine.getState().playing) {\n    if (ownership.owner !== null) {\n      return { ok: true, time: engine.getTime() };\n    }\n    return createMediaPlayFailure(\n      'timeline-failed',\n      'Timeline playback is already controlled by another clock.',\n      onError\n    );\n  }\n\n  const currentTime = engine.getTime();\n  const resolvedStartTime = engine.getPlaybackStartTime(playbackOptions);\n  let timelineTime = quantizeTimelineTimeToFrame(resolvedStartTime, frameRate);\n  if (!rationalEquals(currentTime, timelineTime)) {\n    engine.setTime(timelineTime);\n  }\n  let timelineLayers = engine.media.getActiveLayers({ time: timelineTime, layers });\n\n  if (!timelineLayers.hasActiveClips) {\n    const state = engine.getState();\n    const playbackStartTime = (playbackOptions?.respectInOut ?? true) ? state.inPoint : undefined;\n    const playbackEndTime = getEarliestTimelineTime([\n      playbackOptions?.toTime,\n      state.duration,\n      (playbackOptions?.respectInOut ?? true) ? state.outPoint : undefined,\n    ]);\n    const playbackStartsInContent =\n      playbackStartTime !== undefined &&\n      (playbackEndTime === undefined || compareRational(playbackStartTime, playbackEndTime) < 0) &&\n      engine.media.getActiveLayers({ time: playbackStartTime, layers }).hasActiveClips;\n    const firstContentTime = playbackStartsInContent\n      ? playbackStartTime\n      : engine.media.getFirstContentTime({\n          layers,\n          atOrAfter: playbackStartTime,\n          before: playbackEndTime,\n        });\n    if (firstContentTime === undefined) {\n      if (engine.media.getFirstContentTime({ layers }) === undefined) {\n        return createMediaPlayFailure('no-content', 'No timeline content is available.', onError);\n      }\n    } else {\n      timelineTime = quantizeTimelineTimeToFrame(firstContentTime, frameRate, 'ceil');\n      engine.setTime(timelineTime);\n      timelineLayers = engine.media.getActiveLayers({ time: timelineTime, layers });\n    }\n  }\n\n  if (!timelineLayers.hasActiveClips) {\n    return createMediaPlayFailure(\n      'no-active-content',\n      'No active timeline content is available.',\n      onError\n    );\n  }\n\n  try {\n    startupAdapter = getAdapter();\n    startupAdapter.requestClockActivation?.(engine.getPlaybackRate());\n    const clockStartup = await operationQueue.enqueue(\n      async (): Promise<MediaClockStartupResult> => {\n        if (!isCurrentOperation()) {\n          return { state: 'cancelled' };\n        }\n        startupAdapter = getAdapter();\n        await startupAdapter.seek?.(timelineTime, timelineLayers);\n        if (!isCurrentOperation() || !ownership.isCurrentStart(generation) || !isReady()) {\n          return { state: 'cancelled' };\n        }\n        if (engine.getState().playing) {\n          startupAdapter = getAdapter();\n          startupAdapter.stopClock?.();\n          return { state: 'competing-clock' };\n        }\n\n        startupAdapter = getAdapter();\n        ownership.owner = {\n          generation,\n          adapter: startupAdapter,\n          identity: getAdapterIdentity(),\n        };\n        return {\n          state: 'started',\n          started: await startupAdapter.startClock(timelineTime, engine.getPlaybackRate()),\n        };\n      }\n    );\n    if (\n      clockStartup.state === 'cancelled' ||\n      !isCurrentOperation() ||\n      !ownership.isCurrentStart(generation) ||\n      !isReady()\n    ) {\n      ownership.stopOwnedClock(generation);\n      return createCancelledMediaPlayResult();\n    }\n    if (clockStartup.state === 'competing-clock') {\n      return createMediaPlayFailure(\n        'timeline-failed',\n        'Timeline playback is already controlled by another clock.',\n        onError\n      );\n    }\n    if (!clockStartup.started) {\n      ownership.stopOwnedClock(generation);\n      return createMediaPlayFailure('clock-failed', 'Media clock could not start.', onError);\n    }\n  } catch (clockError) {\n    if (!isCurrentOperation() || !ownership.isCurrentStart(generation) || !isReady()) {\n      ownership.stopOwnedClock(generation);\n      return createCancelledMediaPlayResult();\n    }\n    if (ownership.owner?.generation === generation) {\n      ownership.stopOwnedClock(generation);\n    } else {\n      startupAdapter.stopClock?.();\n    }\n    return createMediaPlayFailure(\n      'clock-failed',\n      'Media clock could not start.',\n      onError,\n      toMediaError(clockError)\n    );\n  }\n\n  let timelineStarted = false;\n  try {\n    if (engine.getState().playing) {\n      ownership.stopOwnedClock(generation);\n      return createMediaPlayFailure(\n        'timeline-failed',\n        'Timeline playback is already controlled by another clock.',\n        onError\n      );\n    }\n    const timelineResult = await playTimeline();\n    if (!timelineResult.ok && timelineResult.reason === 'sync-failed') {\n      ownership.stopOwnedClock(generation);\n      return {\n        ok: false,\n        reason: 'sync-failed',\n        message: withMediaCauseMessage('Media synchronization failed.', timelineResult.cause),\n        ...(timelineResult.cause !== undefined ? { cause: timelineResult.cause } : {}),\n      };\n    }\n    if (!isCurrentOperation() || !ownership.isCurrentStart(generation) || !isReady()) {\n      ownership.stopOwnedClock(generation);\n      return createCancelledMediaPlayResult();\n    }\n    timelineStarted = timelineResult.ok;\n  } catch (timelineError) {\n    if (!isCurrentOperation() || !ownership.isCurrentStart(generation) || !isReady()) {\n      ownership.stopOwnedClock(generation);\n      return createCancelledMediaPlayResult();\n    }\n    ownership.stopOwnedClock(generation);\n    return createMediaPlayFailure(\n      'timeline-failed',\n      'Timeline playback could not start.',\n      onError,\n      toMediaError(timelineError)\n    );\n  }\n\n  if (!timelineStarted) {\n    ownership.stopOwnedClock(generation);\n    return createMediaPlayFailure('timeline-failed', 'Timeline playback could not start.', onError);\n  }\n\n  return { ok: true, time: timelineTime };\n}\n"],"mappings":"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"}