@omnimedia/omnitool 1.1.0-105 → 1.1.0-106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +5 -3
  2. package/package.json +1 -1
  3. package/s/timeline/parts/captions.ts +1 -0
  4. package/s/timeline/parts/item.ts +15 -13
  5. package/s/timeline/parts/transitions.ts +2 -2
  6. package/s/timeline/renderers/parts/handy.ts +3 -3
  7. package/s/timeline/renderers/parts/samplers/audio/parts/init.ts +2 -0
  8. package/s/timeline/renderers/parts/samplers/visual/parts/sample.ts +10 -5
  9. package/s/timeline/renderers/parts/samplers/visual/parts/sequence.ts +3 -0
  10. package/s/timeline/renderers/renderers.test.ts +15 -0
  11. package/s/timeline/sugar/helpers.ts +8 -4
  12. package/s/timeline/sugar/o.ts +15 -7
  13. package/x/demo/demo.bundle.min.js +1 -1
  14. package/x/demo/demo.bundle.min.js.map +3 -3
  15. package/x/index.html +2 -2
  16. package/x/tests.bundle.min.js +48 -48
  17. package/x/tests.bundle.min.js.map +3 -3
  18. package/x/tests.html +1 -1
  19. package/x/timeline/parts/captions.d.ts +1 -0
  20. package/x/timeline/parts/captions.js.map +1 -1
  21. package/x/timeline/parts/item.d.ts +15 -13
  22. package/x/timeline/parts/transitions.d.ts +2 -2
  23. package/x/timeline/renderers/parts/handy.js +3 -3
  24. package/x/timeline/renderers/parts/handy.js.map +1 -1
  25. package/x/timeline/renderers/parts/samplers/audio/parts/init.js +2 -0
  26. package/x/timeline/renderers/parts/samplers/audio/parts/init.js.map +1 -1
  27. package/x/timeline/renderers/parts/samplers/visual/parts/sample.js +9 -5
  28. package/x/timeline/renderers/parts/samplers/visual/parts/sample.js.map +1 -1
  29. package/x/timeline/renderers/parts/samplers/visual/parts/sequence.js +2 -0
  30. package/x/timeline/renderers/parts/samplers/visual/parts/sequence.js.map +1 -1
  31. package/x/timeline/renderers/renderers.test.d.ts +1 -0
  32. package/x/timeline/renderers/renderers.test.js +9 -0
  33. package/x/timeline/renderers/renderers.test.js.map +1 -1
  34. package/x/timeline/sugar/helpers.d.ts +7 -3
  35. package/x/timeline/sugar/helpers.js.map +1 -1
  36. package/x/timeline/sugar/o.d.ts +6 -2
  37. package/x/timeline/sugar/o.js +8 -4
  38. package/x/timeline/sugar/o.js.map +1 -1
package/README.md CHANGED
@@ -56,17 +56,19 @@ const timeline = omni.timeline(o => {
56
56
  })
57
57
  ```
58
58
 
59
- You can label your items:
59
+ Items can have `label` and `enabled` fields:
60
60
 
61
61
  ```ts
62
62
  const visual = o.sequence(
63
63
  "Main sequence",
64
64
  o.video(clip, {label: "Intro"}),
65
- o.transition.fade(500, {label: "Fade"}),
66
- o.text("Hello world", {label: "Title"})
65
+ o.transition.fade(500, {label: "Fade", enabled: false}),
66
+ o.text("Hello world", {label: "Title", enabled: false})
67
67
  )
