@omnimedia/omnitool
Version:
open source video processing tools
442 lines (359 loc) • 10.4 kB
text/typescript
import {ms, Ms} from '../../../units/ms.js'
import {Id, TimelineFile} from '../../parts/basics.js'
import {Keyframes, TransformAnimation} from '../../types.js'
import {SampleContext} from './samplers/visual/parts/types.js'
import {I6, Mat6, mul6, transformToMat6} from '../../utils/matrix.js'
import {ContainerItem, Item, Kind, PlayableItem} from '../../parts/item.js'
import {resolveScalarAnimation, resolveTransformAnimation} from '../../utils/anim.js'
function isPlayableItem(item: Item.Any): item is PlayableItem {
return 'duration' in item
}
type WalkAtCallbacks = {
sequence: (x: Item.Sequence, localTime: Ms, ancestors: AncestorAt[]) => void
stack: (x: Item.Stack, localTime: Ms, ancestors: AncestorAt[]) => void
video: (x: Item.Video, localTime: Ms, ancestors: AncestorAt[]) => void
image: (x: Item.Image, localTime: Ms, ancestors: AncestorAt[]) => void
text: (x: Item.Text, localTime: Ms, ancestors: AncestorAt[]) => void
caption: (x: Item.Caption, localTime: Ms, ancestors: AncestorAt[]) => void
audio: (x: Item.Audio, localTime: Ms, ancestors: AncestorAt[]) => void
}
type WalkCallbacks = {
sequence?: (x: Item.Sequence, matrix: Mat6, ancestors: AncestorAt[]) => void
stack?: (x: Item.Stack, matrix: Mat6, ancestors: AncestorAt[]) => void
video?: (x: Item.Video, matrix: Mat6, ancestors: AncestorAt[]) => void
image?: (x: Item.Image, matrix: Mat6, ancestors: AncestorAt[]) => void
text?: (x: Item.Text, matrix: Mat6, ancestors: AncestorAt[]) => void
caption?: (x: Item.Caption, matrix: Mat6, ancestors: AncestorAt[]) => void
audio?: (x: Item.Audio) => void
}
interface Props {
timeline: TimelineFile
timecode: Ms
}
export interface AncestorAt {
item: ContainerItem
localTime: Ms
}
interface At {
item: Item.Any
localTime: Ms
ancestors: AncestorAt[]
}
export function itemsAt(p: Props): At[] {
const results: At[] = []
const itemMap = new Map(p.timeline.items.map(item => [item.id, item]))
walkAt(p.timeline.rootId, itemMap, p.timecode, {
sequence: () => { },
stack: () => { },
video: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
image: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
text: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
caption: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
audio: (item, localTime, ancestors) => results.push({ item, localTime, ancestors })
})
return results
}
interface FromProps {
timeline: TimelineFile
from: Ms
}
export function itemsFrom(p: FromProps): At[] {
const results: At[] = []
const itemMap = new Map(p.timeline.items.map(item => [item.id, item]))
walkFrom(p.timeline.rootId, itemMap, p.from, {
sequence: () => { },
stack: () => { },
video: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
image: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
text: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
caption: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
audio: (item, localTime, ancestors) => results.push({ item, localTime, ancestors })
})
return results
}
export function computeWorldMatrix(
items: Map<Id, Item.Any>,
ancestors: AncestorAt[],
item: Item.Any,
localTime: Ms,
): Mat6 {
let world = I6
for (const ancestor of ancestors) {
world = applySpatialIfAny(items, ancestor.item, world, ancestor.localTime)
}
return applySpatialIfAny(items, item, world, localTime)
}
function applySpatialIfAny(
items: Map<Id, Item.Any>,
item: Item.Any,
parentMatrix: Mat6,
time: Ms,
) {
let matrix = parentMatrix
if ("spatialId" in item && item.spatialId) {
const spatial = items.get(item.spatialId) as Item.Spatial | undefined
if (spatial && spatial.enabled !== false) {
const local = transformToMat6(spatial.transform)
matrix = mul6(local, matrix)
}
}
if ("animationIds" in item && item.animationIds) {
for (const id of item.animationIds) {
const animation = items.get(id) as Item.Animation | undefined
const anim = animation?.anims.transform
if (animation?.enabled !== false && anim && transformActiveAt(anim, time)) {
const local = transformToMat6(resolveTransformAnimation(time, anim))
matrix = mul6(local, matrix)
}
}
}
return matrix
}
export function walk(
id: Id,
items: Map<Id, Item.Any>,
parentMatrix: Mat6,
localTime: Ms,
callbacks: WalkCallbacks,
ancestors: AncestorAt[] = []
) {
const item = items.get(id)
if (!item) return
const currentMatrix = applySpatialIfAny(items, item, parentMatrix, localTime)
switch (item.kind) {
case Kind.Stack:
callbacks.stack?.(item, currentMatrix, ancestors)
for (const childId of item.childrenIds) {
walk(childId, items, currentMatrix, localTime, callbacks, [...ancestors, {item, localTime}])
}
break
case Kind.Sequence: {
callbacks.sequence?.(item, currentMatrix, ancestors)
for (const childId of item.childrenIds) {
const child = items.get(childId)
if (!child)
continue
if (!isPlayableItem(child)) {
continue
}
walk(
childId,
items,
currentMatrix,
localTime,
callbacks,
[...ancestors, {item, localTime}]
)
}
break
}
case Kind.Video:
callbacks.video?.(item, currentMatrix, ancestors)
break
case Kind.Image:
callbacks.image?.(item, currentMatrix, ancestors)
break
case Kind.Text:
callbacks.text?.(item, currentMatrix, ancestors)
break
case Kind.Caption:
callbacks.caption?.(item, currentMatrix, ancestors)
break
case Kind.Audio:
callbacks.audio?.(item)
break
}
}
function walkAt(
id: Id,
items: Map<Id, Item.Any>,
time: Ms,
callbacks: WalkAtCallbacks,
ancestors: AncestorAt[] = []
) {
const item = items.get(id)
if (!item) return
switch (item.kind) {
case Kind.Stack:
callbacks.stack(item, time, ancestors)
for (const childId of item.childrenIds) {
walkAt(childId, items, time, callbacks, [...ancestors, {item, localTime: time}])
}
break
case Kind.Sequence: {
callbacks.sequence(item, time, ancestors)
let offset = ms(0)
for (const childId of item.childrenIds) {
const child = items.get(childId)
if (!child)
continue
const duration = computeItemDurationFromMap(child.id, items)
if (duration <= 0)
continue
if (time >= offset && time < offset + duration) {
const localTime = ms(time - offset)
walkAt(
childId,
items,
localTime,
callbacks,
[...ancestors, {item, localTime: time}]
)
break
}
offset = ms(offset + duration)
}
break
}
case Kind.Video:
callbacks.video(item, time, ancestors)
break
case Kind.Image:
callbacks.image(item, time, ancestors)
break
case Kind.Text:
callbacks.text(item, time, ancestors)
break
case Kind.Caption:
callbacks.caption(item, time, ancestors)
break
case Kind.Audio:
callbacks.audio(item, time, ancestors)
break
}
}
function walkFrom(
id: Id,
items: Map<Id, Item.Any>,
from: Ms,
callbacks: WalkAtCallbacks,
ancestors: AncestorAt[] = []
) {
const item = items.get(id)
if (!item) return
switch (item.kind) {
case Kind.Stack:
callbacks.stack(item, from, ancestors)
for (const childId of item.childrenIds) {
walkFrom(childId, items, from, callbacks, [...ancestors, {item, localTime: from}])
}
break
case Kind.Sequence: {
callbacks.sequence(item, from, ancestors)
let offset = ms(0)
for (const childId of item.childrenIds) {
const child = items.get(childId)
if (!child)
continue
const duration = computeItemDurationFromMap(child.id, items)
if (duration <= 0)
continue
const end = ms(offset + duration)
if (from >= end) {
offset = end
continue
}
const localTime = ms(Math.max(0, from - offset))
walkFrom(
childId,
items,
localTime,
callbacks,
[...ancestors, {item, localTime: from}]
)
offset = end
}
break
}
case Kind.Video:
callbacks.video(item, from, ancestors)
break
case Kind.Image:
callbacks.image(item, from, ancestors)
break
case Kind.Text:
callbacks.text(item, from, ancestors)
break
case Kind.Caption:
callbacks.caption(item, from, ancestors)
break
case Kind.Audio:
callbacks.audio(item, from, ancestors)
break
}
}
export function computeItemDuration(
id: number,
timeline: TimelineFile
): Ms {
return computeItemDurationFromMap(
id,
new Map(timeline.items.map(item => [item.id, item]))
)
}
function computeItemDurationFromMap(
id: number,
items: Map<Id, Item.Any>
): Ms {
const item = items.get(id)
if (!item) return ms(0)
switch (item.kind) {
case Kind.Sequence: {
let duration = ms(0)
for (const childId of item.childrenIds)
duration = ms(duration + computeItemDurationFromMap(childId, items))
return duration
}
case Kind.Stack: {
let longest = ms(0)
for (const childId of item.childrenIds) {
const duration = computeItemDurationFromMap(childId, items)
if (duration > longest) {
longest = duration
}
}
return longest
}
default: {
if (!isPlayableItem(item))
return ms(0)
return item.duration
}
}
}
export function computeOpacity(
ctx: SampleContext,
item: Item.Any,
time: Ms,
) {
if (!("animationIds" in item) || item.animationIds === undefined)
return 1
let opacity = 1
for (const id of item.animationIds) {
const animation = ctx.items.get(id) as Item.Animation | undefined
const anim = animation?.anims.opacity
if (animation?.enabled !== false && anim && keyframesActiveAt(anim.track, time))
opacity = resolveScalarAnimation(time, anim)
}
return opacity
}
function keyframesActiveAt(keys: Keyframes, time: Ms) {
if (keys.length === 0)
return false
let start = keys[0][0]
let end = keys[0][0]
for (const [keyTime] of keys) {
start = Math.min(start, keyTime)
end = Math.max(end, keyTime)
}
return time >= start && time <= end
}
function transformActiveAt(anim: TransformAnimation, time: Ms) {
return (
keyframesActiveAt(anim.track.position.x, time) ||
keyframesActiveAt(anim.track.position.y, time) ||
keyframesActiveAt(anim.track.scale.x, time) ||
keyframesActiveAt(anim.track.scale.y, time) ||
keyframesActiveAt(anim.track.rotation, time)
)
}