@omnimedia/omnitool 1.1.0-4 → 1.1.0-7

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 (133) hide show
  1. package/package.json +4 -2
  2. package/s/demo/demo.css +5 -0
  3. package/s/demo/routines/transcode-test.ts +4 -2
  4. package/s/demo/routines/transitions-test.ts +43 -0
  5. package/s/driver/driver.ts +17 -9
  6. package/s/driver/fns/schematic.ts +44 -21
  7. package/s/driver/fns/work.ts +112 -97
  8. package/s/features/transition/parts/fragment.ts +24 -0
  9. package/s/features/transition/parts/types.ts +94 -0
  10. package/s/features/transition/parts/uniforms.ts +29 -0
  11. package/s/features/transition/parts/vertex.ts +31 -0
  12. package/s/features/transition/transition.ts +60 -0
  13. package/s/index.html.ts +6 -1
  14. package/s/timeline/parts/basics.ts +1 -1
  15. package/s/timeline/parts/compositor/export.ts +77 -0
  16. package/s/timeline/parts/compositor/parts/html-tree.ts +37 -0
  17. package/s/timeline/parts/compositor/parts/schedulers.ts +85 -0
  18. package/s/timeline/parts/compositor/parts/tree-builder.ts +184 -0
  19. package/s/timeline/parts/compositor/parts/webcodecs-tree.ts +30 -0
  20. package/s/timeline/parts/compositor/playback.ts +81 -0
  21. package/s/timeline/parts/compositor/samplers/html.ts +115 -0
  22. package/s/timeline/parts/compositor/samplers/webcodecs.ts +60 -0
  23. package/s/timeline/parts/item.ts +38 -6
  24. package/s/timeline/parts/media.ts +21 -0
  25. package/s/timeline/parts/waveform.ts +1 -1
  26. package/s/timeline/sugar/builders.ts +102 -0
  27. package/s/timeline/sugar/o.ts +75 -16
  28. package/s/timeline/sugar/omni-test.ts +2 -2
  29. package/s/timeline/sugar/omni.ts +14 -11
  30. package/s/timeline/timeline.ts +22 -0
  31. package/s/timeline/types.ts +29 -0
  32. package/s/timeline/utils/audio-stream.ts +15 -0
  33. package/s/timeline/utils/matrix.ts +33 -0
  34. package/s/timeline/utils/video-cursor.ts +40 -0
  35. package/x/demo/demo.bundle.min.js +39 -37
  36. package/x/demo/demo.bundle.min.js.map +4 -4
  37. package/x/demo/demo.css +5 -0
  38. package/x/demo/routines/transcode-test.js +4 -2
  39. package/x/demo/routines/transcode-test.js.map +1 -1
  40. package/x/demo/routines/transitions-test.d.ts +5 -0
  41. package/x/demo/routines/transitions-test.js +35 -0
  42. package/x/demo/routines/transitions-test.js.map +1 -0
  43. package/x/driver/driver.d.ts +3 -5
  44. package/x/driver/driver.js +16 -9
  45. package/x/driver/driver.js.map +1 -1
  46. package/x/driver/driver.worker.bundle.min.js +2537 -148
  47. package/x/driver/driver.worker.bundle.min.js.map +4 -4
  48. package/x/driver/fns/host.d.ts +9 -2
  49. package/x/driver/fns/schematic.d.ts +38 -20
  50. package/x/driver/fns/work.d.ts +11 -4
  51. package/x/driver/fns/work.js +105 -96
  52. package/x/driver/fns/work.js.map +1 -1
  53. package/x/features/speech/transcribe/worker.bundle.min.js +541 -541
  54. package/x/features/speech/transcribe/worker.bundle.min.js.map +4 -4
  55. package/x/features/transition/parts/fragment.d.ts +1 -0
  56. package/x/features/transition/parts/fragment.js +25 -0
  57. package/x/features/transition/parts/fragment.js.map +1 -0
  58. package/x/features/transition/parts/types.d.ts +23 -0
  59. package/x/features/transition/parts/types.js +2 -0
  60. package/x/features/transition/parts/types.js.map +1 -0
  61. package/x/features/transition/parts/uniforms.d.ts +31 -0
  62. package/x/features/transition/parts/uniforms.js +27 -0
  63. package/x/features/transition/parts/uniforms.js.map +1 -0
  64. package/x/features/transition/parts/vertex.d.ts +1 -0
  65. package/x/features/transition/parts/vertex.js +32 -0
  66. package/x/features/transition/parts/vertex.js.map +1 -0
  67. package/x/features/transition/transition.d.ts +5 -0
  68. package/x/features/transition/transition.js +50 -0
  69. package/x/features/transition/transition.js.map +1 -0
  70. package/x/index.html +13 -3
  71. package/x/index.html.js +6 -1
  72. package/x/index.html.js.map +1 -1
  73. package/x/timeline/parts/basics.d.ts +1 -1
  74. package/x/timeline/parts/compositor/export.d.ts +9 -0
  75. package/x/timeline/parts/compositor/export.js +64 -0
  76. package/x/timeline/parts/compositor/export.js.map +1 -0
  77. package/x/timeline/parts/compositor/parts/html-tree.d.ts +3 -0
  78. package/x/timeline/parts/compositor/parts/html-tree.js +40 -0
  79. package/x/timeline/parts/compositor/parts/html-tree.js.map +1 -0
  80. package/x/timeline/parts/compositor/parts/schedulers.d.ts +15 -0
  81. package/x/timeline/parts/compositor/parts/schedulers.js +64 -0
  82. package/x/timeline/parts/compositor/parts/schedulers.js.map +1 -0
  83. package/x/timeline/parts/compositor/parts/tree-builder.d.ts +37 -0
  84. package/x/timeline/parts/compositor/parts/tree-builder.js +147 -0
  85. package/x/timeline/parts/compositor/parts/tree-builder.js.map +1 -0
  86. package/x/timeline/parts/compositor/parts/webcodecs-tree.d.ts +3 -0
  87. package/x/timeline/parts/compositor/parts/webcodecs-tree.js +28 -0
  88. package/x/timeline/parts/compositor/parts/webcodecs-tree.js.map +1 -0
  89. package/x/timeline/parts/compositor/playback.d.ts +19 -0
  90. package/x/timeline/parts/compositor/playback.js +71 -0
  91. package/x/timeline/parts/compositor/playback.js.map +1 -0
  92. package/x/timeline/parts/compositor/samplers/html.d.ts +3 -0
  93. package/x/timeline/parts/compositor/samplers/html.js +106 -0
  94. package/x/timeline/parts/compositor/samplers/html.js.map +1 -0
  95. package/x/timeline/parts/compositor/samplers/webcodecs.d.ts +2 -0
  96. package/x/timeline/parts/compositor/samplers/webcodecs.js +55 -0
  97. package/x/timeline/parts/compositor/samplers/webcodecs.js.map +1 -0
  98. package/x/timeline/parts/item.d.ts +34 -8
  99. package/x/timeline/parts/item.js +6 -3
  100. package/x/timeline/parts/item.js.map +1 -1
  101. package/x/timeline/parts/media.d.ts +3 -0
  102. package/x/timeline/parts/media.js +17 -0
  103. package/x/timeline/parts/media.js.map +1 -1
  104. package/x/timeline/parts/waveform.js +1 -1
  105. package/x/timeline/parts/waveform.js.map +1 -1
  106. package/x/timeline/sugar/builders.d.ts +96 -0
  107. package/x/timeline/sugar/builders.js +108 -0
  108. package/x/timeline/sugar/builders.js.map +1 -0
  109. package/x/timeline/sugar/o.d.ts +21 -8
  110. package/x/timeline/sugar/o.js +63 -14
  111. package/x/timeline/sugar/o.js.map +1 -1
  112. package/x/timeline/sugar/omni-test.js +1 -1
  113. package/x/timeline/sugar/omni-test.js.map +1 -1
  114. package/x/timeline/sugar/omni.d.ts +7 -3
  115. package/x/timeline/sugar/omni.js +9 -8
  116. package/x/timeline/sugar/omni.js.map +1 -1
  117. package/x/timeline/timeline.d.ts +9 -0
  118. package/x/timeline/timeline.js +22 -0
  119. package/x/timeline/timeline.js.map +1 -0
  120. package/x/timeline/types.d.ts +24 -0
  121. package/x/timeline/types.js +2 -0
  122. package/x/timeline/types.js.map +1 -0
  123. package/x/timeline/utils/audio-stream.d.ts +6 -0
  124. package/x/timeline/utils/audio-stream.js +17 -0
  125. package/x/timeline/utils/audio-stream.js.map +1 -0
  126. package/x/timeline/utils/matrix.d.ts +8 -0
  127. package/x/timeline/utils/matrix.js +26 -0
  128. package/x/timeline/utils/matrix.js.map +1 -0
  129. package/x/timeline/utils/video-cursor.d.ts +10 -0
  130. package/x/timeline/utils/video-cursor.js +36 -0
  131. package/x/timeline/utils/video-cursor.js.map +1 -0
  132. package/x/tools/speech-recognition/whisper/parts/worker.bundle.min.js +6 -6
  133. package/x/tools/speech-recognition/whisper/parts/worker.bundle.min.js.map +4 -4