68
68
  ```
69
69
 
70
+ Disabled items do not render or play audio, but still keep their timeline duration and occupy space.
71
+
70
72
  Declarative helper style (no explicit `o` in timeline declarations):
71
73
 
72
74
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnimedia/omnitool",
3
- "version": "1.1.0-105",
3
+ "version": "1.1.0-106",
4
4
  "description": "open source video processing tools",
5
5
  "license": "MIT",
6
6
  "author": "Przemysław Gałęzki",
@@ -6,6 +6,7 @@ import {Transcription, TranscriptSegment} from "../../features/speech/transcribe
6
6
 
7
7
  export type CaptionOptions = {
8
8
  label?: string
9
+ enabled?: boolean
9
10
  itemId?: Item.Caption["itemId"]
10
11
  start?: number
11
12
  duration?: number
@@ -26,8 +26,9 @@ export enum Kind {
26
26
  Image
27
27
  }
28
28
 
29
- export type ItemMeta = {
29
+ export type ItemBase = {
30
30
  label?: string
31
+ enabled?: boolean
31
32
  }
32
33
 
33
34
  export namespace Item {
@@ -35,6 +36,7 @@ export namespace Item {
35
36
  id: Id
36
37
  kind: Kind.TextStyle
37
38
  style: TextStyleOptions
39
+ enabled?: boolean
38
40
  }
39
41
 
40
42
  export type Spatial = {
@@ -42,14 +44,14 @@ export namespace Item {
42
44
  kind: Kind.Spatial
43
45
  transform: Transform
44
46
  crop?: Crop
45
- enabled: boolean
47
+ enabled?: boolean
46
48
  }
47
49
 
48
50
  export type Animation = {
49
51
  id: Id
50
52
  kind: Kind.Animation
51
53
  anims: VisualAnimations
52
- enabled: boolean
54
+ enabled?: boolean
53
55
  }
54
56
 
55
57
  export type Filter<T extends FilterType = FilterType> = {
@@ -57,14 +59,14 @@ export namespace Item {
57
59
  kind: Kind.Filter
58
60
  type: T
59
61
  params?: FilterParams<T>
60
- enabled: boolean
62
+ enabled?: boolean
61
63
  }
62
64
 
63
65
  export type Gap = {
64
66
  id: Id
65
67
  kind: Kind.Gap
66
68
  duration: number
67
- } & ItemMeta
69
+ } & ItemBase
68
70
 
69
71
  export type Sequence = {
70
72
  id: Id
@@ -72,7 +74,7 @@ export namespace Item {
72
74
  childrenIds: Id[]
73
75
  spatialId?: Id
74
76
  filterIds?: Id[]
75
- } & ItemMeta
77
+ } & ItemBase
76
78
 
77
79
  export type Stack = {
78
80
  id: Id
@@ -80,7 +82,7 @@ export namespace Item {
80
82
  childrenIds: Id[]
81
83
  spatialId?: Id
82
84
  filterIds?: Id[]
83
- } & ItemMeta
85
+ } & ItemBase
84
86
 
85
87
  export type Video = {
86
88
  id: Id
@@ -91,7 +93,7 @@ export namespace Item {
91
93
  spatialId?: Id
92
94
  animationIds?: Id[]
93
95
  filterIds?: Id[]
94
- } & ItemMeta
96
+ } & ItemBase
95
97
 
96
98
  export type Image = {
97
99
  id: Id
@@ -101,7 +103,7 @@ export namespace Item {
101
103
  spatialId?: Id
102
104
  animationIds?: Id[]
103
105
  filterIds?: Id[]
104
- } & ItemMeta
106
+ } & ItemBase
105
107
 
106
108
  export type Audio = {
107
109
  id: Id
@@ -110,7 +112,7 @@ export namespace Item {
110
112
  start: number
111
113
  duration: number
112
114
  gain?: number
113
- } & ItemMeta
115
+ } & ItemBase
114
116
 
115
117
  export type Text = {
116
118
  id: Id
@@ -121,7 +123,7 @@ export namespace Item {
121
123
  animationIds?: Id[]
122
124
  styleId?: Id
123
125
  filterIds?: Id[]
124
- } & ItemMeta
126
+ } & ItemBase
125
127
 
126
128
 
127
129
  export type Caption = {
@@ -138,14 +140,14 @@ export namespace Item {
138
140
  animationIds?: Id[]
139
141
  styleId?: Id
140
142
  filterIds?: Id[]
141
- } & ItemMeta
143
+ } & ItemBase
142
144
 
143
145
  export type Transition = {
144
146
  id: Id
145
147
  kind: Kind.Transition
146
148
  name: TransitionName
147
149
  duration: number
148
- } & ItemMeta
150
+ } & ItemBase
149
151
 
150
152
  export type Any = (
151
153
  | Sequence
@@ -1,4 +1,4 @@
1
- import type {Item, ItemMeta} from "./item.js"
1
+ import type {Item, ItemBase} from "./item.js"
2
2
 
3
3
  export const transitionNames = [
4
4
  "Bounce",
@@ -78,7 +78,7 @@ export type Transition = {
78
78
  }
79
79
 
80
80
  export interface TransitionAction {
81
- (duration: number, options?: ItemMeta): Item.Transition
81
+ (duration: number, options?: ItemBase): Item.Transition
82
82
  }
83
83
 
84
84
  export type TransitionActions = {
@@ -110,7 +110,7 @@ function applySpatialIfAny(
110
110
  let matrix = parentMatrix
111
111
  if ("spatialId" in item && item.spatialId) {
112
112
  const spatial = items.get(item.spatialId) as Item.Spatial | undefined
113
- if (spatial?.enabled) {
113
+ if (spatial && spatial.enabled !== false) {
114
114
  const local = transformToMat6(spatial.transform)
115
115
  matrix = mul6(local, matrix)
116
116
  }
@@ -120,7 +120,7 @@ function applySpatialIfAny(
120
120
  for (const id of item.animationIds) {
121
121
  const animation = items.get(id) as Item.Animation | undefined
122
122
  const anim = animation?.anims.transform
123
- if (animation?.enabled && anim && transformActiveAt(anim, time)) {
123
+ if (animation?.enabled !== false && anim && transformActiveAt(anim, time)) {
124
124
  const local = transformToMat6(resolveTransformAnimation(time, anim))
125
125
  matrix = mul6(local, matrix)
126
126
  }
@@ -410,7 +410,7 @@ export function computeOpacity(
410
410
  for (const id of item.animationIds) {
411
411
  const animation = ctx.items.get(id) as Item.Animation | undefined
412
412
  const anim = animation?.anims.opacity
413
- if (animation?.enabled && anim && keyframesActiveAt(anim.track, time))
413
+ if (animation?.enabled !== false && anim && keyframesActiveAt(anim.track, time))
414
414
  opacity = resolveScalarAnimation(time, anim)
415
415
  }
416
416
  return opacity
@@ -15,6 +15,8 @@ export async function initStreams(
15
15
  items.map(async ({item, localTime}) => {
16
16
  if (item.kind !== Kind.Audio)
17
17
  return
18
+ if (item.enabled === false)
19
+ return
18
20
 
19
21
  const sink = await pool.getSink(item.mediaHash)
20
22
  if (!sink)
@@ -17,6 +17,9 @@ export async function sampleVisual(
17
17
  allowHandles?: boolean
18
18
  } = {}
19
19
  ): Promise<Layer[]> {
20
+ if ("enabled" in item && item.enabled === false)
21
+ return item.kind === Kind.Gap ? [{id: item.id, kind: "gap"}] : []
22
+
20
23
  const matrix = computeWorldMatrix(ctx.items, ancestors, item, time)
21
24
  const alpha = computeOpacity(ctx, item, time)
22
25
  const crop = "spatialId" in item && item.spatialId
@@ -25,7 +28,7 @@ export async function sampleVisual(
25
28
  const filters = "filterIds" in item && item.filterIds
26
29
  ? item.filterIds
27
30
  .map(id => ctx.items.get(id) as Item.Filter | undefined)
28
- .filter((filter): filter is Item.Filter => !!filter?.enabled)
31
+ .filter((filter): filter is Item.Filter => !!filter && filter.enabled !== false)
29
32
  .map(filter => ({type: filter.type, params: filter.params}) as FilterSpec)
30
33
  : undefined
31
34
 
@@ -63,9 +66,10 @@ export async function sampleVisual(
63
66
  case Kind.Text: {
64
67
  if (!options.allowHandles && (time < 0 || time >= item.duration)) return []
65
68
 
66
- const style = item.styleId
67
- ? (ctx.items.get(item.styleId) as Item.TextStyle)?.style
69
+ const textStyle = item.styleId
70
+ ? ctx.items.get(item.styleId) as Item.TextStyle | undefined
68
71
  : undefined
72
+ const style = textStyle?.enabled !== false ? textStyle?.style : undefined
69
73
 
70
74
  return [{id: item.id, kind: "text", content: item.content, style, matrix, alpha, crop, filters}]
71
75
  }
@@ -81,9 +85,10 @@ export async function sampleVisual(
81
85
  if (!segment)
82
86
  return []
83
87
 
84
- const style = item.styleId
85
- ? (ctx.items.get(item.styleId) as Item.TextStyle)?.style
88
+ const textStyle = item.styleId
89
+ ? ctx.items.get(item.styleId) as Item.TextStyle | undefined
86
90
  : undefined
91
+ const style = textStyle?.enabled !== false ? textStyle?.style : undefined
87
92
 
88
93
  return [{id: item.id, kind: "text", content: segment.text, style, matrix, alpha, crop, filters}]
89
94
  }
@@ -53,6 +53,9 @@ function sampleSequenceAt(ctx: SampleContext, seq: Item.Sequence, time: Ms) {
53
53
 
54
54
  const localTime = ms(time - cursor)
55
55
 
56
+ if (child.enabled === false)
57
+ return {isTransitioning: false, item: child, localTime} as const
58
+
56
59
  if (child.kind !== Kind.Transition)
57
60
  return {isTransitioning: false, item: child, localTime} as const
58
61
 
@@ -92,6 +92,21 @@ export default Science.suite({
92
92
  expect(textLayer[0].kind).is("text")
93
93
  }),
94
94
 
95
+ "disabled visual item keeps sequence time but does not render": test(async () => {
96
+ const {omni, videoA, resolveMedia} = await setupTest()
97
+ const {timeline} = new O({timeline: omni.timeline(o => o.sequence(
98
+ o.video(videoA, {duration: 2000, enabled: false}),
99
+ o.video(videoA, {duration: 2000}),
100
+ ))})
101
+ const sampler = createVisualSampler(resolveMedia)
102
+
103
+ const disabledLayers = await sampler.sample(timeline, ms(1000))
104
+ expect(disabledLayers.length).is(0)
105
+
106
+ const nextLayers = await sampler.sample(timeline, ms(2500))
107
+ expect(nextLayers[0].kind).is("image")
108
+ }),
109
+
95
110
  "audio mix sums overlapping chunks": test(async () => {
96
111
  const mixer = new AudioMix({chunkFrames: 4, clamp: false})
97
112
  async function *samples() {
@@ -10,7 +10,7 @@ import {filters, FilterParams, FilterType} from "../parts/filters.js"
10
10
  import {CaptionOptions, CaptionSourceItem} from "../parts/captions.js"
11
11
  import {Transcription} from "../../features/speech/transcribe/types.js"
12
12
  import {AnimationPreset, PresetOptions} from "../parts/animations/types.js"
13
- import {Crop, FilterableItem, Item, ItemMeta, VisualAnimatableItem} from "../parts/item.js"
13
+ import {Crop, FilterableItem, Item, ItemBase, VisualAnimatableItem} from "../parts/item.js"
14
14
  import {animationPresets, visualAnimations} from "../parts/animations/registry.js"
15
15
  import {Anim, AnimateAction, Interpolation, Keyframes, TrackTransform, Transform, Vec2, VisualAnimationInput, VisualAnimations} from "../types.js"
16
16
 
@@ -22,7 +22,7 @@ type BuildPresetAnimateActions = {
22
22
  [TKey in AnimationPreset]: BuildPresetAnimateAction
23
23
  }
24
24
  type BuildTransitionActions = {
25
- [TKey in TransitionName]: (duration: number, options?: ItemMeta) => Build<Item.Transition>
25
+ [TKey in TransitionName]: (duration: number, options?: ItemBase) => Build<Item.Transition>
26
26
  }
27
27
  type ContainerInput = [label: string, ...items: Build[]] | Build[]
28
28
 
@@ -69,6 +69,7 @@ export function video(
69
69
  start?: number,
70
70
  duration?: number
71
71
  label?: string
72
+ enabled?: boolean
72
73
  }
73
74
  ): Build<Item.Video> {
74
75
  return o => o.video(media, options)
@@ -79,6 +80,7 @@ export function image(
79
80
  options?: {
80
81
  duration?: number
81
82
  label?: string
83
+ enabled?: boolean
82
84
  }
83
85
  ): Build<Item.Image> {
84
86
  return o => o.image(media, options)
@@ -91,6 +93,7 @@ export function audio(
91
93
  duration?: number,
92
94
  gain?: number
93
95
  label?: string
96
+ enabled?: boolean
94
97
  }
95
98
  ): Build<Item.Audio> {
96
99
  return o => o.audio(media, options)
@@ -102,6 +105,7 @@ export function text(
102
105
  duration?: number,
103
106
  styles?: TextStyleOptions
104
107
  label?: string
108
+ enabled?: boolean
105
109
  }
106
110
  ): Build<Item.Text> {
107
111
  return o => o.text(content, options)
@@ -115,7 +119,7 @@ export function captions(
115
119
  return o => o.captions(item(o), transcript, options)
116
120
  }
117
121
 
118
- export function gap(duration: number, options?: ItemMeta): Build<Item.Gap> {
122
+ export function gap(duration: number, options?: ItemBase): Build<Item.Gap> {
119
123
  return o => o.gap(duration, options)
120
124
  }
121
125
 
@@ -249,7 +253,7 @@ export function textStyle(style: TextStyleOptions): Build<Item.TextStyle> {
249
253
 
250
254
  function makeTransitionActions(): BuildTransitionActions {
251
255
  const entries = Object.keys(transitions)
252
- .map(key => [key, (duration: number, options?: ItemMeta) => (o: O) => o.transition[key as TransitionName](duration, options)])
256
+ .map(key => [key, (duration: number, options?: ItemBase) => (o: O) => o.transition[key as TransitionName](duration, options)])
253
257
  return Object.fromEntries(entries) as BuildTransitionActions
254
258
  }
255
259
 
@@ -6,7 +6,7 @@ import {Id, TimelineFile} from "../parts/basics.js"
6
6
  import {FilterAction, FilterActions} from "../parts/filters.js"
7
7
  import {filters, FilterParams, FilterType} from "../parts/filters.js"
8
8
  import {Transcription} from "../../features/speech/transcribe/types.js"
9
- import {Crop, FilterableItem, Item, ItemMeta, Kind, VisualAnimatableItem} from "../parts/item.js"
9
+ import {Crop, FilterableItem, Item, ItemBase, Kind, VisualAnimatableItem} from "../parts/item.js"
10
10
  import {animationPresets, makeAnimationPresets, visualAnimations} from "../parts/animations/registry.js"
11
11
  import {TransitionAction, TransitionActions, transitions, TransitionName} from "../parts/transitions.js"
12
12
  import {AnimationPreset, PresetAnimateAction, PresetAnimateActions, PresetAnimation, PresetOptions} from "../parts/animations/types.js"
@@ -52,7 +52,7 @@ export class O {
52
52
  const item = {
53
53
  id: this.getId(),
54
54
  kind: Kind.TextStyle,
55
- style
55
+ style,
56
56
  } as Item.TextStyle
57
57
  this.register(item)
58
58
  return item
@@ -64,7 +64,6 @@ export class O {
64
64
  kind: Kind.Spatial,
65
65
  transform: transform ?? this.transform(),
66
66
  crop,
67
- enabled: true
68
67
  }
69
68
  this.register(item)
70
69
  return item
@@ -75,7 +74,6 @@ export class O {
75
74
  id: this.getId(),
76
75
  kind: Kind.Animation,
77
76
  anims,
78
- enabled: true
79
77
  }
80
78
  this.register(item)
81
79
  return item
@@ -136,7 +134,6 @@ export class O {
136
134
  kind: Kind.Filter,
137
135
  type,
138
136
  params,
139
- enabled: true
140
137
  }
141
138
  this.register(item)
142
139
  return item
@@ -265,6 +262,7 @@ export class O {
265
262
  start?: number,
266
263
  duration?: number
267
264
  label?: string
265
+ enabled?: boolean
268
266
  }): Item.Video => {
269
267
 
270
268
  if(!media.hasVideo)
@@ -274,6 +272,7 @@ export class O {
274
272
  kind: Kind.Video,
275
273
  id: this.getId(),
276
274
  label: options?.label,
275
+ enabled: options?.enabled,
277
276
  mediaHash: media.datafile.checksum.hash,
278
277
  start: options?.start ?? 0,
279
278
  duration: options?.duration ?? media.duration
@@ -287,6 +286,7 @@ export class O {
287
286
  options?: {
288
287
  duration?: number
289
288
  label?: string
289
+ enabled?: boolean
290
290
  }): Item.Image => {
291
291
 
292
292
  if(!media.isImage)
@@ -296,6 +296,7 @@ export class O {
296
296
  kind: Kind.Image,
297
297
  id: this.getId(),
298
298
  label: options?.label,
299
+ enabled: options?.enabled,
299
300
  mediaHash: media.datafile.checksum.hash,
300
301
  duration: options?.duration ?? 2000
301
302
  }
@@ -310,6 +311,7 @@ export class O {
310
311
  duration?: number,
311
312
  gain?: number
312
313
  label?: string
314
+ enabled?: boolean
313
315
  }): Item.Audio => {
314
316
 
315
317
  if(!media.hasAudio)
@@ -319,6 +321,7 @@ export class O {
319
321
  kind: Kind.Audio,
320
322
  id: this.getId(),
321
323
  label: options?.label,
324
+ enabled: options?.enabled,
322
325
  mediaHash: media.datafile.checksum.hash,
323
326
  start: options?.start ?? 0,
324
327
  duration: options?.duration ?? media.duration,
@@ -332,11 +335,13 @@ export class O {
332
335
  duration?: number,
333
336
  styles?: TextStyleOptions
334
337
  label?: string
338
+ enabled?: boolean
335
339
  }): Item.Text => {
336
340
 
337
341
  const item = {
338
342
  id: this.getId(),
339
343
  label: options?.label,
344
+ enabled: options?.enabled,
340
345
  content,
341
346
  kind: Kind.Text,
342
347
  duration: options?.duration ?? 2000
@@ -360,6 +365,7 @@ export class O {
360
365
  id: this.getId(),
361
366
  kind: Kind.Caption,
362
367
  label: options?.label,
368
+ enabled: options?.enabled,
363
369
  transcript,
364
370
  itemId: options?.itemId,
365
371
  start,
@@ -405,11 +411,12 @@ export class O {
405
411
  {presets: this.#makeCaptionPresetActions()}
406
412
  ) as CaptionActions
407
413
 
408
- gap = (duration: number, options?: ItemMeta): Item.Gap => {
414
+ gap = (duration: number, options?: ItemBase): Item.Gap => {
409
415
  const item = {
410
416
  id: this.getId(),
411
417
  kind: Kind.Gap,
412
418
  label: options?.label,
419
+ enabled: options?.enabled,
413
420
  duration
414
421
  } as Item.Gap
415
422
  this.register(item)
@@ -417,11 +424,12 @@ export class O {
417
424
  }
418
425
 
419
426
  #makeTransition = (key: TransitionName): TransitionAction => {
420
- return (duration: number, options?: ItemMeta): Item.Transition => {
427
+ return (duration: number, options?: ItemBase): Item.Transition => {
421
428
  const item: Item.Transition = {
422
429
  id: this.getId(),
423
430
  kind: Kind.Transition,
424
431
  label: options?.label,
432
+ enabled: options?.enabled,
425
433
  name: transitions[key].name,
426
434
  duration,
427
435
  }