@omnimedia/omnitool 1.0.0 → 1.1.0-3

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 (177) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -2
  3. package/package.json +56 -27
  4. package/s/_archive/types.ts +107 -0
  5. package/s/context.ts +7 -0
  6. package/s/demo/demo.bundle.ts +64 -0
  7. package/s/demo/demo.css +54 -0
  8. package/s/demo/routines/filmstrip-test.ts +68 -0
  9. package/s/demo/routines/load-video.ts +7 -0
  10. package/s/demo/routines/transcode-test.ts +44 -0
  11. package/s/demo/routines/waveform-test.ts +12 -0
  12. package/s/driver/driver.test.ts +15 -0
  13. package/s/driver/driver.ts +116 -0
  14. package/s/driver/driver.worker.bundle.ts +7 -0
  15. package/s/driver/fns/host.ts +12 -0
  16. package/s/driver/fns/schematic.ts +83 -0
  17. package/s/driver/fns/work.ts +237 -0
  18. package/s/driver/parts/constants.ts +17 -0
  19. package/s/driver/parts/machina.ts +27 -0
  20. package/s/driver/utils/load-decoder-source.ts +13 -0
  21. package/s/driver/utils/sleep.ts +3 -0
  22. package/s/index.html.ts +53 -0
  23. package/s/index.ts +2 -39
  24. package/s/tests.test.ts +8 -0
  25. package/s/timeline/index.ts +14 -0
  26. package/s/timeline/parts/basics.ts +17 -0
  27. package/s/timeline/parts/filmstrip.ts +159 -0
  28. package/s/timeline/parts/item.ts +58 -0
  29. package/s/timeline/parts/media.ts +14 -0
  30. package/s/timeline/parts/resource-pool.ts +27 -0
  31. package/s/timeline/parts/resource.ts +11 -0
  32. package/s/timeline/parts/waveform.ts +62 -0
  33. package/s/timeline/sugar/o.ts +60 -0
  34. package/s/timeline/sugar/omni-test.ts +38 -0
  35. package/s/timeline/sugar/omni.ts +30 -0
  36. package/s/timeline/utils/checksum.ts +19 -0
  37. package/s/timeline/utils/datafile.ts +21 -0
  38. package/s/timeline/utils/dummy-data.ts +7 -0
  39. package/x/context.d.ts +4 -0
  40. package/x/context.js +6 -0
  41. package/x/context.js.map +1 -0
  42. package/x/demo/demo.bundle.d.ts +1 -0
  43. package/x/demo/demo.bundle.js +51 -0
  44. package/x/demo/demo.bundle.js.map +1 -0
  45. package/x/demo/demo.bundle.min.js +118 -0
  46. package/x/demo/demo.bundle.min.js.map +7 -0
  47. package/x/demo/demo.css +54 -0
  48. package/x/demo/routines/filmstrip-test.d.ts +1 -0
  49. package/x/demo/routines/filmstrip-test.js +62 -0
  50. package/x/demo/routines/filmstrip-test.js.map +1 -0
  51. package/x/demo/routines/load-video.d.ts +1 -0
  52. package/x/demo/routines/load-video.js +6 -0
  53. package/x/demo/routines/load-video.js.map +1 -0
  54. package/x/demo/routines/transcode-test.d.ts +6 -0
  55. package/x/demo/routines/transcode-test.js +38 -0
  56. package/x/demo/routines/transcode-test.js.map +1 -0
  57. package/x/demo/routines/waveform-test.d.ts +1 -0
  58. package/x/demo/routines/waveform-test.js +11 -0
  59. package/x/demo/routines/waveform-test.js.map +1 -0
  60. package/x/driver/driver.d.ts +22 -0
  61. package/x/driver/driver.js +97 -0
  62. package/x/driver/driver.js.map +1 -0
  63. package/x/driver/driver.test.d.ts +5 -0
  64. package/x/driver/driver.test.js +12 -0
  65. package/x/driver/driver.test.js.map +1 -0
  66. package/x/driver/driver.worker.bundle.d.ts +1 -0
  67. package/x/driver/driver.worker.bundle.js +4 -0
  68. package/x/driver/driver.worker.bundle.js.map +1 -0
  69. package/x/driver/driver.worker.bundle.min.js +1148 -0
  70. package/x/driver/driver.worker.bundle.min.js.map +7 -0
  71. package/x/driver/fns/host.d.ts +18 -0
  72. package/x/driver/fns/host.js +7 -0
  73. package/x/driver/fns/host.js.map +1 -0
  74. package/x/driver/fns/schematic.d.ts +66 -0
  75. package/x/driver/fns/schematic.js +2 -0
  76. package/x/driver/fns/schematic.js.map +1 -0
  77. package/x/driver/fns/work.d.ts +19 -0
  78. package/x/driver/fns/work.js +192 -0
  79. package/x/driver/fns/work.js.map +1 -0
  80. package/x/driver/parts/constants.d.ts +2 -0
  81. package/x/driver/parts/constants.js +17 -0
  82. package/x/driver/parts/constants.js.map +1 -0
  83. package/x/driver/parts/machina.d.ts +23 -0
  84. package/x/driver/parts/machina.js +14 -0
  85. package/x/driver/parts/machina.js.map +1 -0
  86. package/x/driver/utils/load-decoder-source.d.ts +2 -0
  87. package/x/driver/utils/load-decoder-source.js +12 -0
  88. package/x/driver/utils/load-decoder-source.js.map +1 -0
  89. package/x/driver/utils/sleep.d.ts +1 -0
  90. package/x/driver/utils/sleep.js +4 -0
  91. package/x/driver/utils/sleep.js.map +1 -0
  92. package/x/index.d.ts +2 -9
  93. package/x/index.html +105 -0
  94. package/x/index.html.d.ts +2 -0
  95. package/x/index.html.js +47 -0
  96. package/x/index.html.js.map +1 -0
  97. package/x/index.js +2 -29
  98. package/x/index.js.map +1 -1
  99. package/x/tests.test.d.ts +1 -0
  100. package/x/tests.test.js +6 -0
  101. package/x/tests.test.js.map +1 -0
  102. package/x/timeline/index.d.ts +10 -0
  103. package/x/timeline/index.js +11 -0
  104. package/x/timeline/index.js.map +1 -0
  105. package/x/timeline/parts/basics.d.ts +12 -0
  106. package/x/timeline/parts/basics.js +2 -0
  107. package/x/timeline/parts/basics.js.map +1 -0
  108. package/x/timeline/parts/filmstrip.d.ts +39 -0
  109. package/x/timeline/parts/filmstrip.js +117 -0
  110. package/x/timeline/parts/filmstrip.js.map +1 -0
  111. package/x/timeline/parts/item.d.ts +42 -0
  112. package/x/timeline/parts/item.js +13 -0
  113. package/x/timeline/parts/item.js.map +1 -0
  114. package/x/timeline/parts/media.d.ts +7 -0
  115. package/x/timeline/parts/media.js +13 -0
  116. package/x/timeline/parts/media.js.map +1 -0
  117. package/x/timeline/parts/resource-pool.d.ts +7 -0
  118. package/x/timeline/parts/resource-pool.js +19 -0
  119. package/x/timeline/parts/resource-pool.js.map +1 -0
  120. package/x/timeline/parts/resource.d.ts +8 -0
  121. package/x/timeline/parts/resource.js +2 -0
  122. package/x/timeline/parts/resource.js.map +1 -0
  123. package/x/timeline/parts/waveform.d.ts +8 -0
  124. package/x/timeline/parts/waveform.js +51 -0
  125. package/x/timeline/parts/waveform.js.map +1 -0
  126. package/x/timeline/sugar/o.d.ts +14 -0
  127. package/x/timeline/sugar/o.js +48 -0
  128. package/x/timeline/sugar/o.js.map +1 -0
  129. package/x/timeline/sugar/omni-test.d.ts +1 -0
  130. package/x/timeline/sugar/omni-test.js +22 -0
  131. package/x/timeline/sugar/omni-test.js.map +1 -0
  132. package/x/timeline/sugar/omni.d.ts +11 -0
  133. package/x/timeline/sugar/omni.js +20 -0
  134. package/x/timeline/sugar/omni.js.map +1 -0
  135. package/x/timeline/utils/checksum.d.ts +8 -0
  136. package/x/timeline/utils/checksum.js +20 -0
  137. package/x/timeline/utils/checksum.js.map +1 -0
  138. package/x/timeline/utils/datafile.d.ts +9 -0
  139. package/x/timeline/utils/datafile.js +20 -0
  140. package/x/timeline/utils/datafile.js.map +1 -0
  141. package/x/timeline/utils/dummy-data.d.ts +2 -0
  142. package/x/timeline/utils/dummy-data.js +3 -0
  143. package/x/timeline/utils/dummy-data.js.map +1 -0
  144. package/s/parts/compositor.ts +0 -5
  145. package/s/parts/export.ts +0 -5
  146. package/s/parts/video-decoder.ts +0 -27
  147. package/s/parts/video-encoder.ts +0 -15
  148. package/s/tools/generate-id.ts +0 -7
  149. package/s/tools/mp4boxjs/LICENSE.md +0 -24
  150. package/s/tools/mp4boxjs/demuxer.ts +0 -106
  151. package/s/tools/mp4boxjs/mp4box.adapter.ts +0 -148
  152. package/s/tools/mp4boxjs/mp4box.js +0 -8206
  153. package/s/types.ts +0 -10
  154. package/x/parts/compositor.d.ts +0 -4
  155. package/x/parts/compositor.js +0 -5
  156. package/x/parts/compositor.js.map +0 -1
  157. package/x/parts/export.d.ts +0 -7
  158. package/x/parts/export.js +0 -5
  159. package/x/parts/export.js.map +0 -1
  160. package/x/parts/video-decoder.d.ts +0 -8
  161. package/x/parts/video-decoder.js +0 -20
  162. package/x/parts/video-decoder.js.map +0 -1
  163. package/x/parts/video-encoder.d.ts +0 -6
  164. package/x/parts/video-encoder.js +0 -12
  165. package/x/parts/video-encoder.js.map +0 -1
  166. package/x/tools/generate-id.d.ts +0 -1
  167. package/x/tools/generate-id.js +0 -8
  168. package/x/tools/generate-id.js.map +0 -1
  169. package/x/tools/mp4boxjs/demuxer.d.ts +0 -24
  170. package/x/tools/mp4boxjs/demuxer.js +0 -88
  171. package/x/tools/mp4boxjs/demuxer.js.map +0 -1
  172. package/x/tools/mp4boxjs/mp4box.adapter.d.ts +0 -128
  173. package/x/tools/mp4boxjs/mp4box.adapter.js +0 -11
  174. package/x/tools/mp4boxjs/mp4box.adapter.js.map +0 -1
  175. package/x/types.d.ts +0 -7
  176. package/x/types.js +0 -2
  177. package/x/types.js.map +0 -1
