@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.
- package/LICENSE +21 -0
- package/README.md +120 -2
- package/package.json +56 -27
- package/s/_archive/types.ts +107 -0
- package/s/context.ts +7 -0
- package/s/demo/demo.bundle.ts +64 -0
- package/s/demo/demo.css +54 -0
- package/s/demo/routines/filmstrip-test.ts +68 -0
- package/s/demo/routines/load-video.ts +7 -0
- package/s/demo/routines/transcode-test.ts +44 -0
- package/s/demo/routines/waveform-test.ts +12 -0
- package/s/driver/driver.test.ts +15 -0
- package/s/driver/driver.ts +116 -0
- package/s/driver/driver.worker.bundle.ts +7 -0
- package/s/driver/fns/host.ts +12 -0
- package/s/driver/fns/schematic.ts +83 -0
- package/s/driver/fns/work.ts +237 -0
- package/s/driver/parts/constants.ts +17 -0
- package/s/driver/parts/machina.ts +27 -0
- package/s/driver/utils/load-decoder-source.ts +13 -0
- package/s/driver/utils/sleep.ts +3 -0
- package/s/index.html.ts +53 -0
- package/s/index.ts +2 -39
- package/s/tests.test.ts +8 -0
- package/s/timeline/index.ts +14 -0
- package/s/timeline/parts/basics.ts +17 -0
- package/s/timeline/parts/filmstrip.ts +159 -0
- package/s/timeline/parts/item.ts +58 -0
- package/s/timeline/parts/media.ts +14 -0
- package/s/timeline/parts/resource-pool.ts +27 -0
- package/s/timeline/parts/resource.ts +11 -0
- package/s/timeline/parts/waveform.ts +62 -0
- package/s/timeline/sugar/o.ts +60 -0
- package/s/timeline/sugar/omni-test.ts +38 -0
- package/s/timeline/sugar/omni.ts +30 -0
- package/s/timeline/utils/checksum.ts +19 -0
- package/s/timeline/utils/datafile.ts +21 -0
- package/s/timeline/utils/dummy-data.ts +7 -0
- package/x/context.d.ts +4 -0
- package/x/context.js +6 -0
- package/x/context.js.map +1 -0
- package/x/demo/demo.bundle.d.ts +1 -0
- package/x/demo/demo.bundle.js +51 -0
- package/x/demo/demo.bundle.js.map +1 -0
- package/x/demo/demo.bundle.min.js +118 -0
- package/x/demo/demo.bundle.min.js.map +7 -0
- package/x/demo/demo.css +54 -0
- package/x/demo/routines/filmstrip-test.d.ts +1 -0
- package/x/demo/routines/filmstrip-test.js +62 -0
- package/x/demo/routines/filmstrip-test.js.map +1 -0
- package/x/demo/routines/load-video.d.ts +1 -0
- package/x/demo/routines/load-video.js +6 -0
- package/x/demo/routines/load-video.js.map +1 -0
- package/x/demo/routines/transcode-test.d.ts +6 -0
- package/x/demo/routines/transcode-test.js +38 -0
- package/x/demo/routines/transcode-test.js.map +1 -0
- package/x/demo/routines/waveform-test.d.ts +1 -0
- package/x/demo/routines/waveform-test.js +11 -0
- package/x/demo/routines/waveform-test.js.map +1 -0
- package/x/driver/driver.d.ts +22 -0
- package/x/driver/driver.js +97 -0
- package/x/driver/driver.js.map +1 -0
- package/x/driver/driver.test.d.ts +5 -0
- package/x/driver/driver.test.js +12 -0
- package/x/driver/driver.test.js.map +1 -0
- package/x/driver/driver.worker.bundle.d.ts +1 -0
- package/x/driver/driver.worker.bundle.js +4 -0
- package/x/driver/driver.worker.bundle.js.map +1 -0
- package/x/driver/driver.worker.bundle.min.js +1148 -0
- package/x/driver/driver.worker.bundle.min.js.map +7 -0
- package/x/driver/fns/host.d.ts +18 -0
- package/x/driver/fns/host.js +7 -0
- package/x/driver/fns/host.js.map +1 -0
- package/x/driver/fns/schematic.d.ts +66 -0
- package/x/driver/fns/schematic.js +2 -0
- package/x/driver/fns/schematic.js.map +1 -0
- package/x/driver/fns/work.d.ts +19 -0
- package/x/driver/fns/work.js +192 -0
- package/x/driver/fns/work.js.map +1 -0
- package/x/driver/parts/constants.d.ts +2 -0
- package/x/driver/parts/constants.js +17 -0
- package/x/driver/parts/constants.js.map +1 -0
- package/x/driver/parts/machina.d.ts +23 -0
- package/x/driver/parts/machina.js +14 -0
- package/x/driver/parts/machina.js.map +1 -0
- package/x/driver/utils/load-decoder-source.d.ts +2 -0
- package/x/driver/utils/load-decoder-source.js +12 -0
- package/x/driver/utils/load-decoder-source.js.map +1 -0
- package/x/driver/utils/sleep.d.ts +1 -0
- package/x/driver/utils/sleep.js +4 -0
- package/x/driver/utils/sleep.js.map +1 -0
- package/x/index.d.ts +2 -9
- package/x/index.html +105 -0
- package/x/index.html.d.ts +2 -0
- package/x/index.html.js +47 -0
- package/x/index.html.js.map +1 -0
- package/x/index.js +2 -29
- package/x/index.js.map +1 -1
- package/x/tests.test.d.ts +1 -0
- package/x/tests.test.js +6 -0
- package/x/tests.test.js.map +1 -0
- package/x/timeline/index.d.ts +10 -0
- package/x/timeline/index.js +11 -0
- package/x/timeline/index.js.map +1 -0
- package/x/timeline/parts/basics.d.ts +12 -0
- package/x/timeline/parts/basics.js +2 -0
- package/x/timeline/parts/basics.js.map +1 -0
- package/x/timeline/parts/filmstrip.d.ts +39 -0
- package/x/timeline/parts/filmstrip.js +117 -0
- package/x/timeline/parts/filmstrip.js.map +1 -0
- package/x/timeline/parts/item.d.ts +42 -0
- package/x/timeline/parts/item.js +13 -0
- package/x/timeline/parts/item.js.map +1 -0
- package/x/timeline/parts/media.d.ts +7 -0
- package/x/timeline/parts/media.js +13 -0
- package/x/timeline/parts/media.js.map +1 -0
- package/x/timeline/parts/resource-pool.d.ts +7 -0
- package/x/timeline/parts/resource-pool.js +19 -0
- package/x/timeline/parts/resource-pool.js.map +1 -0
- package/x/timeline/parts/resource.d.ts +8 -0
- package/x/timeline/parts/resource.js +2 -0
- package/x/timeline/parts/resource.js.map +1 -0
- package/x/timeline/parts/waveform.d.ts +8 -0
- package/x/timeline/parts/waveform.js +51 -0
- package/x/timeline/parts/waveform.js.map +1 -0
- package/x/timeline/sugar/o.d.ts +14 -0
- package/x/timeline/sugar/o.js +48 -0
- package/x/timeline/sugar/o.js.map +1 -0
- package/x/timeline/sugar/omni-test.d.ts +1 -0
- package/x/timeline/sugar/omni-test.js +22 -0
- package/x/timeline/sugar/omni-test.js.map +1 -0
- package/x/timeline/sugar/omni.d.ts +11 -0
- package/x/timeline/sugar/omni.js +20 -0
- package/x/timeline/sugar/omni.js.map +1 -0
- package/x/timeline/utils/checksum.d.ts +8 -0
- package/x/timeline/utils/checksum.js +20 -0
- package/x/timeline/utils/checksum.js.map +1 -0
- package/x/timeline/utils/datafile.d.ts +9 -0
- package/x/timeline/utils/datafile.js +20 -0
- package/x/timeline/utils/datafile.js.map +1 -0
- package/x/timeline/utils/dummy-data.d.ts +2 -0
- package/x/timeline/utils/dummy-data.js +3 -0
- package/x/timeline/utils/dummy-data.js.map +1 -0
- package/s/parts/compositor.ts +0 -5
- package/s/parts/export.ts +0 -5
- package/s/parts/video-decoder.ts +0 -27
- package/s/parts/video-encoder.ts +0 -15
- package/s/tools/generate-id.ts +0 -7
- package/s/tools/mp4boxjs/LICENSE.md +0 -24
- package/s/tools/mp4boxjs/demuxer.ts +0 -106
- package/s/tools/mp4boxjs/mp4box.adapter.ts +0 -148
- package/s/tools/mp4boxjs/mp4box.js +0 -8206
- package/s/types.ts +0 -10
- package/x/parts/compositor.d.ts +0 -4
- package/x/parts/compositor.js +0 -5
- package/x/parts/compositor.js.map +0 -1
- package/x/parts/export.d.ts +0 -7
- package/x/parts/export.js +0 -5
- package/x/parts/export.js.map +0 -1
- package/x/parts/video-decoder.d.ts +0 -8
- package/x/parts/video-decoder.js +0 -20
- package/x/parts/video-decoder.js.map +0 -1
- package/x/parts/video-encoder.d.ts +0 -6
- package/x/parts/video-encoder.js +0 -12
- package/x/parts/video-encoder.js.map +0 -1
- package/x/tools/generate-id.d.ts +0 -1
- package/x/tools/generate-id.js +0 -8
- package/x/tools/generate-id.js.map +0 -1
- package/x/tools/mp4boxjs/demuxer.d.ts +0 -24
- package/x/tools/mp4boxjs/demuxer.js +0 -88
- package/x/tools/mp4boxjs/demuxer.js.map +0 -1
- package/x/tools/mp4boxjs/mp4box.adapter.d.ts +0 -128
- package/x/tools/mp4boxjs/mp4box.adapter.js +0 -11
- package/x/tools/mp4boxjs/mp4box.adapter.js.map +0 -1
- package/x/types.d.ts +0 -7
- package/x/types.js +0 -2
- 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,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
|
+
|
package/s/index.html.ts
ADDED
|
@@ -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
|
|
package/s/tests.test.ts
ADDED
|
@@ -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
|
+
|