@omnimedia/omnitool 1.1.0-52 → 1.1.0-55

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 (50) hide show
  1. package/package.json +1 -1
  2. package/s/demo/routines/transcode-test.ts +2 -2
  3. package/s/demo/routines/transitions-test.ts +2 -2
  4. package/s/driver/driver.ts +26 -5
  5. package/s/driver/fns/schematic.ts +2 -0
  6. package/s/driver/fns/work.ts +41 -20
  7. package/s/timeline/parts/waveform.ts +1 -1
  8. package/s/timeline/renderers/export/parts/cursor.ts +75 -56
  9. package/s/timeline/renderers/export/parts/produce-video.ts +2 -3
  10. package/s/timeline/renderers/player/parts/playback.ts +33 -6
  11. package/s/timeline/renderers/player/player.ts +2 -14
  12. package/s/timeline/renderers/renderers.test.ts +1 -3
  13. package/s/timeline/sugar/o.ts +10 -10
  14. package/x/demo/demo.bundle.min.js +9 -9
  15. package/x/demo/demo.bundle.min.js.map +4 -4
  16. package/x/demo/routines/transcode-test.js +2 -2
  17. package/x/demo/routines/transcode-test.js.map +1 -1
  18. package/x/demo/routines/transitions-test.js +2 -2
  19. package/x/demo/routines/transitions-test.js.map +1 -1
  20. package/x/driver/driver.d.ts +14 -2
  21. package/x/driver/driver.js +26 -5
  22. package/x/driver/driver.js.map +1 -1
  23. package/x/driver/driver.worker.bundle.min.js +1 -1
  24. package/x/driver/driver.worker.bundle.min.js.map +3 -3
  25. package/x/driver/fns/host.d.ts +2 -0
  26. package/x/driver/fns/schematic.d.ts +2 -0
  27. package/x/driver/fns/work.d.ts +2 -0
  28. package/x/driver/fns/work.js +33 -20
  29. package/x/driver/fns/work.js.map +1 -1
  30. package/x/index.html +2 -2
  31. package/x/tests.bundle.min.js +16 -16
  32. package/x/tests.bundle.min.js.map +4 -4
  33. package/x/tests.html +1 -1
  34. package/x/timeline/parts/waveform.js +1 -1
  35. package/x/timeline/parts/waveform.js.map +1 -1
  36. package/x/timeline/renderers/export/parts/cursor.d.ts +4 -5
  37. package/x/timeline/renderers/export/parts/cursor.js +68 -50
  38. package/x/timeline/renderers/export/parts/cursor.js.map +1 -1
  39. package/x/timeline/renderers/export/parts/produce-video.js +2 -3
  40. package/x/timeline/renderers/export/parts/produce-video.js.map +1 -1
  41. package/x/timeline/renderers/player/parts/playback.d.ts +9 -5
  42. package/x/timeline/renderers/player/parts/playback.js +25 -6
  43. package/x/timeline/renderers/player/parts/playback.js.map +1 -1
  44. package/x/timeline/renderers/player/player.js +2 -9
  45. package/x/timeline/renderers/player/player.js.map +1 -1
  46. package/x/timeline/renderers/renderers.test.js +1 -2
  47. package/x/timeline/renderers/renderers.test.js.map +1 -1
  48. package/x/timeline/sugar/o.d.ts +1 -0
  49. package/x/timeline/sugar/o.js +10 -10
  50. package/x/timeline/sugar/o.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnimedia/omnitool",
3
- "version": "1.1.0-52",
3
+ "version": "1.1.0-55",
4
4
  "description": "open source video processing tools",
5
5
  "license": "MIT",
6
6
  "author": "Przemysław Gałęzki",
