@omnimedia/omnitool 1.1.0-3 → 1.1.0-31

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 (189) hide show
  1. package/package.json +15 -10
  2. package/s/context.ts +0 -7
  3. package/s/demo/demo.bundle.ts +39 -5
  4. package/s/demo/demo.css +5 -0
  5. package/s/demo/routines/filmstrip-test.ts +2 -2
  6. package/s/demo/routines/transcode-test.ts +8 -4
  7. package/s/demo/routines/transcriber-test.ts +34 -0
  8. package/s/demo/routines/transitions-test.ts +43 -0
  9. package/s/demo/routines/waveform-test.ts +3 -2
  10. package/s/driver/driver.ts +19 -11
  11. package/s/driver/fns/host.ts +7 -6
  12. package/s/driver/fns/schematic.ts +47 -24
  13. package/s/driver/fns/work.ts +165 -156
  14. package/s/driver/utils/load-decoder-source.ts +3 -4
  15. package/s/features/speech/transcribe/default-spec.ts +11 -0
  16. package/s/features/speech/transcribe/parts/load-pipe.ts +19 -0
  17. package/s/features/speech/transcribe/parts/prep-audio.ts +23 -0
  18. package/s/features/speech/transcribe/parts/transcribe.ts +70 -0
  19. package/s/features/speech/transcribe/transcriber.ts +46 -0
  20. package/s/features/speech/transcribe/types.ts +82 -0
  21. package/s/features/speech/transcribe/worker.bundle.ts +40 -0
  22. package/s/features/transition/parts/fragment.ts +24 -0
  23. package/s/features/transition/parts/types.ts +94 -0
  24. package/s/features/transition/parts/uniforms.ts +29 -0
  25. package/s/features/transition/parts/vertex.ts +31 -0
  26. package/s/features/transition/transition.ts +60 -0
  27. package/s/index.html.ts +6 -1
  28. package/s/timeline/index.ts +1 -0
  29. package/s/timeline/parts/basics.ts +1 -1
  30. package/s/timeline/parts/compositor/export.ts +77 -0
  31. package/s/timeline/parts/compositor/parts/html-tree.ts +37 -0
  32. package/s/timeline/parts/compositor/parts/schedulers.ts +94 -0
  33. package/s/timeline/parts/compositor/parts/tree-builder.ts +196 -0
  34. package/s/timeline/parts/compositor/parts/webcodecs-tree.ts +30 -0
  35. package/s/timeline/parts/compositor/playback.ts +94 -0
  36. package/s/timeline/parts/compositor/samplers/html.ts +115 -0
  37. package/s/timeline/parts/compositor/samplers/webcodecs.ts +61 -0
  38. package/s/timeline/parts/item.ts +48 -6
  39. package/s/timeline/parts/media.ts +21 -0
  40. package/s/timeline/parts/waveform.ts +3 -4
  41. package/s/timeline/sugar/builders.ts +102 -0
  42. package/s/timeline/sugar/o.ts +163 -38
  43. package/s/timeline/sugar/omni-test.ts +5 -3
  44. package/s/timeline/sugar/omni.ts +26 -11
  45. package/s/timeline/types.ts +29 -0
  46. package/s/timeline/utils/audio-stream.ts +15 -0
  47. package/s/timeline/utils/checksum.ts +2 -1
  48. package/s/timeline/utils/matrix.ts +33 -0
  49. package/s/timeline/utils/video-cursor.ts +40 -0
  50. package/x/context.d.ts +1 -4
  51. package/x/context.js +1 -5
  52. package/x/context.js.map +1 -1
  53. package/x/demo/demo.bundle.js +26 -5
  54. package/x/demo/demo.bundle.js.map +1 -1
  55. package/x/demo/demo.bundle.min.js +606 -36
  56. package/x/demo/demo.bundle.min.js.map +4 -4
  57. package/x/demo/demo.css +5 -0
  58. package/x/demo/routines/filmstrip-test.d.ts +1 -1
  59. package/x/demo/routines/filmstrip-test.js +2 -2
  60. package/x/demo/routines/filmstrip-test.js.map +1 -1
  61. package/x/demo/routines/transcode-test.js +8 -4
  62. package/x/demo/routines/transcode-test.js.map +1 -1
  63. package/x/demo/routines/transcriber-test.d.ts +4 -0
  64. package/x/demo/routines/transcriber-test.js +33 -0
  65. package/x/demo/routines/transcriber-test.js.map +1 -0
  66. package/x/demo/routines/transitions-test.d.ts +5 -0
  67. package/x/demo/routines/transitions-test.js +35 -0
  68. package/x/demo/routines/transitions-test.js.map +1 -0
  69. package/x/demo/routines/waveform-test.d.ts +2 -1
  70. package/x/demo/routines/waveform-test.js +2 -2
  71. package/x/demo/routines/waveform-test.js.map +1 -1
  72. package/x/driver/driver.d.ts +4 -6
  73. package/x/driver/driver.js +17 -10
  74. package/x/driver/driver.js.map +1 -1
  75. package/x/driver/driver.worker.bundle.min.js +2537 -148
  76. package/x/driver/driver.worker.bundle.min.js.map +4 -4
  77. package/x/driver/fns/host.d.ts +9 -2
  78. package/x/driver/fns/host.js +3 -3
  79. package/x/driver/fns/host.js.map +1 -1
  80. package/x/driver/fns/schematic.d.ts +41 -23
  81. package/x/driver/fns/work.d.ts +11 -4
  82. package/x/driver/fns/work.js +113 -107
  83. package/x/driver/fns/work.js.map +1 -1
  84. package/x/driver/utils/load-decoder-source.d.ts +2 -1
  85. package/x/driver/utils/load-decoder-source.js +2 -3
  86. package/x/driver/utils/load-decoder-source.js.map +1 -1
  87. package/x/features/speech/transcribe/default-spec.d.ts +2 -0
  88. package/x/features/speech/transcribe/default-spec.js +8 -0
  89. package/x/features/speech/transcribe/default-spec.js.map +1 -0
  90. package/x/features/speech/transcribe/parts/load-pipe.d.ts +2 -0
  91. package/x/features/speech/transcribe/parts/load-pipe.js +13 -0
  92. package/x/features/speech/transcribe/parts/load-pipe.js.map +1 -0
  93. package/x/features/speech/transcribe/parts/prep-audio.d.ts +5 -0
  94. package/x/features/speech/transcribe/parts/prep-audio.js +21 -0
  95. package/x/features/speech/transcribe/parts/prep-audio.js.map +1 -0
  96. package/x/features/speech/transcribe/parts/transcribe.d.ts +5 -0
  97. package/x/features/speech/transcribe/parts/transcribe.js +56 -0
  98. package/x/features/speech/transcribe/parts/transcribe.js.map +1 -0
  99. package/x/features/speech/transcribe/transcriber.d.ts +5 -0
  100. package/x/features/speech/transcribe/transcriber.js +33 -0
  101. package/x/features/speech/transcribe/transcriber.js.map +1 -0
  102. package/x/features/speech/transcribe/types.d.ts +66 -0
  103. package/x/features/speech/transcribe/types.js +2 -0
  104. package/x/features/speech/transcribe/types.js.map +1 -0
  105. package/x/features/speech/transcribe/worker.bundle.d.ts +1 -0
  106. package/x/features/speech/transcribe/worker.bundle.js +33 -0
  107. package/x/features/speech/transcribe/worker.bundle.js.map +1 -0
  108. package/x/features/speech/transcribe/worker.bundle.min.js +2916 -0
  109. package/x/features/speech/transcribe/worker.bundle.min.js.map +7 -0
  110. package/x/features/transition/parts/fragment.d.ts +1 -0
  111. package/x/features/transition/parts/fragment.js +25 -0
  112. package/x/features/transition/parts/fragment.js.map +1 -0
  113. package/x/features/transition/parts/types.d.ts +23 -0
  114. package/x/features/transition/parts/types.js +2 -0
  115. package/x/features/transition/parts/types.js.map +1 -0
  116. package/x/features/transition/parts/uniforms.d.ts +31 -0
  117. package/x/features/transition/parts/uniforms.js +27 -0
  118. package/x/features/transition/parts/uniforms.js.map +1 -0
  119. package/x/features/transition/parts/vertex.d.ts +1 -0
  120. package/x/features/transition/parts/vertex.js +32 -0
  121. package/x/features/transition/parts/vertex.js.map +1 -0
  122. package/x/features/transition/transition.d.ts +5 -0
  123. package/x/features/transition/transition.js +50 -0
  124. package/x/features/transition/transition.js.map +1 -0
  125. package/x/index.html +13 -3
  126. package/x/index.html.js +6 -1
  127. package/x/index.html.js.map +1 -1
  128. package/x/timeline/index.d.ts +1 -0
  129. package/x/timeline/index.js +1 -0
  130. package/x/timeline/index.js.map +1 -1
  131. package/x/timeline/parts/basics.d.ts +1 -1
  132. package/x/timeline/parts/compositor/export.d.ts +11 -0
  133. package/x/timeline/parts/compositor/export.js +64 -0
  134. package/x/timeline/parts/compositor/export.js.map +1 -0
  135. package/x/timeline/parts/compositor/parts/html-tree.d.ts +3 -0
  136. package/x/timeline/parts/compositor/parts/html-tree.js +40 -0
  137. package/x/timeline/parts/compositor/parts/html-tree.js.map +1 -0
  138. package/x/timeline/parts/compositor/parts/schedulers.d.ts +15 -0
  139. package/x/timeline/parts/compositor/parts/schedulers.js +69 -0
  140. package/x/timeline/parts/compositor/parts/schedulers.js.map +1 -0
  141. package/x/timeline/parts/compositor/parts/tree-builder.d.ts +37 -0
  142. package/x/timeline/parts/compositor/parts/tree-builder.js +160 -0
  143. package/x/timeline/parts/compositor/parts/tree-builder.js.map +1 -0
  144. package/x/timeline/parts/compositor/parts/webcodecs-tree.d.ts +3 -0
  145. package/x/timeline/parts/compositor/parts/webcodecs-tree.js +28 -0
  146. package/x/timeline/parts/compositor/parts/webcodecs-tree.js.map +1 -0
  147. package/x/timeline/parts/compositor/playback.d.ts +26 -0
  148. package/x/timeline/parts/compositor/playback.js +79 -0
  149. package/x/timeline/parts/compositor/playback.js.map +1 -0
  150. package/x/timeline/parts/compositor/samplers/html.d.ts +3 -0
  151. package/x/timeline/parts/compositor/samplers/html.js +106 -0
  152. package/x/timeline/parts/compositor/samplers/html.js.map +1 -0
  153. package/x/timeline/parts/compositor/samplers/webcodecs.d.ts +3 -0
  154. package/x/timeline/parts/compositor/samplers/webcodecs.js +52 -0
  155. package/x/timeline/parts/compositor/samplers/webcodecs.js.map +1 -0
  156. package/x/timeline/parts/item.d.ts +42 -8
  157. package/x/timeline/parts/item.js +7 -3
  158. package/x/timeline/parts/item.js.map +1 -1
  159. package/x/timeline/parts/media.d.ts +3 -0
  160. package/x/timeline/parts/media.js +17 -0
  161. package/x/timeline/parts/media.js.map +1 -1
  162. package/x/timeline/parts/waveform.d.ts +2 -1
  163. package/x/timeline/parts/waveform.js +2 -4
  164. package/x/timeline/parts/waveform.js.map +1 -1
  165. package/x/timeline/sugar/builders.d.ts +1 -0
  166. package/x/timeline/sugar/builders.js +104 -0
  167. package/x/timeline/sugar/builders.js.map +1 -0
  168. package/x/timeline/sugar/o.d.ts +27 -5
  169. package/x/timeline/sugar/o.js +137 -38
  170. package/x/timeline/sugar/o.js.map +1 -1
  171. package/x/timeline/sugar/omni-test.js +4 -2
  172. package/x/timeline/sugar/omni-test.js.map +1 -1
  173. package/x/timeline/sugar/omni.d.ts +8 -2
  174. package/x/timeline/sugar/omni.js +22 -9
  175. package/x/timeline/sugar/omni.js.map +1 -1
  176. package/x/timeline/types.d.ts +24 -0
  177. package/x/timeline/types.js +2 -0
  178. package/x/timeline/types.js.map +1 -0
  179. package/x/timeline/utils/audio-stream.d.ts +6 -0
  180. package/x/timeline/utils/audio-stream.js +17 -0
  181. package/x/timeline/utils/audio-stream.js.map +1 -0
  182. package/x/timeline/utils/checksum.js +2 -1
  183. package/x/timeline/utils/checksum.js.map +1 -1
  184. package/x/timeline/utils/matrix.d.ts +8 -0
  185. package/x/timeline/utils/matrix.js +26 -0
  186. package/x/timeline/utils/matrix.js.map +1 -0
  187. package/x/timeline/utils/video-cursor.d.ts +10 -0
  188. package/x/timeline/utils/video-cursor.js +36 -0
  189. package/x/timeline/utils/video-cursor.js.map +1 -0