@@ -0,0 +1,29 @@
1
+ import {GLTransition} from "./types.js"
2
+
3
+ export const uniforms = {
4
+ custom: (transition: GLTransition) => Object.fromEntries(
5
+ Object.entries(transition.defaultParams).map(([name, value]) => [
6
+ name,
7
+ {
8
+ value,
9
+ type: getUniformType(transition.paramsTypes[name])
10
+ }
11
+ ])
12
+ ),
13
+ basics: {
14
+ _fromR: {value: 1, type: "f32"},
15
+ _toR: {value: 1, type: "f32"},
16
+ ratio: {value: 1, type: "f32"},
17
+ progress: {value: 0, type: "f32"},
18
+ customUniform: {value: 0, type: "f32"},
19
+ }
20
+ }
21
+
22
+ const getUniformType = (type: string) => {
23
+ if(type === "f32" || type === "i32") {
24
+ return type
25
+ } else if(type === "float") {
26
+ return "f32"
27
+ }
28
+ else return `${type}<f32>`
29
+ }
@@ -0,0 +1,31 @@
1
+ export const vertex = `
2
+ in vec2 aPosition;
3
+ varying vec2 _uv; // gl-transition
4
+ uniform mat3 projectionMatrix;
5
+ uniform vec4 uInputSize;
6
+ uniform vec4 uOutputFrame;
7
+ out vec2 vTextureCoord;
8
+ uniform vec4 uOutputTexture;
9
+
10
+ vec4 filterVertexPosition( void )
11
+ {
12
+ vec2 position = aPosition * uOutputFrame.zw + uOutputFrame.xy;
13
+
14
+ position.x = position.x * (2.0 / uOutputTexture.x) - 1.0;
15
+ position.y = position.y * (2.0*uOutputTexture.z / uOutputTexture.y) - uOutputTexture.z;
16
+
17
+ return vec4(position, 0.0, 1.0);
18
+ }
19
+
20
+ vec2 filterTextureCoord( void )
21
+ {
22
+ return aPosition * (uOutputFrame.zw * uInputSize.zw);
23
+ }
24
+
25
+ void main(void)
26
+ {
27
+ gl_Position = filterVertexPosition();
28
+ vTextureCoord = filterTextureCoord();
29
+ _uv = vec2(0.5, 0.5) * (aPosition +vec2(1.0, 1.0)); // gl-transition
30
+ }
31
+ `
@@ -0,0 +1,60 @@
1
+ //@ts-ignore
2
+ import transitions from "gl-transitions"
3
+ import {Filter, GlProgram, Sprite, Texture, ImageSource} from "pixi.js"
4
+
5
+ import {vertex} from "./parts/vertex.js"
6
+ import {uniforms} from "./parts/uniforms.js"
7
+ import {fragment} from "./parts/fragment.js"
8
+ import {GLTransition, TransitionOptions, TransitionRendererOptions} from "./parts/types.js"
9
+
10
+ export function makeTransition({name, renderer}: TransitionOptions) {
11
+ const transition = transitions.find((t: GLTransition) => t.name === name) as GLTransition
12
+ const transitionSprite = new Sprite()
13
+ const transitionTexture = new Texture()
14
+ const sourceFrom = new ImageSource({})
15
+ const sourceTo = new ImageSource({})
16
+
17
+ const filter = new Filter({
18
+ glProgram: new GlProgram({
19
+ vertex,
20
+ fragment: fragment(transition.glsl),
21
+ }),
22
+ resources: {
23
+ from: sourceFrom,
24
+ to: sourceTo,
25
+ uniforms: {
26
+ ...uniforms.basics,
27
+ ...uniforms.custom(transition)
28
+ }
29
+ }
30
+ })
31
+
32
+ transitionSprite.filters = [filter]
33
+
34
+ return {
35
+ render({width, height, from, to, progress}: TransitionRendererOptions) {
36
+ if(transitionSprite.width !== width || transitionSprite.height !== height) {
37
+ transitionSprite.setSize({width, height})
38
+ transitionTexture.source.resize(width, height)
39
+ }
40
+
41
+ sourceFrom.resource = from
42
+ sourceTo.resource = to
43
+ sourceFrom.update()
44
+ sourceTo.update()
45
+
46
+ filter.resources.uniforms.uniforms.progress = progress
47
+
48
+ renderer.render({
49
+ container: transitionSprite,
50
+ target: transitionTexture,
51
+ clear: false,
52
+ width,
53
+ height
54
+ })
55
+
56
+ return transitionTexture
57
+ }
58
+ }
59
+ }
60
+
package/s/index.html.ts CHANGED
@@ -30,7 +30,7 @@ export default ssg.page(import.meta.url, async orb => ({
30
30
  <section>
31
31
  <h1>Omnitool <small>v${orb.packageVersion()}</small></h1>
32
32
  <button class=fetch>fetch</button>
33
- <button class="import">import</button>
33
+ <input type="file" class="file-input">
34
34
  <div class=results></div>
35
35
  <div class=filmstrip-demo>
36
36
  <label for="viewable-range">viewable range:</label>
@@ -47,6 +47,11 @@ export default ssg.page(import.meta.url, async orb => ({
47
47
  <label for="width">width:</label>
48
48
  <input class="width" id="width" name="width" type="range" min="100" max="1000000" value="1000" />
49
49
  </div>
50
+ <div class=player>
51
+ <input class="seek" type="number" min="0">
52
+ <button class=play>play</button>
53
+ <button class=stop>stop</button>
54
+ </div>
50
55
  </section>
51
56
  `,
52
57
  }))
@@ -11,7 +11,7 @@ export type TimelineFile = {
11
11
  info: "https://omniclip.app/"
12
12
  format: "timeline"
13
13
  version: number
14
- root: Id
14
+ rootId: Id
15
15
  items: Item.Any[]
16
16
  }
17
17
 
@@ -0,0 +1,77 @@
1
+ import {TimelineFile} from "../basics.js"
2
+ import {context} from "../../../context.js"
3
+ import {fixedStep} from "./parts/schedulers.js"
4
+ import {makeWebCodecsSampler} from "./samplers/webcodecs.js"
5
+ import {DecoderSource} from "../../../driver/fns/schematic.js"
6
+ import {buildWebCodecsNodeTree} from "./parts/webcodecs-tree.js"
7
+
8
+ export class Export {
9
+ #sampler
10
+ constructor(
11
+ private framerate = 30,
12
+ private resolveMedia: (hash: string) => DecoderSource = _hash => "/assets/temp/gl.mp4"
13
+ ) {
14
+ this.#sampler = makeWebCodecsSampler(this.resolveMedia)
15
+ }
16
+
17
+ async #build(timeline: TimelineFile) {
18
+ const rootItem = new Map(timeline.items.map(i => [i.id, i])).get(timeline.rootId)!
19
+ const items = new Map(timeline.items.map(i => [i.id, i]))
20
+ return await buildWebCodecsNodeTree(rootItem, items, this.#sampler)
21
+ }
22
+
23
+ async render(timeline: TimelineFile) {
24
+ const root = await this.#build(timeline)
25
+
26
+ const driver = await context.driver
27
+ const videoStream = new TransformStream<VideoFrame, VideoFrame>()
28
+ const audioStream = new TransformStream<AudioData, AudioData>()
29
+
30
+ const encodePromise = driver.encode({
31
+ video: videoStream.readable,
32
+ audio: audioStream.readable,
33
+ config: {
34
+ audio: {codec: "opus", bitrate: 128000},
35
+ video: {codec: "vp9", bitrate: 1000000},
36
+ },
37
+ })
38
+
39
+ const videoWriter = videoStream.writable.getWriter()
40
+ const audioWriter = audioStream.writable.getWriter()
41
+
42
+ const audioPromise = (async () => {
43
+ if (root.audio) {
44
+ for await (const chunk of root.audio.getStream()) {
45
+ await audioWriter.write(chunk)
46
+ }
47
+ }
48
+ await audioWriter.close()
49
+ })()
50
+
51
+ const videoPromise = (async () => {
52
+ let i = 0
53
+ const dt = 1 / this.framerate
54
+
55
+ await fixedStep(
56
+ {fps: this.framerate, duration: root.duration ?? 0},
57
+ async t => {
58
+ const layers = await root.visuals?.sampleAt(t) ?? []
59
+ const composed = await driver.composite(layers)
60
+ const vf = new VideoFrame(composed, {
61
+ timestamp: Math.round(i * dt * 1_000_000),
62
+ duration: Math.round(dt * 1_000_000),
63
+ })
64
+ await videoWriter.write(vf)
65
+ composed.close()
66
+ i++
67
+ }
68
+ )
69
+ await videoWriter.close()
70
+ })()
71
+
72
+ await audioPromise
73
+ await videoPromise
74
+ await encodePromise
75
+ // this.#sampler.dispose()
76
+ }
77
+ }
@@ -0,0 +1,37 @@
1
+ import {Item} from "../../item.js"
2
+ import {AudioPlaybackComponent, HTMLSampler, Node, TreeBuilder} from "./tree-builder.js"
3
+
4
+ class HTMLNodeBuilder extends TreeBuilder<AudioPlaybackComponent> {
5
+ constructor(protected items: Map<number, Item.Any>, protected sampler: HTMLSampler) {
6
+ super(items, sampler)
7
+ }
8
+
9
+ composeAudio_Stack(children: Node<AudioPlaybackComponent>[]) {
10
+ return {
11
+ onTimeUpdate: (time: number) => {
12
+ for (const child of children) {
13
+ if (child.audio) child.audio.onTimeUpdate(time)
14
+ }
15
+ }
16
+ }
17
+ }
18
+ composeAudio_Sequence(children: Node<AudioPlaybackComponent>[]) {
19
+ return {
20
+ onTimeUpdate: (time: number) => {
21
+ let localTime = time
22
+ for (const child of children) {
23
+ if (localTime < child.duration) {
24
+ if (child.audio) child.audio.onTimeUpdate(localTime)
25
+ break
26
+ }
27
+ localTime -= child.duration
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ export function buildHTMLNodeTree(root: Item.Any, items: Map<number, Item.Any>, sampler: HTMLSampler) {
35
+ const builder = new HTMLNodeBuilder(items, sampler)
36
+ return builder.build(root)
37
+ }
@@ -0,0 +1,85 @@
1
+ export type RealtimeController = {
2
+ play(): void
3
+ pause(): void
4
+ seek(t: number): void
5
+ dispose(): void
6
+ setFPS(v: number): void
7
+ isPlaying(): boolean
8
+ }
9
+
10
+ export const realtime = (onTick: (t: number) => void): RealtimeController => {
11
+ let playing = false
12
+ let rafId: number | null = null
13
+ let fps = 60
14
+
15
+ let frameDuration = 1000 / fps
16
+ let currentTimeS = 0
17
+ let lastTime = 0
18
+ let accumulator = 0
19
+
20
+ const tick = (now: number) => {
21
+ if (!playing) return
22
+
23
+ const deltaTime = now - lastTime
24
+ lastTime = now
25
+
26
+ accumulator += deltaTime
27
+
28
+ while (accumulator >= frameDuration) {
29
+ onTick(currentTimeS)
30
+ currentTimeS += frameDuration / 1000
31
+ accumulator -= frameDuration
32
+ }
33
+
34
+ rafId = requestAnimationFrame(tick)
35
+ }
36
+
37
+ return {
38
+ play() {
39
+ if (playing) return
40
+ playing = true
41
+ lastTime = performance.now()
42
+ rafId = requestAnimationFrame(tick)
43
+ },
44
+ pause() {
45
+ if (!playing) return
46
+ playing = false
47
+ if (rafId !== null) cancelAnimationFrame(rafId)
48
+ rafId = null
49
+ },
50
+ seek(t) {
51
+ currentTimeS = Math.max(0, t)
52
+ accumulator = 0
53
+ },
54
+ dispose() {
55
+ this.pause()
56
+ },
57
+ isPlaying() {
58
+ return playing
59
+ },
60
+ setFPS(v) {
61
+ fps = v
62
+ frameDuration = 1000 / fps
63
+ }
64
+ }
65
+ }
66
+
67
+ export type FixedStepOptions = {
68
+ fps: number
69
+ duration: number
70
+ abort?: AbortSignal
71
+ }
72
+
73
+ export const fixedStep = async (
74
+ opts: FixedStepOptions,
75
+ onFrame: (t: number, index: number) => Promise<void> | void
76
+ ) => {
77
+ const dt = 1 / opts.fps
78
+ const total = Math.ceil(opts.duration * opts.fps)
79
+
80
+ for (let i = 0; i < total; i++) {
81
+ if (opts.abort?.aborted) break
82
+ const t = i * dt
83
+ await onFrame(t, i)
84
+ }
85
+ }
@@ -0,0 +1,184 @@
1
+ import {Item, Kind} from "../../item.js"
2
+ import {ImageLayer, Layer} from "../../../../driver/fns/schematic.js"
3
+ import {I6, Mat6, mul6, transformToMat6} from "../../../utils/matrix.js"
4
+
5
+ export type AudioStreamComponent = {
6
+ getStream: () => AsyncGenerator<AudioData>
7
+ }
8
+ export type AudioPlaybackComponent = {
9
+ onTimeUpdate: (time: number) => void
10
+ }
11
+ export type VisualComponent = {
12
+ sampleAt: (time: number) => Promise<Layer[]>
13
+ }
14
+
15
+ export type Node<T> = {
16
+ duration: number
17
+ visuals?: VisualComponent
18
+ audio?: T
19
+ }
20
+
21
+ interface Sampler<T> {
22
+ video(item: Item.Video, parentMatrix: Mat6): Promise<Node<T>>
23
+ audio(item: Item.Audio): Promise<Node<T>>
24
+ dispose(): Promise<void>
25
+ }
26
+
27
+ const requireItem = (items: Map<number, Item.Any>, id: number) => items.get(id)!
28
+ export const getWorldMat6 = (
29
+ items: Map<number, Item.Any>,
30
+ item: Item.Text | Item.Sequence | Item.Stack | Item.Video,
31
+ parent?: Mat6
32
+ ): Mat6 => {
33
+ let world = parent ?? I6
34
+ if (item.spatialId) {
35
+ const spatial = requireItem(items, item.spatialId) as Item.Spatial
36
+ const local = transformToMat6(spatial.transform)
37
+ world = mul6(local, world)
38
+ }
39
+ return world
40
+ }
41
+
42
+ export type WebcodecsSampler = Sampler<AudioStreamComponent>
43
+ export interface HTMLSampler extends Sampler<AudioPlaybackComponent> {
44
+ setPaused(v: boolean): void
45
+ }
46
+
47
+ export abstract class TreeBuilder<T> {
48
+ constructor(protected items: Map<number, Item.Any>, protected sampler: Sampler<T>) {}
49
+
50
+ async build(root: Item.Any, parentMatrix?: Mat6): Promise<Node<T>> {
51
+ switch (root.kind) {
52
+ case Kind.Video: return this.sampler.video(root, getWorldMat6(this.items, root, parentMatrix))
53
+ case Kind.Audio: return this.sampler.audio(root)
54
+ case Kind.Text: {
55
+ const matrix = getWorldMat6(this.items, root, parentMatrix)
56
+ return {
57
+ duration: Infinity,
58
+ visuals: {
59
+ sampleAt: async () => [{kind: "text", content: root.content, color: "white", fontSize: 48, matrix}]
60
+ }
61
+ }
62
+ }
63
+ case Kind.Gap: return {
64
+ duration: root.duration,
65
+ visuals: {
66
+ sampleAt: async () => []
67
+ }
68
+ }
69
+ case Kind.Stack: {
70
+ const matrix = getWorldMat6(this.items, root, parentMatrix)
71
+ const children = await Promise.all(root.childrenIds.map(id => this.build(requireItem(this.items, id), matrix)))
72
+ return this.#composeStack(children)
73
+ }
74
+ case Kind.Sequence: {
75
+ const matrix = getWorldMat6(this.items, root, parentMatrix)
76
+ return this.#composeSequence(root, matrix)
77
+ }
78
+ default: return {duration: 0}
79
+ }
80
+ }
81
+
82
+ abstract composeAudio_Stack(children: Node<T>[]): T | undefined
83
+ abstract composeAudio_Sequence(children: Node<T>[]): T | undefined
84
+
85
+ // Visual composition is the same for both builders, so it lives here.
86
+ #composeVisuals_Stack(children: Node<T>[]): VisualComponent {
87
+ return {
88
+ sampleAt: async (time) => {
89
+ const layers = await Promise.all(children.map(c => c.visuals ? c.visuals.sampleAt(time) : Promise.resolve([])))
90
+ return layers.flat()
91
+ }
92
+ }
93
+ }
94
+
95
+ #composeVisuals_Sequence(children: Node<T>[]): VisualComponent {
96
+ return {
97
+ sampleAt: async (time) => {
98
+ let localTime = time
99
+ for (const child of children) {
100
+ if (localTime < child.duration) return child.visuals ? child.visuals.sampleAt(localTime) : []
101
+ localTime -= child.duration
102
+ }
103
+ return []
104
+ }
105
+ }
106
+ }
107
+
108
+ #composeStack(children: Node<T>[]): Node<T> {
109
+ const duration = Math.max(0, ...children.map(k => (Number.isFinite(k.duration) ? k.duration : 0)))
110
+ return {
111
+ duration,
112
+ visuals: this.#composeVisuals_Stack(children),
113
+ audio: this.composeAudio_Stack(children),
114
+ }
115
+ }
116
+
117
+ async #composeSequence(sequence: Item.Sequence, parentMatrix?: Mat6): Promise<Node<T>> {
118
+ const childItems = sequence.childrenIds.map(id => requireItem(this.items, id))
119
+ const children = await this.#processChildren(childItems, parentMatrix)
120
+ const duration = children.reduce((a, k) => a + k.duration, 0)
121
+ return {
122
+ duration,
123
+ visuals: this.#composeVisuals_Sequence(children),
124
+ audio: this.composeAudio_Sequence(children),
125
+ }
126
+ }
127
+
128
+ async #processChildren(childItems: Item.Any[], parentMatrix?: Mat6): Promise<Node<T>[]> {
129
+ const processedNodes: Node<T>[] = []
130
+ for (let i = 0; i < childItems.length; i++) {
131
+ const item = childItems[i]
132
+
133
+ if (item.kind !== Kind.Transition) {
134
+ processedNodes.push(await this.build(item, parentMatrix))
135
+ continue
136
+ }
137
+
138
+ const outgoingNode = processedNodes.pop()
139
+ const incomingItem = childItems[i + 1]
140
+
141
+ if (!outgoingNode || !incomingItem || incomingItem.kind === Kind.Transition) {
142
+ if (outgoingNode) processedNodes.push(outgoingNode)
143
+ continue
144
+ }
145
+
146
+ const incomingNode = await this.build(incomingItem, parentMatrix)
147
+ const transitionNode = await this.#createTransitionNode(item, outgoingNode, incomingNode)
148
+ processedNodes.push(transitionNode)
149
+ i++
150
+ }
151
+ return processedNodes
152
+ }
153
+
154
+ async #createTransitionNode(transitionItem: Item.Transition, outgoingNode: Node<T>, incomingNode: Node<T>): Promise<Node<T>> {
155
+ const overlap = Math.max(0, Math.min(transitionItem.duration, outgoingNode.duration, incomingNode.duration))
156
+ const start = Math.max(0, outgoingNode.duration - overlap)
157
+ const combinedDuration = outgoingNode.duration + incomingNode.duration - overlap
158
+ return {
159
+ duration: combinedDuration,
160
+ visuals: {
161
+ sampleAt: async (t) => {
162
+ if (!outgoingNode.visuals || !incomingNode.visuals) return []
163
+ if (t < start) return outgoingNode.visuals.sampleAt(t)
164
+ if (t < outgoingNode.duration) {
165
+ const localTime = t - start
166
+ const progress = overlap > 0 ? (localTime / overlap) : 1
167
+ const from = await outgoingNode.visuals.sampleAt(t) as ImageLayer[]
168
+ const to = await incomingNode.visuals.sampleAt(localTime) as ImageLayer[]
169
+ if(!from[0]?.frame || !to[0]?.frame) return []
170
+ return [{
171
+ kind: "transition",
172
+ name: "circle",
173
+ progress,
174
+ from: from[0].frame,
175
+ to: to[0].frame,
176
+ }]
177
+ }
178
+ return incomingNode.visuals.sampleAt(t - outgoingNode.duration + overlap)
179
+ }
180
+ },
181
+ audio: this.composeAudio_Sequence([outgoingNode, incomingNode])
182
+ }
183
+ }
184
+ }
@@ -0,0 +1,30 @@
1
+ import {Item} from "../../item.js"
2
+ import {AudioStreamComponent, Node, WebcodecsSampler, TreeBuilder} from "./tree-builder.js"
3
+
4
+ class WebCodecsNodeBuilder extends TreeBuilder<AudioStreamComponent> {
5
+ composeAudio_Stack(children: Node<AudioStreamComponent>[]) {
6
+ return {
7
+ getStream: async function*() {
8
+ for (const child of children) {
9
+ if (child.audio)
10
+ yield* child.audio.getStream()
11
+ }
12
+ }
13
+ }
14
+ }
15
+ composeAudio_Sequence(children: Node<AudioStreamComponent>[]) {
16
+ return {
17
+ getStream: async function*() {
18
+ for (const child of children) {
19
+ if (child.audio)
20
+ yield* child.audio.getStream()
21
+ }
22
+ }
23
+ }
24
+ }
25
+ }
26
+
27
+ export function buildWebCodecsNodeTree(root: Item.Any, items: Map<number, Item.Any>, sampler: WebcodecsSampler) {
28
+ const builder = new WebCodecsNodeBuilder(items, sampler)
29
+ return builder.build(root)
30
+ }
@@ -0,0 +1,81 @@
1
+ import {TimelineFile} from "../basics.js"
2
+ import {context} from "../../../context.js"
3
+ import {realtime} from "./parts/schedulers.js"
4
+ import {makeHtmlSampler} from "./samplers/html.js"
5
+ import {buildHTMLNodeTree} from "./parts/html-tree.js"
6
+ import {DecoderSource} from "../../../driver/fns/schematic.js"
7
+ import {AudioPlaybackComponent, HTMLSampler, Node} from "./parts/tree-builder.js"
8
+
9
+ type ResolveMedia = (hash: string) => DecoderSource
10
+
11
+ export class VideoPlayer {
12
+ #controller = realtime(t => this.#tick(t))
13
+
14
+ constructor(
15
+ public canvas: HTMLCanvasElement,
16
+ private root: Node<AudioPlaybackComponent>,
17
+ private sampler: HTMLSampler,
18
+ private resolveMedia: ResolveMedia = _hash => "/assets/temp/gl.mp4"
19
+ ) {
20
+ this.#controller.setFPS(30)
21
+ }
22
+
23
+ get context() {
24
+ return this.canvas.getContext("2d")!
25
+ }
26
+
27
+ static async create(timeline: TimelineFile) {
28
+ const rootItem = new Map(timeline.items.map(i => [i.id, i])).get(timeline.rootId)!
29
+ const items = new Map(timeline.items.map(i => [i.id, i]))
30
+ const sampler = makeHtmlSampler(() => "/assets/temp/gl.mp4")
31
+ const root = await buildHTMLNodeTree(rootItem, items, sampler)
32
+ const canvas = document.createElement("canvas")
33
+ canvas.width = 1920
34
+ canvas.height = 1080
35
+ return new this(canvas, root, sampler)
36
+ }
37
+
38
+ async #tick(t: number) {
39
+ const driver = await context.driver
40
+ const dur = this.root.duration
41
+ const tt = t > dur ? dur : t
42
+ this.root.audio?.onTimeUpdate(tt)
43
+ for (const layer of await this.root.visuals?.sampleAt(tt) ?? []) {
44
+ const frame = await driver.composite(layer)
45
+ this.context.drawImage(frame, 0, 0)
46
+ frame.close()
47
+ }
48
+ if (t >= dur) this.pause()
49
+ }
50
+
51
+ async play() {
52
+ if (!this.#controller.isPlaying()) {
53
+ this.sampler.setPaused!(false)
54
+ this.#controller.play()
55
+ }
56
+ }
57
+
58
+ pause() {
59
+ if(this.#controller.isPlaying()) {
60
+ this.#controller.pause()
61
+ this.sampler.setPaused!(true)
62
+ }
63
+ }
64
+
65
+ async seek(time: number) {
66
+ const driver = await context.driver
67
+ this.pause()
68
+ this.#controller.seek(time)
69
+ this.root.audio?.onTimeUpdate(time)
70
+ for (const draw of await this.root.visuals?.sampleAt(time) ?? []) {
71
+ const frame = await driver.composite(draw)
72
+ this.context.drawImage(frame, 0, 0)
73
+ frame.close()
74
+ }
75
+ }
76
+
77
+ setFPS(value: number) {
78
+ this.#controller.setFPS(value)
79
+ }
80
+ }
81
+