@@ -38,8 +38,8 @@ export function setupTranscodeTest(driver: Driver, source: DecoderSource) {
38
38
  const audio = driver.decodeAudio({source})
39
39
 
40
40
  const {readable, done} = driver.encode({
41
- video,
42
- audio,
41
+ video: video.readable,
42
+ audio: audio.readable,
43
43
  config: {
44
44
  audio: {codec: "opus", bitrate: 128000},
45
45
  video: {codec: "vp9", bitrate: 1000000}
@@ -30,8 +30,8 @@ export async function setupTransitionsTest(driver: Driver, source: DecoderSource
30
30
  }
31
31
  })
32
32
 
33
- await driver.encode({
34
- video,
33
+ driver.encode({
34
+ video: video.readable,
35
35
  config: {
36
36
  audio: {codec: "opus", bitrate: 128000},
37
37
  video: {codec: "vp9", bitrate: 1000000}
@@ -58,33 +58,54 @@ export class Driver {
58
58
 
59
59
  decodeVideo(input: DecoderInput) {
60
60
  let lastFrame: VideoFrame | null = null
61
+ const {port1, port2} = new MessageChannel()
61
62
  const videoTransform = new TransformStream<VideoFrame, VideoFrame>({
62
63
  async transform(chunk, controller) {
63
64
  const frame = await input.onFrame?.(chunk) ?? chunk
64
- // below code is to prevent mem leaks and hardware accelerated decoder stall
65
65
  lastFrame?.close()
66
66
  controller.enqueue(frame)
67
67
  lastFrame = frame
68
68
  }
69
69
  })
70
- this.thread.work.decodeVideo[tune]({transfer: [videoTransform.writable]})({
70
+ this.thread.work.decodeVideo[tune]({transfer: [videoTransform.writable, port2]})({
71
71
  source: input.source,
72
+ cancel: port2,
72
73
  video: videoTransform.writable,
73
74
  start: input.start,
74
75
  end: input.end
75
76
  })
76
- return videoTransform.readable
77
+ return {
78
+ readable: videoTransform.readable,
79
+ /**
80
+ * use this to stop decoding (premature interruption)
81
+ * */
82
+ cancel() {
83
+ port1.postMessage("close")
84
+ port1.close()
85
+ }
86
+ }
77
87
  }
78
88
 
79
89
  decodeAudio(input: DecoderInput) {
80
90
  const audioTransform = new TransformStream<AudioData, AudioData>()
81
- this.thread.work.decodeAudio[tune]({transfer: [audioTransform.writable]})({
91
+ const {port1, port2} = new MessageChannel()
92
+ this.thread.work.decodeAudio[tune]({transfer: [audioTransform.writable, port2]})({
82
93
  source: input.source,
94
+ cancel: port2,
83
95
  audio: audioTransform.writable,
84
96
  start: input.start,
85
97
  end: input.end
86
98
  })
87
- return audioTransform.readable
99
+ return {
100
+ readable: audioTransform.readable,
101
+ /**
102
+ * use this to stop decoding (premature interruption)
103
+ * */
104
+ cancel() {
105
+ port1.postMessage("close")
106
+ port1.close()
107
+ }
108
+ }
88
109
  }
89
110
 
90
111
  encode({audio, video, config}: EncoderInput) {
@@ -14,6 +14,7 @@ export type DriverSchematic = AsSchematic<{
14
14
 
15
15
  decodeAudio(input: {
16
16
  source: DecoderSource
17
+ cancel: MessagePort
17
18
  audio: WritableStream<AudioData>
18
19
  start?: number
19
20
  end?: number
@@ -21,6 +22,7 @@ export type DriverSchematic = AsSchematic<{
21
22
 
22
23
  decodeVideo(input: {
23
24
  source: DecoderSource
25
+ cancel: MessagePort
24
26
  video: WritableStream<VideoFrame>
25
27
  start?: number
26
28
  end?: number
@@ -20,7 +20,7 @@ export const setupDriverWork = (
20
20
  await shell.host.world()
21
21
  },
22
22
 
23
- async decodeAudio({source, audio, start, end}) {
23
+ async decodeAudio({source, audio, start, end, cancel}) {
24
24
  const input = new Input({
25
25
  source: await loadSource(source),
26
26
  formats: ALL_FORMATS
@@ -30,19 +30,29 @@ export const setupDriverWork = (
30
30
  const audioDecodable = await audioTrack?.canDecode()
31
31
  const audioWriter = audio.getWriter()
32
32
 
33
- if (audioDecodable && audioTrack) {
34
- const sink = new AudioSampleSink(audioTrack)
35
- for await (const sample of sink.samples(start, end)) {
36
- const frame = sample.toAudioData()
37
- await audioWriter.write(frame)
38
- sample.close()
39
- frame.close()
40
- }
41
- await audioWriter.close()
33
+ if(!audioDecodable || !audioTrack)
34
+ return
35
+
36
+ const sink = new AudioSampleSink(audioTrack)
37
+ const samples = sink.samples(start, end)
38
+
39
+ cancel.onmessage = async () => {
40
+ samples.return()
41
+ input.dispose()
42
+ cancel.close()
42
43
  }
44
+
45
+ for await (const sample of samples) {
46
+ const frame = sample.toAudioData()
47
+ sample.close()
48
+ await audioWriter.write(frame)
49
+ frame.close()
50
+ }
51
+
52
+ await audioWriter.close()
43
53
  },
44
54
 
45
- async decodeVideo({source, video, start, end}) {
55
+ async decodeVideo({source, video, start, end, cancel}) {
46
56
  const input = new Input({
47
57
  source: await loadSource(source),
48
58
  formats: ALL_FORMATS
@@ -52,16 +62,26 @@ export const setupDriverWork = (
52
62
  const videoDecodable = await videoTrack?.canDecode()
53
63
  const videoWriter = video.getWriter()
54
64
 
55
- if (videoDecodable && videoTrack) {
56
- const sink = new VideoSampleSink(videoTrack)
57
- for await (const sample of sink.samples(start, end)) {
58
- const frame = sample.toVideoFrame()
59
- await videoWriter.write(frame)
60
- sample.close()
61
- frame.close()
62
- }
63
- await videoWriter.close()
65
+ if(!videoDecodable || !videoTrack)
66
+ return
67
+
68
+ const sink = new VideoSampleSink(videoTrack)
69
+ const samples = sink.samples(start, end)
70
+
71
+ cancel.onmessage = async () => {
72
+ samples.return()
73
+ input.dispose()
74
+ cancel.close()
64
75
  }
76
+
77
+ for await (const sample of samples) {
78
+ const frame = sample.toVideoFrame()
79
+ sample.close()
80
+ await videoWriter.write(frame)
81
+ frame.close()
82
+ }
83
+
84
+ await videoWriter.close()
65
85
  },
66
86
 
67
87
  async encode({video, audio, config, writable}) {
@@ -78,6 +98,7 @@ export const setupDriverWork = (
78
98
  const sample = new VideoSample(frame)
79
99
  await videoSource.add(sample)
80
100
  sample.close()
101
+ frame.close()
81
102
  }
82
103
  }
83
104
 
@@ -20,7 +20,7 @@ export class Waveform {
20
20
  }
21
21
 
22
22
  static async init(driver: Driver, source: DecoderSource, container: HTMLElement) {
23
- const reader = driver.decodeAudio({source}).getReader()
23
+ const reader = driver.decodeAudio({source}).readable.getReader()
24
24
 
25
25
  const peaks: number[] = []
26
26
  let buffer: number[] = []
@@ -1,6 +1,5 @@
1
1
 
2
2
  import {ms, Ms} from "../../../../units/ms.js"
3
- import {Item} from "../../../parts/item.js"
4
3
  import {Driver} from "../../../../driver/driver.js"
5
4
  import {TimelineFile} from "../../../parts/basics.js"
6
5
  import {DecoderSource} from "../../../../driver/fns/schematic.js"
@@ -13,87 +12,107 @@ import {createVisualSampler} from "../../parts/samplers/visual/sampler.js"
13
12
  */
14
13
 
15
14
  export class CursorVisualSampler {
16
- #sampler
15
+ #lastTimecode = -Infinity
17
16
  #videoCursors = new Map<number, VideoFrameCursor>()
17
+ #sampler
18
18
 
19
19
  constructor(
20
20
  private driver: Driver,
21
- private resolveMedia: (hash: string) => DecoderSource
21
+ private resolveMedia: (hash: string) => DecoderSource,
22
+ private timeline: TimelineFile
22
23
  ) {
23
- this.#sampler = createVisualSampler(resolveMedia, (item, time) => {
24
- const mediaTime = toUs(ms(item.start + time))
25
- const cursor = this.#getCursorForVideo(item)
26
- return cursor.next(mediaTime)
24
+ this.#sampler = createVisualSampler(this.resolveMedia, (item, time) => {
25
+ const targetUs = toUs(time)
26
+ let cursor = this.#videoCursors.get(item.id)
27
+
28
+ if (!cursor) {
29
+ const source = this.resolveMedia(item.mediaHash)
30
+ const endUs = toUs(ms(item.start + item.duration))
31
+ cursor = this.#createVideoCursor(source, targetUs, endUs)
32
+ this.#videoCursors.set(item.id, cursor)
33
+ }
34
+
35
+ return cursor.next(targetUs)
27
36
  })
28
37
  }
29
38
 
30
- cursor(timeline: TimelineFile) {
31
- let lastTimecode = Number.NEGATIVE_INFINITY
32
- return {
33
- next: (timecode: Ms) => {
34
- if (timecode < lastTimecode)
35
- throw new Error(`CursorVisualSampler is forward-only: requested ${timecode}ms after ${lastTimecode}ms`)
36
- lastTimecode = timecode
37
- return this.#sampler.sample(timeline, timecode)
38
- },
39
- cancel: () => this.#cancel(),
40
- }
41
- }
42
-
43
- #getCursorForVideo(videoItem: Item.Video) {
44
- const existing = this.#videoCursors.get(videoItem.id)
45
- if (existing)
46
- return existing
47
-
48
- const source = this.resolveMedia(videoItem.mediaHash)
49
- const video = this.driver.decodeVideo({ source })
50
- const cursor = this.#cursor(video.getReader())
39
+ next(timecode: Ms) {
40
+ if (timecode < this.#lastTimecode)
41
+ throw new Error(`Forward-only cursor regression: ${timecode}ms < ${this.#lastTimecode}ms`)
51
42
 
52
- this.#videoCursors.set(videoItem.id, cursor)
53
- return cursor
43
+ this.#lastTimecode = timecode
44
+ return this.#sampler.sample(this.timeline, timecode)
54
45
  }
55
46
 
56
- async #cancel() {
57
- await Promise.all(
58
- [...this.#videoCursors.values()].map(cursor => cursor.cancel())
59
- )
47
+ async cancel() {
48
+ await Promise.all([...this.#videoCursors.values()].map(c => c.cancel()))
60
49
  this.#videoCursors.clear()
61
50
  }
62
51
 
63
- // forward only
64
- #cursor(reader: ReadableStreamDefaultReader<VideoFrame>) {
52
+ #createVideoCursor(source: DecoderSource, startUs: number, endUs: number): VideoFrameCursor {
53
+ const video = this.driver.decodeVideo({source, start: startUs / 1_000_000, end: endUs / 1_000_000})
54
+ const reader = video.readable.getReader()
55
+
56
+ let current: VideoFrame | null = null
57
+ let nextPromise: Promise<VideoFrame | null> | null = null
58
+ let ended = false
59
+
60
+ const readNext = async () => {
61
+ if (ended) return null
62
+ const {done, value} = await reader.read()
63
+ if (done) return (ended = true, null)
64
+
65
+ const frame = new VideoFrame(value)
66
+ value.close()
67
+ return frame
68
+ }
69
+
65
70
  return {
66
71
  async next(targetUs: number): Promise<VideoFrame | undefined> {
67
- let prev: VideoFrame | null = null
72
+ current ??= await readNext()
73
+ if (!current) return undefined
74
+
68
75
  while (true) {
69
- const { done, value: hit } = await reader.read()
76
+ nextPromise ??= readNext()
77
+ const nextFrame = await nextPromise
70
78
 
71
- if (done) {
72
- const out = prev ? new VideoFrame(prev) : undefined
73
- prev?.close()
74
- return out
75
- }
79
+ if (!nextFrame) return new VideoFrame(current)
76
80
 
77
- const hitUs = hit.timestamp ?? 0
78
- if (hitUs >= targetUs) {
79
- const prevUs = prev?.timestamp ?? Number.NEGATIVE_INFINITY
80
- const usePrev = !!prev && Math.abs(prevUs - targetUs) < Math.abs(hitUs - targetUs)
81
+ const currentUs = current.timestamp ?? -Infinity
82
+ const nextUs = nextFrame.timestamp ?? currentUs
81
83
 
82
- const chosen = usePrev ? prev! : hit
83
- const other = usePrev ? hit : prev
84
+ if (nextUs < targetUs) {
85
+ current.close()
86
+ current = nextFrame
87
+ nextPromise = null
88
+ continue
89
+ }
90
+
91
+ const useNext = Math.abs(nextUs - targetUs) < Math.abs(currentUs - targetUs)
84
92
 
85
- const copy = new VideoFrame(chosen)
86
- chosen.close()
87
- other?.close()
88
- return copy
93
+ if (useNext) {
94
+ current.close()
95
+ current = nextFrame
96
+ nextPromise = null
97
+ continue
89
98
  }
90
99
 
91
- prev?.close()
92
- prev = hit
100
+ return new VideoFrame(current)
93
101
  }
94
102
  },
95
103
 
96
- cancel: async () => await reader.cancel()
104
+ async cancel() {
105
+ const pending = nextPromise
106
+ nextPromise = null
107
+
108
+ current?.close()
109
+ current = null
110
+
111
+ video.cancel()
112
+
113
+ const buffered = await pending?.catch(() => null)
114
+ buffered?.close()
115
+ }
97
116
  }
98
117
  }
99
118
  }
@@ -21,14 +21,13 @@ export function produceVideo({
21
21
 
22
22
  const stream = new TransformStream<VideoFrame, VideoFrame>()
23
23
  const writer = stream.writable.getWriter()
24
- const sampler = new CursorVisualSampler(driver, resolveMedia)
25
- const cursor = sampler.cursor(timeline)
24
+ const sampler = new CursorVisualSampler(driver, resolveMedia, timeline)
26
25
  const dt = 1 / fps
27
26
  const duration = computeItemDuration(timeline.rootId, timeline)
28
27
 
29
28
  async function produce() {
30
29
  await fixedStep({fps, duration}, async (timecode, i) => {
31
- const layers = await cursor.next(timecode)
30
+ const layers = await sampler.next(timecode)
32
31
  const composed = await driver.composite(layers)
33
32
 
34
33
  const frame = new VideoFrame(composed, {
@@ -1,17 +1,20 @@
1
1
 
2
- import {pub} from '@e280/stz'
3
2
  import {Fps} from '../../../../units/fps.js'
4
3
  import {ms, Ms} from '../../../../units/ms.js'
4
+ import {Driver} from '../../../../driver/driver.js'
5
5
  import {realtime} from '../../parts/schedulers.js'
6
6
  import {TimelineFile} from '../../../parts/basics.js'
7
+ import {computeItemDuration} from '../../parts/handy.js'
7
8
  import {seconds, Seconds} from '../../../../units/seconds.js'
9
+ import {CursorVisualSampler} from '../../export/parts/cursor.js'
8
10
  import {DecoderSource} from '../../../../driver/fns/schematic.js'
9
11
  import {createAudioSampler} from '../../parts/samplers/audio/sampler.js'
10
12
  import {createVisualSampler} from '../../parts/samplers/visual/sampler.js'
11
13
 
12
14
  export class Playback {
13
- visualSampler
14
15
  audioSampler
16
+ seekVisualSampler
17
+ playVisualSampler: CursorVisualSampler | null = null
15
18
 
16
19
  #playbackStart = ms(0)
17
20
  #audioStartSec: number | null = null
@@ -25,28 +28,39 @@ export class Playback {
25
28
  #audioAbort: AbortController | null = null
26
29
 
27
30
  constructor(
31
+ private driver: Driver,
28
32
  private timeline: TimelineFile,
29
33
  private resolveMedia: (hash: string) => DecoderSource
30
34
  ) {
31
35
  this.audioGain.connect(this.audioContext.destination)
32
36
  this.audioGain.gain.value = 0.7 ** 2
33
- this.visualSampler = createVisualSampler(this.resolveMedia)
37
+ this.seekVisualSampler = createVisualSampler(this.resolveMedia)
34
38
  this.audioSampler = createAudioSampler(this.resolveMedia)
39
+ this.#samples()
35
40
  }
36
41
 
37
- async *samples() {
42
+ async #samples() {
38
43
  for await (const _ of this.#controller.ticks()) {
39
- yield this.visualSampler.sample(this.timeline, this.currentTime)
44
+ const layers = await this.playVisualSampler?.next(this.currentTime) ?? []
45
+
46
+ const frame = await this.driver.composite(layers)
47
+ frame.close()
48
+
49
+ if (this.currentTime >= this.duration)
50
+ this.pause()
40
51
  }
41
52
  }
42
53
 
43
54
  async seek(time: Ms) {
44
55
  this.pause()
45
56
  this.#playbackStart = time
46
- return await this.visualSampler.sample(this.timeline, time)
57
+ return await this.seekVisualSampler.sample(this.timeline, time)
47
58
  }
48
59
 
49
60
  async start(timeline: TimelineFile) {
61
+ if(this.#controller.isPlaying())
62
+ return
63
+
50
64
  this.timeline = timeline
51
65
  await this.audioContext.resume()
52
66
 
@@ -61,6 +75,9 @@ export class Playback {
61
75
 
62
76
  this.audioNodes.clear()
63
77
 
78
+ this.playVisualSampler?.cancel()
79
+ this.playVisualSampler = new CursorVisualSampler(this.driver, this.resolveMedia, this.timeline)
80
+
64
81
  this.#controller.play()
65
82
  this.#startAudio(this.#audioAbort.signal, seconds(this.#playbackStart / 1000))
66
83
  }
@@ -74,6 +91,16 @@ export class Playback {
74
91
  node.stop()
75
92
 
76
93
  this.audioNodes.clear()
94
+
95
+ this.playVisualSampler?.cancel()
96
+ this.playVisualSampler = null
97
+ }
98
+
99
+ get duration() {
100
+ return computeItemDuration(
101
+ this.timeline.rootId,
102
+ this.timeline
103
+ )
77
104
  }
78
105
 
79
106
  get currentTime() {
@@ -4,7 +4,6 @@ import {fps} from "../../../units/fps.js"
4
4
  import {Playback} from "./parts/playback.js"
5
5
  import {Driver} from "../../../driver/driver.js"
6
6
  import {TimelineFile} from "../../parts/basics.js"
7
- import {computeItemDuration} from "../parts/handy.js"
8
7
  import {DecoderSource} from "../../../driver/fns/schematic.js"
9
8
 
10
9
  type ResolveMedia = (hash: string) => DecoderSource
@@ -18,20 +17,12 @@ export class VideoPlayer {
18
17
  resolveMedia: ResolveMedia,
19
18
  private timeline: TimelineFile,
20
19
  ) {
21
- this.playback = new Playback(timeline, resolveMedia)
20
+ this.playback = new Playback(driver, timeline, resolveMedia)
22
21
  this.canvas = driver.compositor.pixi.renderer.canvas
23
22
  }
24
23
 
25
24
  async play() {
26
25
  await this.playback.start(this.timeline)
27
-
28
- for await (const layers of this.playback.samples()) {
29
- const frame = await this.driver.composite(layers)
30
- frame.close()
31
-
32
- if (this.currentTime >= this.duration)
33
- this.pause()
34
- }
35
26
  }
36
27
 
37
28
  pause() {
@@ -49,10 +40,7 @@ export class VideoPlayer {
49
40
  }
50
41
 
51
42
  get duration() {
52
- return computeItemDuration(
53
- this.timeline.rootId,
54
- this.timeline
55
- )
43
+ return this.playback.duration
56
44
  }
57
45
 
58
46
  get currentTime() {
@@ -49,8 +49,7 @@ export default Science.suite({
49
49
  o.video(videoA, {duration: 2000}),
50
50
  o.audio(videoA, {duration: 500}),
51
51
  ))})
52
- const sampler = new CursorVisualSampler(driver, resolveMedia)
53
- const cursor = sampler.cursor(timeline)
52
+ const cursor = new CursorVisualSampler(driver, resolveMedia, timeline)
54
53
  await cursor.next(ms(1000))
55
54
  await expect(async () => await cursor.next(ms(100))).throwsAsync()
56
55
  }),
@@ -384,4 +383,3 @@ export default Science.suite({
384
383
  expect(totalFrames).is(240000)
385
384
  })
386
385
  })
387
-
@@ -20,7 +20,7 @@ export class O {
20
20
  return this.state.timeline
21
21
  }
22
22
 
23
- #getId() {
23
+ getId() {
24
24
  return hex.toInteger(hex.random())
25
25
  }
26
26
 
@@ -37,7 +37,7 @@ export class O {
37
37
 
38
38
  textStyle = (style: TextStyleOptions): Item.TextStyle => {
39
39
  const item = {
40
- id: this.#getId(),
40
+ id: this.getId(),
41
41
  kind: Kind.TextStyle,
42
42
  style
43
43
  } as Item.TextStyle
@@ -47,7 +47,7 @@ export class O {
47
47
 
48
48
  spatial = (transform: Transform): Item.Spatial => {
49
49
  const item: Item.Spatial = {
50
- id: this.#getId(),
50
+ id: this.getId(),
51
51
  kind: Kind.Spatial,
52
52
  transform,
53
53
  enabled: true
@@ -58,7 +58,7 @@ export class O {
58
58
 
59
59
  sequence = (...items: Item.Any[]): Item.Sequence => {
60
60
  const item = {
61
- id: this.#getId(),
61
+ id: this.getId(),
62
62
  kind: Kind.Sequence,
63
63
  childrenIds: items.map(item => item.id)
64
64
  } as Item.Sequence
@@ -69,7 +69,7 @@ export class O {
69
69
  stack = (...items: Item.Any[]): Item.Stack => {
70
70
  const item = {
71
71
  kind: Kind.Stack,
72
- id: this.#getId(),
72
+ id: this.getId(),
73
73
  childrenIds: items.map(item => item.id)
74
74
  } as Item.Stack
75
75
  this.register(item)
@@ -88,7 +88,7 @@ export class O {
88
88
 
89
89
  const item: Item.Video = {
90
90
  kind: Kind.Video,
91
- id: this.#getId(),
91
+ id: this.getId(),
92
92
  mediaHash: media.datafile.checksum.hash,
93
93
  start: options?.start ?? 0,
94
94
  duration: options?.duration ?? media.duration
@@ -110,7 +110,7 @@ export class O {
110
110
 
111
111
  const item: Item.Audio = {
112
112
  kind: Kind.Audio,
113
- id: this.#getId(),
113
+ id: this.getId(),
114
114
  mediaHash: media.datafile.checksum.hash,
115
115
  start: options?.start ?? 0,
116
116
  duration: options?.duration ?? media.duration,
@@ -126,7 +126,7 @@ export class O {
126
126
  }): Item.Text => {
127
127
 
128
128
  const item = {
129
- id: this.#getId(),
129
+ id: this.getId(),
130
130
  content,
131
131
  kind: Kind.Text,
132
132
  duration: options?.duration ?? 2000
@@ -141,7 +141,7 @@ export class O {
141
141
 
142
142
  gap = (duration: number): Item.Gap => {
143
143
  const item = {
144
- id: this.#getId(),
144
+ id: this.getId(),
145
145
  kind: Kind.Gap,
146
146
  duration
147
147
  } as Item.Gap
@@ -152,7 +152,7 @@ export class O {
152
152
  transition = {
153
153
  crossfade: (duration: number): Item.Transition => {
154
154
  const item = {
155
- id: this.#getId(),
155
+ id: this.getId(),
156
156
  kind: Kind.Transition,
157
157
  effect: Effect.Crossfade,
158
158
  duration,