@@ -0,0 +1,196 @@
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
+ const styleItem = root.styleId !== undefined
57
+ ? this.items.get(root.styleId) as Item.TextStyle
58
+ : undefined
59
+ return {
60
+ duration: root.duration,
61
+ visuals: {
62
+ sampleAt: async (t) => {
63
+ if (t < 0 || t >= root.duration)
64
+ return []
65
+ else return [{
66
+ kind: "text",
67
+ content: root.content,
68
+ style: styleItem?.style,
69
+ matrix
70
+ }]
71
+ }
72
+ }
73
+ }
74
+ }
75
+ case Kind.Gap: return {
76
+ duration: root.duration,
77
+ visuals: {
78
+ sampleAt: async () => []
79
+ }
80
+ }
81
+ case Kind.Stack: {
82
+ const matrix = getWorldMat6(this.items, root, parentMatrix)
83
+ const children = await Promise.all(root.childrenIds.map(id => this.build(requireItem(this.items, id), matrix)))
84
+ return this.#composeStack(children)
85
+ }
86
+ case Kind.Sequence: {
87
+ const matrix = getWorldMat6(this.items, root, parentMatrix)
88
+ return this.#composeSequence(root, matrix)
89
+ }
90
+ default: return {duration: 0}
91
+ }
92
+ }
93
+
94
+ abstract composeAudio_Stack(children: Node<T>[]): T | undefined
95
+ abstract composeAudio_Sequence(children: Node<T>[]): T | undefined
96
+
97
+ // Visual composition is the same for both builders, so it lives here.
98
+ #composeVisuals_Stack(children: Node<T>[]): VisualComponent {
99
+ return {
100
+ sampleAt: async (time) => {
101
+ const layers = await Promise.all(children.map(c => c.visuals ? c.visuals.sampleAt(time) : Promise.resolve([])))
102
+ return layers.flat()
103
+ }
104
+ }
105
+ }
106
+
107
+ #composeVisuals_Sequence(children: Node<T>[]): VisualComponent {
108
+ return {
109
+ sampleAt: async (time) => {
110
+ let localTime = time
111
+ for (const child of children) {
112
+ if (localTime < child.duration) return child.visuals ? child.visuals.sampleAt(localTime) : []
113
+ localTime -= child.duration
114
+ }
115
+ return []
116
+ }
117
+ }
118
+ }
119
+
120
+ #composeStack(children: Node<T>[]): Node<T> {
121
+ const duration = Math.max(0, ...children.map(k => (Number.isFinite(k.duration) ? k.duration : 0)))
122
+ return {
123
+ duration,
124
+ visuals: this.#composeVisuals_Stack(children),
125
+ audio: this.composeAudio_Stack(children),
126
+ }
127
+ }
128
+
129
+ async #composeSequence(sequence: Item.Sequence, parentMatrix?: Mat6): Promise<Node<T>> {
130
+ const childItems = sequence.childrenIds.map(id => requireItem(this.items, id))
131
+ const children = await this.#processChildren(childItems, parentMatrix)
132
+ const duration = children.reduce((a, k) => a + k.duration, 0)
133
+ return {
134
+ duration,
135
+ visuals: this.#composeVisuals_Sequence(children),
136
+ audio: this.composeAudio_Sequence(children),
137
+ }
138
+ }
139
+
140
+ async #processChildren(childItems: Item.Any[], parentMatrix?: Mat6): Promise<Node<T>[]> {
141
+ const processedNodes: Node<T>[] = []
142
+ for (let i = 0; i < childItems.length; i++) {
143
+ const item = childItems[i]
144
+
145
+ if (item.kind !== Kind.Transition) {
146
+ processedNodes.push(await this.build(item, parentMatrix))
147
+ continue
148
+ }
149
+
150
+ const outgoingNode = processedNodes.pop()
151
+ const incomingItem = childItems[i + 1]
152
+
153
+ if (!outgoingNode || !incomingItem || incomingItem.kind === Kind.Transition) {
154
+ if (outgoingNode) processedNodes.push(outgoingNode)
155
+ continue
156
+ }
157
+
158
+ const incomingNode = await this.build(incomingItem, parentMatrix)
159
+ const transitionNode = await this.#createTransitionNode(item, outgoingNode, incomingNode)
160
+ processedNodes.push(transitionNode)
161
+ i++
162
+ }
163
+ return processedNodes
164
+ }
165
+
166
+ async #createTransitionNode(transitionItem: Item.Transition, outgoingNode: Node<T>, incomingNode: Node<T>): Promise<Node<T>> {
167
+ const overlap = Math.max(0, Math.min(transitionItem.duration, outgoingNode.duration, incomingNode.duration))
168
+ const start = Math.max(0, outgoingNode.duration - overlap)
169
+ const combinedDuration = outgoingNode.duration + incomingNode.duration - overlap
170
+ return {
171
+ duration: combinedDuration,
172
+ visuals: {
173
+ sampleAt: async (t) => {
174
+ if (!outgoingNode.visuals || !incomingNode.visuals) return []
175
+ if (t < start) return outgoingNode.visuals.sampleAt(t)
176
+ if (t < outgoingNode.duration) {
177
+ const localTime = t - start
178
+ const progress = overlap > 0 ? (localTime / overlap) : 1
179
+ const from = await outgoingNode.visuals.sampleAt(t) as ImageLayer[]
180
+ const to = await incomingNode.visuals.sampleAt(localTime) as ImageLayer[]
181
+ if(!from[0]?.frame || !to[0]?.frame) return []
182
+ return [{
183
+ kind: "transition",
184
+ name: "circle",
185
+ progress,
186
+ from: from[0].frame,
187
+ to: to[0].frame,
188
+ }]
189
+ }
190
+ return incomingNode.visuals.sampleAt(t - outgoingNode.duration + overlap)
191
+ }
192
+ },
193
+ audio: this.composeAudio_Sequence([outgoingNode, incomingNode])
194
+ }
195
+ }
196
+ }
@@ -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,94 @@
1
+ import {signal} from "@e280/strata"
2
+
3
+ import {TimelineFile} from "../basics.js"
4
+ import {realtime} from "./parts/schedulers.js"
5
+ import {Driver} from "../../../driver/driver.js"
6
+ import {makeHtmlSampler} from "./samplers/html.js"
7
+ import {buildHTMLNodeTree} from "./parts/html-tree.js"
8
+ import {DecoderSource} from "../../../driver/fns/schematic.js"
9
+ import {AudioPlaybackComponent, HTMLSampler, Node} from "./parts/tree-builder.js"
10
+
11
+ type ResolveMedia = (hash: string) => DecoderSource
12
+
13
+ export class VideoPlayer {
14
+ readonly currentTime = signal(0)
15
+ #controller = realtime(
16
+ compositeTime => this.#tick(compositeTime),
17
+ currentTime => this.currentTime(currentTime)
18
+ )
19
+
20
+ constructor(
21
+ private driver: Driver,
22
+ public canvas: HTMLCanvasElement,
23
+ private root: Node<AudioPlaybackComponent>,
24
+ private sampler: HTMLSampler,
25
+ private resolveMedia: ResolveMedia = _hash => "/assets/temp/gl.mp4"
26
+ ) {
27
+ this.#controller.setFPS(30)
28
+ }
29
+
30
+ get context() {
31
+ return this.canvas.getContext("2d")!
32
+ }
33
+
34
+ static async create(driver: Driver, timeline: TimelineFile) {
35
+ const rootItem = new Map(timeline.items.map(i => [i.id, i])).get(timeline.rootId)!
36
+ const items = new Map(timeline.items.map(i => [i.id, i]))
37
+ const sampler = makeHtmlSampler(() => "/assets/temp/gl.mp4")
38
+ const root = await buildHTMLNodeTree(rootItem, items, sampler)
39
+ const canvas = document.createElement("canvas")
40
+ canvas.width = 1920
41
+ canvas.height = 1080
42
+ return new this(driver, canvas, root, sampler)
43
+ }
44
+
45
+ async #tick(ms: number) {
46
+ const duration = this.root.duration
47
+ const tt = ms > duration ? duration : ms
48
+ this.root.audio?.onTimeUpdate(tt)
49
+ const layers = await this.root.visuals?.sampleAt(tt) ?? []
50
+ const frame = await this.driver.composite(layers)
51
+ this.context.drawImage(frame, 0, 0)
52
+ frame.close()
53
+ if (ms >= duration) this.pause()
54
+ }
55
+
56
+ async play() {
57
+ if (!this.#controller.isPlaying()) {
58
+ this.sampler.setPaused!(false)
59
+ this.#controller.play()
60
+ }
61
+ }
62
+
63
+ pause() {
64
+ if(this.#controller.isPlaying()) {
65
+ this.#controller.pause()
66
+ this.sampler.setPaused!(true)
67
+ }
68
+ }
69
+
70
+ async seek(ms: number) {
71
+ this.pause()
72
+ this.#controller.seek(ms)
73
+ this.root.audio?.onTimeUpdate(ms)
74
+ const layers = await this.root.visuals?.sampleAt(ms) ?? []
75
+ const frame = await this.driver.composite(layers)
76
+ this.context.drawImage(frame, 0, 0)
77
+ frame.close()
78
+ }
79
+
80
+ setFPS(value: number) {
81
+ this.#controller.setFPS(value)
82
+ }
83
+
84
+ /**
85
+ call this whenever your timeline state changes
86
+ */
87
+ async update(timeline: TimelineFile) {
88
+ const rootItem = new Map(timeline.items.map(i => [i.id, i])).get(timeline.rootId)!
89
+ const items = new Map(timeline.items.map(i => [i.id, i]))
90
+ this.root = await buildHTMLNodeTree(rootItem, items, this.sampler)
91
+ await this.seek(this.currentTime())
92
+ }
93
+ }
94
+
@@ -0,0 +1,115 @@
1
+ import {Item} from "../../item.js"
2
+ import {HTMLSampler} from "../parts/tree-builder.js"
3
+ import {DecoderSource} from "../../../../driver/fns/schematic.js"
4
+
5
+ const toUrl = (src: DecoderSource) => (src instanceof Blob ? URL.createObjectURL(src) : String(src))
6
+
7
+ export function makeHtmlSampler(resolveMedia: (hash: string) => DecoderSource): HTMLSampler {
8
+ const videoElements = new Map<number, HTMLVideoElement>()
9
+ const audioElements = new Map<number, HTMLAudioElement>()
10
+
11
+ function getOrCreateVideoElement(clip: Item.Video) {
12
+ let video = videoElements.get(clip.id)
13
+ if (!video) {
14
+ video = document.createElement("video")
15
+ video.playsInline = true
16
+ video.muted = true
17
+ video.preload = "auto"
18
+ video.crossOrigin = "anonymous"
19
+ video.src = toUrl(resolveMedia(clip.mediaHash))
20
+ videoElements.set(clip.id, video)
21
+ }
22
+ return video
23
+ }
24
+
25
+ function getOrCreateAudioElement(clip: Item.Audio) {
26
+ let audio = audioElements.get(clip.id)
27
+ if (!audio) {
28
+ audio = document.createElement("audio")
29
+ audio.preload = "auto"
30
+ audio.crossOrigin = "anonymous"
31
+ audio.src = toUrl(resolveMedia(clip.mediaHash))
32
+ audio.volume = 0.2
33
+ audioElements.set(clip.id, audio)
34
+ }
35
+ return audio
36
+ }
37
+
38
+ let paused = true
39
+
40
+ return {
41
+ async video(item, matrix) {
42
+ const video = getOrCreateVideoElement(item)
43
+ return {
44
+ duration: item.duration,
45
+ // if paused seek otherwise play
46
+ visuals: {
47
+ sampleAt: async (ms) => {
48
+ if (ms < 0 || ms >= item.duration)
49
+ return []
50
+
51
+ if(video.paused && paused) {
52
+ await seek(video, ms / 1000)
53
+ }
54
+
55
+ if(video.paused && !paused) {
56
+ await video.play()
57
+ }
58
+
59
+ const frame = new VideoFrame(video)
60
+ return frame ? [{kind: "image", frame, matrix}] : []
61
+ }
62
+ }
63
+ }
64
+ },
65
+ async audio(item) {
66
+ const audio = getOrCreateAudioElement(item)
67
+ return {
68
+ duration: item.duration,
69
+ audio: {
70
+ onTimeUpdate: async (ms) => {
71
+ const localTime = item.start + ms
72
+ if(audio.paused && paused) {
73
+ await seek(audio, localTime / 1000)
74
+ }
75
+ if(audio.paused && !paused) {
76
+ await audio.play()
77
+ }
78
+ return []
79
+ }
80
+ }
81
+ }
82
+ },
83
+ async dispose() {
84
+ const elements = [...videoElements.values(), ...audioElements.values()]
85
+ for (const element of elements) {
86
+ element.pause()
87
+ if (element.src.startsWith("blob:"))
88
+ URL.revokeObjectURL(element.src)
89
+ element.remove()
90
+ }
91
+ videoElements.clear()
92
+ audioElements.clear()
93
+ },
94
+ async setPaused(p) {
95
+ paused = p
96
+ const elements = [...videoElements.values(), ...audioElements.values()]
97
+ for(const element of elements) {
98
+ if(p) element.pause()
99
+ }
100
+ },
101
+ }
102
+ }
103
+
104
+ function seek(media: HTMLVideoElement | HTMLAudioElement, time: number): Promise<void> {
105
+ return new Promise((resolve) => {
106
+ const onSeeked = () => {
107
+ media.removeEventListener("seeked", onSeeked)
108
+ resolve()
109
+ }
110
+ media.addEventListener("seeked", onSeeked)
111
+ if(media.fastSeek) {
112
+ media.fastSeek(time)
113
+ } else media.currentTime = time
114
+ })
115
+ }
@@ -0,0 +1,61 @@
1
+ import {Item} from "../../item.js"
2
+ import {Driver} from "../../../../driver/driver.js"
3
+ import {WebcodecsSampler} from "../parts/tree-builder.js"
4
+ import {VideoCursor} from "../../../utils/video-cursor.js"
5
+ import {AudioStream} from "../../../utils/audio-stream.js"
6
+
7
+ const toUs = (seconds: number) => Math.round(seconds * 1_000_000)
8
+
9
+ export function makeWebCodecsSampler(
10
+ driver: Driver,
11
+ resolveMedia: (hash: string) => any
12
+ ): WebcodecsSampler {
13
+ const videoCursors = new Map<number, VideoCursor>()
14
+
15
+ async function getCursorForVideo(videoItem: Item.Video): Promise<VideoCursor> {
16
+ const existing = videoCursors.get(videoItem.id)
17
+ if (existing) return existing
18
+ const source = resolveMedia(videoItem.mediaHash)
19
+ const video = driver.decodeVideo({source})
20
+ const cursor = new VideoCursor(video.getReader())
21
+ videoCursors.set(videoItem.id, cursor)
22
+ return cursor
23
+ }
24
+
25
+ return {
26
+ async video(item, matrix) {
27
+ const cursor = await getCursorForVideo(item)
28
+ const baseUs = toUs(item.start ?? 0)
29
+ return {
30
+ duration: item.duration,
31
+ visuals: {
32
+ sampleAt: async (time: number) => {
33
+ const frame = await cursor.atOrNear(baseUs + toUs(time))
34
+ return frame ? [{kind: "image", frame, matrix}] : []
35
+ }
36
+ }
37
+ }
38
+ },
39
+ async audio(item) {
40
+ return {
41
+ duration: item.duration,
42
+ audio: {
43
+ getStream: async function*() {
44
+ const source = resolveMedia(item.mediaHash)
45
+ const startUs = item.start
46
+ const endUs = (item.start + item.duration)
47
+ const audio = driver.decodeAudio({source, start: startUs, end: endUs})
48
+ const audioStream = new AudioStream(audio.getReader())
49
+ yield* audioStream.stream()
50
+ },
51
+ }
52
+ }
53
+ },
54
+ async dispose() {
55
+ const tasks = Array.from([...videoCursors.values()], c => c.cancel())
56
+ videoCursors.clear()
57
+ await Promise.all(tasks)
58
+ }
59
+ }
60
+ }
61
+
@@ -1,12 +1,18 @@
1
+ import {TextStyleOptions} from "pixi.js"
1
2
 