@@ -0,0 +1,116 @@
1
+ import {Comrade, tune, Thread} from "@e280/comrade"
2
+ import {ALL_FORMATS, Input, type StreamTargetChunk} from "mediabunny"
3
+
4
+ import {Machina} from "./parts/machina.js"
5
+ import {setupDriverHost} from "./fns/host.js"
6
+ import {loadDecoderSource} from "./utils/load-decoder-source.js"
7
+ import {DecoderInput, DriverSchematic, Composition, EncoderInput, DecoderSource} from "./fns/schematic.js"
8
+
9
+ export type DriverOptions = {
10
+ workerUrl: URL | string
11
+ }
12
+
13
+ export class Driver {
14
+ static async setup(options: DriverOptions) {
15
+ const machina = new Machina()
16
+ const thread = await Comrade.thread<DriverSchematic>({
17
+ label: "OmnitoolDriver",
18
+ workerUrl: options.workerUrl,
19
+ setupHost: setupDriverHost(machina),
20
+ })
21
+ return new this(machina, thread)
22
+ }
23
+
24
+ constructor(
25
+ public machina: Machina,
26
+ public thread: Thread<DriverSchematic>
27
+ ) {}
28
+
29
+ async hello() {
30
+ return this.thread.work.hello()
31
+ }
32
+
33
+ async getAudioDuration(source: DecoderSource) {
34
+ const input = new Input({
35
+ source: await loadDecoderSource(source),
36
+ formats: ALL_FORMATS
37
+ })
38
+
39
+ const audioTrack = await input.getPrimaryAudioTrack()
40
+ return await audioTrack?.computeDuration()
41
+ }
42
+
43
+ async getVideoDuration(source: DecoderSource) {
44
+ const input = new Input({
45
+ source: await loadDecoderSource(source),
46
+ formats: ALL_FORMATS
47
+ })
48
+
49
+ const videoTrack = await input.getPrimaryVideoTrack()
50
+ return await videoTrack?.computeDuration()
51
+ }
52
+
53
+ decode(input: DecoderInput) {
54
+ let lastFrame: VideoFrame | null = null
55
+ const videoTransform = new TransformStream<VideoFrame, VideoFrame>({
56
+ async transform(chunk, controller) {
57
+ const frame = await input.onFrame?.(chunk) ?? chunk
58
+ // below code is to prevent mem leaks and hardware accelerated decoder stall
59
+ lastFrame?.close()
60
+ controller.enqueue(frame)
61
+ lastFrame = frame
62
+ }
63
+ })
64
+ const audioTransform = new TransformStream<AudioData, AudioData>()
65
+ this.thread.work.decode[tune]({transfer: [videoTransform.writable, audioTransform.writable]})({
66
+ source: input.source,
67
+ video: videoTransform.writable,
68
+ audio: audioTransform.writable,
69
+ })
70
+ return {
71
+ audio: audioTransform.readable,
72
+ video: videoTransform.readable
73
+ }
74
+ }
75
+
76
+ async encode({readables, config}: EncoderInput) {
77
+ const handle = await window.showSaveFilePicker()
78
+ const writable = await handle.createWritable()
79
+ // making bridge because file picker writable is not transferable
80
+ const bridge = new WritableStream<StreamTargetChunk>({
81
+ async write(chunk) {
82
+ await writable.write(chunk)
83
+ },
84
+ async close() {
85
+ await writable.close()
86
+ }
87
+ })
88
+ return await this.thread.work.encode[tune]({transfer: [readables.audio, readables.video, bridge]})({readables, config, bridge})
89
+ }
90
+
91
+ async composite(
92
+ composition: Composition,
93
+ ) {
94
+ const transfer = this.#collectTransferablesFromComposition(composition)
95
+ return await this.thread.work.composite[tune]({transfer})(composition)
96
+ }
97
+
98
+ #collectTransferablesFromComposition(composition: Composition) {
99
+ const transferables: Transferable[] = []
100
+
101
+ const visit = (node: Composition) => {
102
+ if (Array.isArray(node)) {
103
+ for (const child of node)
104
+ visit(child)
105
+ }
106
+ else if (node && typeof node === 'object' && 'kind' in node) {
107
+ if (node.kind === 'image' && node.frame instanceof VideoFrame)
108
+ transferables.push(node.frame)
109
+ }
110
+ }
111
+
112
+ visit(composition)
113
+ return transferables
114
+ }
115
+ }
116
+
@@ -0,0 +1,7 @@
1
+
2
+ import {Comrade} from "@e280/comrade"
3
+ import {setupDriverWork} from "./fns/work.js"
4
+ import {DriverSchematic} from "./fns/schematic.js"
5
+
6
+ await Comrade.worker<DriverSchematic>(setupDriverWork)
7
+
@@ -0,0 +1,12 @@
1
+
2
+ import {Comrade} from "@e280/comrade"
3
+ import {Machina} from "../parts/machina.js"
4
+ import {DriverSchematic} from "./schematic.js"
5
+
6
+ export const setupDriverHost = (machina: Machina) => Comrade.host<DriverSchematic>(({work}, rig) => ({
7
+
8
+ async world() {
9
+ machina.count++
10
+ }
11
+ }))
12
+
@@ -0,0 +1,83 @@
1
+
2
+ import {AsSchematic} from "@e280/comrade"
3
+ import type {AudioEncodingConfig, StreamTargetChunk, VideoEncodingConfig} from "mediabunny"
4
+
5
+ export type DriverSchematic = AsSchematic<{
6
+
7
+ // happens on the web worker
8
+ work: {
9
+ hello(): Promise<void>
10
+
11
+ decode(input: {
12
+ source: DecoderSource
13
+ video: WritableStream<VideoFrame>
14
+ audio: WritableStream<AudioData>
15
+ }): Promise<void>
16
+
17
+ encode(input: EncoderInput & {bridge: WritableStream<StreamTargetChunk>}): Promise<void>
18
+
19
+ composite(input: Composition): Promise<VideoFrame>
20
+ }
21
+
22
+ // happens on the main thread
23
+ host: {
24
+ world(): Promise<void>
25
+ }
26
+ }>
27
+
28
+ export interface EncoderInput {
29
+ readables: {
30
+ video: ReadableStream<VideoFrame>
31
+ audio: ReadableStream<AudioData>
32
+ },
33
+ config: {
34
+ video: VideoEncodingConfig
35
+ audio: AudioEncodingConfig
36
+ }
37
+ }
38
+
39
+ export type DecoderSource = FileSystemFileHandle | string
40
+
41
+ export interface DecoderInput {
42
+ source: DecoderSource
43
+ onFrame?: (frame: VideoFrame) => Promise<VideoFrame>
44
+ }
45
+
46
+ export interface MuxOpts {
47
+ config: {
48
+ video: {
49
+ width: number
50
+ height: number
51
+ },
52
+ audio?: {
53
+ codec: "opus" | "aac"
54
+ numberOfChannels: number
55
+ sampleRate: number
56
+ }
57
+ }
58
+ }
59
+
60
+ export type Composition = Layer | (Layer | Composition)[]
61
+
62
+ export type Transform = {
63
+ x?: number
64
+ y?: number
65
+ scale?: number
66
+ opacity?: number
67
+ anchor?: number
68
+ }
69
+
70
+ export type TextLayer = {
71
+ kind: 'text'
72
+ content: string
73
+ fontSize?: number
74
+ color?: string
75
+ } & Transform
76
+
77
+ export type ImageLayer = {
78
+ kind: 'image'
79
+ frame: VideoFrame
80
+ } & Transform
81
+
82
+ export type Layer = TextLayer | ImageLayer
83
+
@@ -0,0 +1,237 @@
1
+ import {Comrade} from "@e280/comrade"
2
+ import {
3
+ Input, ALL_FORMATS, VideoSampleSink, Output, Mp4OutputFormat, VideoSampleSource, VideoSample,
4
+ AudioSampleSink, AudioSampleSource, AudioSample, StreamTarget, BlobSource, UrlSource
5
+ } from "mediabunny"
6
+ import {autoDetectRenderer, Container, Renderer, Sprite, Text, Texture, DOMAdapter, WebWorkerAdapter} from "pixi.js"
7
+
8
+ import {Composition, DriverSchematic, Layer, Transform} from "./schematic.js"
9
+
10
+ DOMAdapter.set(WebWorkerAdapter)
11
+
12
+ export const setupDriverWork = Comrade.work<DriverSchematic>(({host}, rig) => ({
13
+
14
+ async hello() {
15
+ await host.world()
16
+ },
17
+
18
+ async decode({source, video, audio}) {
19
+ const loadSource = async () => {
20
+ if(source instanceof FileSystemFileHandle) {
21
+ const file = await source.getFile()
22
+ return new BlobSource(file)
23
+ } else {
24
+ return new UrlSource(source)
25
+ }
26
+ }
27
+ const input = new Input({
28
+ source: await loadSource(),
29
+ formats: ALL_FORMATS
30
+ })
31
+
32
+ const [videoTrack, audioTrack] = await Promise.all([
33
+ input.getPrimaryVideoTrack(),
34
+ input.getPrimaryAudioTrack()
35
+ ])
36
+
37
+ const videoDecodable = await videoTrack?.canDecode()
38
+ const audioDecodable = await audioTrack?.canDecode()
39
+
40
+ const videoWriter = video.getWriter()
41
+ const audioWriter = audio.getWriter()
42
+
43
+ await Promise.all([
44
+ (async () => {
45
+ if (videoDecodable && videoTrack) {
46
+ const sink = new VideoSampleSink(videoTrack)
47
+ for await (const sample of sink.samples()) {
48
+ const frame = sample.toVideoFrame()
49
+ await videoWriter.write(frame)
50
+ sample.close()
51
+ frame.close()
52
+ }
53
+ await videoWriter.close()
54
+ }
55
+ })(),
56
+ (async () => {
57
+ if (audioDecodable && audioTrack) {
58
+ const sink = new AudioSampleSink(audioTrack)
59
+ for await (const sample of sink.samples()) {
60
+ const frame = sample.toAudioData()
61
+ await audioWriter.write(frame)
62
+ sample.close()
63
+ frame.close()
64
+ }
65
+ await audioWriter.close()
66
+ }
67
+ })()
68
+ ])
69
+ },
70
+
71
+ async encode({readables, config, bridge}) {
72
+ const output = new Output({
73
+ format: new Mp4OutputFormat(),
74
+ target: new StreamTarget(bridge, {chunked: true})
75
+ })
76
+ const videoSource = new VideoSampleSource(config.video)
77
+ output.addVideoTrack(videoSource)
78
+ // since AudioSample is not transferable it fails to transfer encoder bitrate config
79
+ // so it needs to be hardcoded not set through constants eg QUALITY_LOW
80
+ const audioSource = new AudioSampleSource(config.audio)
81
+ output.addAudioTrack(audioSource)
82
+
83
+ await output.start()
84
+
85
+ const videoReader = readables.video.getReader()
86
+ const audioReader = readables.audio.getReader()
87
+
88
+ await Promise.all([
89
+ (async () => {
90
+ while (true) {
91
+ const {done, value} = await videoReader.read()
92
+ if (done) break
93
+ const sample = new VideoSample(value)
94
+ await videoSource.add(sample)
95
+ sample.close()
96
+ }
97
+ })(),
98
+ (async () => {
99
+ while (true) {
100
+ const {done, value} = await audioReader.read()
101
+ if (done) break
102
+ const sample = new AudioSample(value)
103
+ await audioSource.add(sample)
104
+ sample.close()
105
+ value.close()
106
+ }
107
+ })()
108
+ ])
109
+
110
+ await output.finalize()
111
+ },
112
+
113
+ async composite(composition) {
114
+ const {stage, renderer} = await renderPIXI(1920, 1080)
115
+ stage.removeChildren()
116
+
117
+ const {baseFrame, disposables} = await renderLayer(composition, stage)
118
+ renderer.render(stage)
119
+
120
+ // make sure browser support webgl/webgpu otherwise it might take much longer to construct frame
121
+ // if its very slow on eg edge try chrome
122
+ const frame = new VideoFrame(renderer.canvas, {
123
+ timestamp: baseFrame?.timestamp,
124
+ duration: baseFrame?.duration ?? undefined,
125
+ })
126
+
127
+ baseFrame?.close()
128
+ renderer.clear()
129
+
130
+ for (const disposable of disposables) {
131
+ disposable.destroy(true)
132
+ }
133
+
134
+ rig.transfer = [frame]
135
+ return frame
136
+ }
137
+ }))
138
+
139
+
140
+ let pixi: {
141
+ renderer: Renderer
142
+ stage: Container
143
+ } | null = null
144
+
145
+ async function renderPIXI(width: number, height: number) {
146
+ if (pixi)
147
+ return pixi
148
+
149
+ const renderer = await autoDetectRenderer({
150
+ width,
151
+ height,
152
+ preference: "webgl", // webgl and webgl2 causes memory leaks on chrome
153
+ background: "black",
154
+ preferWebGLVersion: 2
155
+ })
156
+
157
+ const stage = new Container()
158
+ pixi = {renderer, stage}
159
+
160
+ return pixi
161
+ }
162
+
163
+ type RenderableObject = Sprite | Text | Texture
164
+
165
+ async function renderLayer(
166
+ layer: Layer | Composition,
167
+ parent: Container,
168
+ disposables: RenderableObject[] = []
169
+ ) {
170
+ if (Array.isArray(layer)) {
171
+ let baseFrame: VideoFrame | undefined
172
+ for (const child of layer) {
173
+ const result = await renderLayer(child, parent, disposables)
174
+ baseFrame ??= result.baseFrame
175
+ }
176
+ return {baseFrame, disposables}
177
+ }
178
+
179
+ if (!isRenderableLayer(layer)) {
180
+ console.warn('Invalid layer', layer)
181
+ return {disposables}
182
+ }
183
+
184
+ switch (layer.kind) {
185
+ case 'text':
186
+ return renderTextLayer(layer, parent, disposables)
187
+ case 'image':
188
+ return renderImageLayer(layer, parent, disposables)
189
+ default:
190
+ console.warn('Unknown layer kind', (layer as any).kind)
191
+ return {disposables}
192
+ }
193
+ }
194
+
195
+ function isRenderableLayer(layer: any): layer is Layer {
196
+ return !!layer && typeof layer === 'object' && typeof layer.kind === 'string'
197
+ }
198
+
199
+ function renderTextLayer(
200
+ layer: Extract<Layer, {kind: 'text'}>,
201
+ parent: Container,
202
+ disposables: RenderableObject[]
203
+ ) {
204
+ const text = new Text({
205
+ text: layer.content,
206
+ style: {
207
+ fontFamily: 'sans-serif',
208
+ fontSize: layer.fontSize ?? 48,
209
+ fill: layer.color ?? 'white'
210
+ }
211
+ })
212
+ applyTransform(text, layer)
213
+ parent.addChild(text)
214
+ disposables.push(text)
215
+ return {disposables}
216
+ }
217
+
218
+ function renderImageLayer(
219
+ layer: Extract<Layer, {kind: 'image'}>,
220
+ parent: Container,
221
+ disposables: RenderableObject[]
222
+ ) {
223
+ const texture = Texture.from(layer.frame)
224
+ const sprite = new Sprite(texture)
225
+ applyTransform(sprite, layer)
226
+ parent.addChild(sprite)
227
+ disposables.push(sprite, texture)
228
+ return {baseFrame: layer.frame, disposables}
229
+ }
230
+
231
+ function applyTransform(target: Sprite | Text, t: Transform = {}) {
232
+ if(t.x) target.x = t.x
233
+ if(t.y) target.y = t.y
234
+ if(t.scale) target.scale.set(t.scale)
235
+ if(t.opacity) target.alpha = t.opacity
236
+ if(t.anchor && 'anchor' in target) target.anchor.set(t.anchor)
237
+ }
@@ -0,0 +1,17 @@
1
+ // for later: https://github.com/gpac/mp4box.js/issues/243
2
+ export const encoderDefaultConfig: VideoEncoderConfig = {
3
+ codec: "avc1.640034",
4
+ avc: {format: "annexb"},
5
+ width: 1280,
6
+ height: 720,
7
+ bitrate: 9_000_000, // 9 Mbps
8
+ framerate: 60,
9
+ bitrateMode: "variable",
10
+ hardwareAcceleration: "no-preference" // prefer-hardware seems like 2x slower from what i been testing
11
+ }
12
+
13
+ export const audioEncoderDefaultConfig: AudioEncoderConfig = {
14
+ codec: "opus",
15
+ numberOfChannels: 2,
16
+ sampleRate: 44100
17
+ }
@@ -0,0 +1,27 @@
1
+ import {WebMediaInfo} from "web-demuxer"
2
+
3
+ type Events =
4
+ | {type: "config", config: {audio: AudioDecoderConfig, video: VideoDecoderConfig}}
5
+ | {type: "info", data: WebMediaInfo}
6
+ | {type: "encoderQueueSize", size: number}
7
+
8
+ type Handler = (event: Events) => void
9
+
10
+ export class Machina {
11
+ count = 0
12
+
13
+ #handlers = new Map<number, Handler>()
14
+
15
+ register(id: number, handler: Handler) {
16
+ this.#handlers.set(id, handler)
17
+ }
18
+
19
+ unregister(id: number) {
20
+ this.#handlers.delete(id)
21
+ }
22
+
23
+ dispatch(id: number, event: Events) {
24
+ this.#handlers.get(id)?.(event)
25
+ }
26
+ }
27
+
@@ -0,0 +1,13 @@
1
+ import {BlobSource, UrlSource} from "mediabunny"
2
+ import {DecoderSource} from "../fns/schematic.js"
3
+
4
+ // only streamable sources
5
+ export async function loadDecoderSource(source: DecoderSource) {
6
+ if(source instanceof FileSystemFileHandle) {
7
+ const file = await source.getFile()
8
+ return new BlobSource(file)
9
+ } else {
10
+ return new UrlSource(source)
11
+ }
12
+ }
13
+
@@ -0,0 +1,3 @@
1
+ export function sleep(ms: number) {
2
+ return new Promise(resolve => setTimeout(resolve, ms));
3
+ }
@@ -0,0 +1,53 @@
1
+
2
+ import {ssg, html} from "@e280/scute"
3
+
4
+ const title = "omnitool"
5
+ const domain = "omnitool.omniclip.app"
6
+ const favicon = "/assets/favicon.png"
7
+
8
+ export default ssg.page(import.meta.url, async orb => ({
9
+ title,
10
+ // favicon,
11
+ dark: true,
12
+ css: "demo/demo.css",
13
+ js: "demo/demo.bundle.min.js",
14
+
15
+ head: html`
16
+ <link rel="preconnect" href="https://fonts.googleapis.com">
17
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
18
+ <link href="https://fonts.googleapis.com/css2?family=Share+Tech&display=swap" rel="stylesheet">
19
+ `,
20
+
21
+ socialCard: {
22
+ title,
23
+ description: "video processing toolkit",
24
+ themeColor: "#3cff9c",
25
+ siteName: domain,
26
+ image: `https://${domain}${favicon}`,
27
+ },
28
+
29
+ body: html`
30
+ <section>
31
+ <h1>Omnitool <small>v${orb.packageVersion()}</small></h1>
32
+ <button class=fetch>fetch</button>
33
+ <button class="import">import</button>
34
+ <div class=results></div>
35
+ <div class=filmstrip-demo>
36
+ <label for="viewable-range">viewable range:</label>
37
+ <input type="range" min="0" max="100" step="1" value="10" class="range" id="viewable-range" name="viewable-range">
38
+ <div class="range-view"></div>
39
+ <label for="range-size">viewable range size:</label>
40
+ <input type="range" class="range-size" min="0.1" max="10" step="0.1" value="0.5" id="range-size" name="range-size">
41
+ <label for="frequency">frequency:</label>
42
+ <input type="range" class="frequency" min="0.1" max="120" step="0.1" value="10" id="frequency" name="frequency">
43
+ <div class="frequency-view">10 (fps)</div>
44
+ <div id=filmstrip></div>
45
+ </div>
46
+ <div class=waveform-demo>
47
+ <label for="width">width:</label>
48
+ <input class="width" id="width" name="width" type="range" min="100" max="1000000" value="1000" />
49
+ </div>
50
+ </section>
51
+ `,
52
+ }))
53
+
package/s/index.ts CHANGED
@@ -1,41 +1,4 @@
1
- import {MP4Demuxer} from "./tools/mp4boxjs/demuxer.js"
2
-
3
- import {Clip, LoadedVideoClip} from "./types.js"
4
- import {generateId} from "./tools/generate-id.js"
5
- import { OmniVideoDecoder } from "./parts/video-decoder.js"
6
-
7
- export class OmniTool {
8
- #clips: Map<Clip, LoadedVideoClip> = new Map()
9
- #getLoadedClip = (clip: Clip) => this.#clips.get(clip)
10
-
11
- videoDecoder = new OmniVideoDecoder(this.#getLoadedClip)
12
-
13
- // ffmpeg node or web version
14
- constructor(ffmpeg) {}
15
-
16
-
17
- load(file: File): Promise<Clip> {
18
- return new Promise((resolve) => {
19
- const demuxer = new MP4Demuxer(file, {
20
- onChunk: () => {},
21
- onConfig: () => {},
22
- framesCount: () => {},
23
- setStatus: () => {},
24
- OnReady: () => {
25
- const demuxed = {id: generateId(), demuxer}
26
- this.#clips.set(demuxed, demuxed)
27
- resolve(demuxed)
28
- }
29
- })
30
- })
31
-
32
- }
33
-
34
- async getFramesCount() {}
35
- }
36
-
37
- const tool = new OmniTool('ffmpeg')
38
- // const file = tool.load()
39
- // tool.videoDecoder.decode(file)
40
1
 
2
+ export * from "./driver/driver.js"
3
+ export * from "./timeline/index.js"
41
4
 
@@ -0,0 +1,8 @@
1
+
2
+ import {Science} from "@e280/science"
3
+ import driver from "./driver/driver.test.js"
4
+
5
+ await Science.run({
6
+ driver,
7
+ })
8
+
@@ -0,0 +1,14 @@
1
+
2
+ export * from "./parts/basics.js"
3
+ export * from "./parts/item.js"
4
+ export * from "./parts/media.js"
5
+ export * from "./parts/resource-pool.js"
6
+ export * from "./parts/resource.js"
7
+ export * from "./parts/filmstrip.js"
8
+
9
+ export * from "./sugar/o.js"
10
+ export * from "./sugar/omni.js"
11
+
12
+ export * from "./utils/checksum.js"
13
+ export * from "./utils/datafile.js"
14
+
@@ -0,0 +1,17 @@
1
+
2
+ import {Item} from "./item.js"
3
+
4
+ /** sha256 hash */
5
+ export type Hash = string
6
+
7
+ /** item identifier */
8
+ export type Id = number
9
+
10
+ export type TimelineFile = {
11
+ info: "https://omniclip.app/"
12
+ format: "timeline"
13
+ version: number
14
+ root: Id
15
+ items: Item.Any[]
16
+ }
17
+