2
3
  import {Id, Hash} from "./basics.js"
4
+ import {Transform} from "../types.js"
3
5
 
4
6
  export enum Kind {
5
7
  Sequence,
6
8
  Stack,
7
- Clip,
9
+ Video,
10
+ Audio,
8
11
  Text,
12
+ Gap,
13
+ Spatial,
9
14
  Transition,
15
+ TextStyle
10
16
  }
11
17
 
12
18
  export enum Effect {
@@ -14,21 +20,50 @@ export enum Effect {
14
20
  }
15
21
 
16
22
  export namespace Item {
23
+ export type TextStyle = {
24
+ id: Id
25
+ kind: Kind.TextStyle
26
+ style: TextStyleOptions
27
+ }
28
+
29
+ export type Spatial = {
30
+ id: Id
31
+ kind: Kind.Spatial
32
+ transform: Transform
33
+ }
34
+
35
+ export type Gap = {
36
+ id: Id
37
+ kind: Kind.Gap
38
+ duration: number
39
+ }
40
+
17
41
  export type Sequence = {
18
42
  id: Id
19
43
  kind: Kind.Sequence
20
- children: Id[]
44
+ childrenIds: Id[]
45
+ spatialId?: Id
21
46
  }
22
47
 
23
48
  export type Stack = {
24
49
  id: Id
25
50
  kind: Kind.Stack
26
- children: Id[]
51
+ childrenIds: Id[]
52
+ spatialId?: Id
27
53
  }
28
54
 
29
- export type Clip = {
55
+ export type Video = {
30
56
  id: Id
31
- kind: Kind.Clip
57
+ kind: Kind.Video
58
+ mediaHash: Hash
59
+ start: number
60
+ duration: number
61
+ spatialId?: Id
62
+ }
63
+
64
+ export type Audio = {
65
+ id: Id
66
+ kind: Kind.Audio
32
67
  mediaHash: Hash
33
68
  start: number
34
69
  duration: number
@@ -38,6 +73,9 @@ export namespace Item {
38
73
  id: Id
39
74
  kind: Kind.Text
40
75
  content: string
76
+ duration: number
77
+ spatialId?: Id
78
+ styleId?: Id
41
79
  }
42
80
 
43
81
  export type Transition = {
@@ -50,9 +88,13 @@ export namespace Item {
50
88
  export type Any = (
51
89
  | Sequence
52
90
  | Stack
53
- | Clip
91
+ | Video
92
+ | Audio
54
93
  | Text
94
+ | Gap
55
95
  | Transition
96
+ | Spatial
97
+ | TextStyle
56
98
  )
57
99
  }
58
100
 
@@ -1,14 +1,35 @@
1
1
 
2
+ import {ALL_FORMATS, Input} from "mediabunny"
3
+
2
4
  import {Datafile} from "../utils/datafile.js"
5
+ import {DecoderSource} from "../../driver/fns/schematic.js"
6
+ import {loadDecoderSource} from "../../driver/utils/load-decoder-source.js"
3
7
 
4
8
  export class Media {
5
9
  duration = 0
10
+ hasVideo = false
11
+ hasAudio = false
12
+
6
13
  constructor(public datafile: Datafile) {}
7
14
 
8
15
  static async analyze(datafile: Datafile) {
9
16
  const media = new this(datafile)
10
17
  media.duration = 10
18
+ const {video, audio} = await this.#has("/assets/temp/gl.mp4")
19
+ media.hasAudio = audio
20
+ media.hasVideo = video
11
21
  return media
12
22
  }
23
+
24
+ static async #has(source: DecoderSource) {
25
+ const input = new Input({
26
+ formats: ALL_FORMATS,
27
+ source: await loadDecoderSource(source)
28
+ })
29
+ return {
30
+ audio: !!(await input.getPrimaryAudioTrack()),
31
+ video: !!(await input.getPrimaryVideoTrack())
32
+ }
33
+ }
13
34
  }
14
35
 
@@ -1,6 +1,6 @@
1
1
  import WaveSurfer from "wavesurfer.js"
2
2
 
3
- import {context} from "../../context.js"
3
+ import {Driver} from "../../driver/driver.js"
4
4
  import {DecoderSource} from "../../driver/fns/schematic.js"
5
5
 
6
6
  export class Waveform {
@@ -19,9 +19,8 @@ export class Waveform {
19
19
  })
20
20
  }
21
21
 
22
- static async init(source: DecoderSource, container: HTMLElement) {
23
- const driver = await context.driver
24
- const reader = driver.decode({ source }).audio.getReader()
22
+ static async init(driver: Driver, source: DecoderSource, container: HTMLElement) {
23
+ const reader = driver.decodeAudio({source}).getReader()
25
24
 
26
25
  const peaks: number[] = []
27
26
  let buffer: number[] = []