@omnimedia/omnitool 1.1.0-5 → 1.1.0-8
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/package.json +3 -2
- package/s/demo/demo.css +5 -0
- package/s/demo/routines/transcode-test.ts +4 -2
- package/s/demo/routines/transitions-test.ts +2 -2
- package/s/driver/driver.ts +17 -9
- package/s/driver/fns/schematic.ts +44 -21
- package/s/driver/fns/work.ts +112 -97
- package/s/index.html.ts +6 -1
- package/s/timeline/index.ts +1 -0
- package/s/timeline/parts/basics.ts +1 -1
- package/s/timeline/parts/compositor/export.ts +77 -0
- package/s/timeline/parts/compositor/parts/html-tree.ts +37 -0
- package/s/timeline/parts/compositor/parts/schedulers.ts +85 -0
- package/s/timeline/parts/compositor/parts/tree-builder.ts +184 -0
- package/s/timeline/parts/compositor/parts/webcodecs-tree.ts +30 -0
- package/s/timeline/parts/compositor/playback.ts +81 -0
- package/s/timeline/parts/compositor/samplers/html.ts +115 -0
- package/s/timeline/parts/compositor/samplers/webcodecs.ts +60 -0
- package/s/timeline/parts/item.ts +38 -6
- package/s/timeline/parts/media.ts +21 -0
- package/s/timeline/parts/waveform.ts +1 -1
- package/s/timeline/sugar/builders.ts +102 -0
- package/s/timeline/sugar/o.ts +75 -16
- package/s/timeline/sugar/omni-test.ts +2 -2
- package/s/timeline/sugar/omni.ts +14 -11
- package/s/timeline/timeline.ts +22 -0
- package/s/timeline/types.ts +29 -0
- package/s/timeline/utils/audio-stream.ts +15 -0
- package/s/timeline/utils/matrix.ts +33 -0
- package/s/timeline/utils/video-cursor.ts +40 -0
- package/x/demo/demo.bundle.min.js +39 -37
- package/x/demo/demo.bundle.min.js.map +4 -4
- package/x/demo/demo.css +5 -0
- package/x/demo/routines/transcode-test.js +4 -2
- package/x/demo/routines/transcode-test.js.map +1 -1
- package/x/demo/routines/transitions-test.js +2 -2
- package/x/demo/routines/transitions-test.js.map +1 -1
- package/x/driver/driver.d.ts +3 -5
- package/x/driver/driver.js +16 -9
- package/x/driver/driver.js.map +1 -1
- package/x/driver/driver.worker.bundle.min.js +2537 -148
- package/x/driver/driver.worker.bundle.min.js.map +4 -4
- package/x/driver/fns/host.d.ts +9 -2
- package/x/driver/fns/schematic.d.ts +38 -20
- package/x/driver/fns/work.d.ts +11 -4
- package/x/driver/fns/work.js +105 -96
- package/x/driver/fns/work.js.map +1 -1
- package/x/features/speech/transcribe/worker.bundle.min.js +541 -541
- package/x/features/speech/transcribe/worker.bundle.min.js.map +4 -4
- package/x/index.html +13 -3
- package/x/index.html.js +6 -1
- package/x/index.html.js.map +1 -1
- package/x/timeline/index.d.ts +1 -0
- package/x/timeline/index.js +1 -0
- package/x/timeline/index.js.map +1 -1
- package/x/timeline/parts/basics.d.ts +1 -1
- package/x/timeline/parts/compositor/export.d.ts +9 -0
- package/x/timeline/parts/compositor/export.js +64 -0
- package/x/timeline/parts/compositor/export.js.map +1 -0
- package/x/timeline/parts/compositor/parts/html-tree.d.ts +3 -0
- package/x/timeline/parts/compositor/parts/html-tree.js +40 -0
- package/x/timeline/parts/compositor/parts/html-tree.js.map +1 -0
- package/x/timeline/parts/compositor/parts/schedulers.d.ts +15 -0
- package/x/timeline/parts/compositor/parts/schedulers.js +64 -0
- package/x/timeline/parts/compositor/parts/schedulers.js.map +1 -0
- package/x/timeline/parts/compositor/parts/tree-builder.d.ts +37 -0
- package/x/timeline/parts/compositor/parts/tree-builder.js +147 -0
- package/x/timeline/parts/compositor/parts/tree-builder.js.map +1 -0
- package/x/timeline/parts/compositor/parts/webcodecs-tree.d.ts +3 -0
- package/x/timeline/parts/compositor/parts/webcodecs-tree.js +28 -0
- package/x/timeline/parts/compositor/parts/webcodecs-tree.js.map +1 -0
- package/x/timeline/parts/compositor/playback.d.ts +19 -0
- package/x/timeline/parts/compositor/playback.js +71 -0
- package/x/timeline/parts/compositor/playback.js.map +1 -0
- package/x/timeline/parts/compositor/samplers/html.d.ts +3 -0
- package/x/timeline/parts/compositor/samplers/html.js +106 -0
- package/x/timeline/parts/compositor/samplers/html.js.map +1 -0
- package/x/timeline/parts/compositor/samplers/webcodecs.d.ts +2 -0
- package/x/timeline/parts/compositor/samplers/webcodecs.js +55 -0
- package/x/timeline/parts/compositor/samplers/webcodecs.js.map +1 -0
- package/x/timeline/parts/item.d.ts +34 -8
- package/x/timeline/parts/item.js +6 -3
- package/x/timeline/parts/item.js.map +1 -1
- package/x/timeline/parts/media.d.ts +3 -0
- package/x/timeline/parts/media.js +17 -0
- package/x/timeline/parts/media.js.map +1 -1
- package/x/timeline/parts/waveform.js +1 -1
- package/x/timeline/parts/waveform.js.map +1 -1
- package/x/timeline/sugar/builders.d.ts +96 -0
- package/x/timeline/sugar/builders.js +108 -0
- package/x/timeline/sugar/builders.js.map +1 -0
- package/x/timeline/sugar/o.d.ts +21 -8
- package/x/timeline/sugar/o.js +63 -14
- package/x/timeline/sugar/o.js.map +1 -1
- package/x/timeline/sugar/omni-test.js +1 -1
- package/x/timeline/sugar/omni-test.js.map +1 -1
- package/x/timeline/sugar/omni.d.ts +7 -3
- package/x/timeline/sugar/omni.js +9 -8
- package/x/timeline/sugar/omni.js.map +1 -1
- package/x/timeline/timeline.d.ts +9 -0
- package/x/timeline/timeline.js +22 -0
- package/x/timeline/timeline.js.map +1 -0
- package/x/timeline/types.d.ts +24 -0
- package/x/timeline/types.js +2 -0
- package/x/timeline/types.js.map +1 -0
- package/x/timeline/utils/audio-stream.d.ts +6 -0
- package/x/timeline/utils/audio-stream.js +17 -0
- package/x/timeline/utils/audio-stream.js.map +1 -0
- package/x/timeline/utils/matrix.d.ts +8 -0
- package/x/timeline/utils/matrix.js +26 -0
- package/x/timeline/utils/matrix.js.map +1 -0
- package/x/timeline/utils/video-cursor.d.ts +10 -0
- package/x/timeline/utils/video-cursor.js +36 -0
- package/x/timeline/utils/video-cursor.js.map +1 -0
- package/x/tools/speech-recognition/whisper/parts/worker.bundle.min.js +6 -6
- package/x/tools/speech-recognition/whisper/parts/worker.bundle.min.js.map +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@omnimedia/omnitool",
|
|
3
|
-
"version": "1.1.0-
|
|
3
|
+
"version": "1.1.0-8",
|
|
4
4
|
"description": "open source video processing tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Przemysław Gałęzki",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"@huggingface/transformers": "^3.7.1",
|
|
39
39
|
"comrade": "^0.0.3",
|
|
40
40
|
"gl-transitions": "^1.43.0",
|
|
41
|
-
"
|
|
41
|
+
"gsap": "^3.13.0",
|
|
42
|
+
"mediabunny": "^1.14.3",
|
|
42
43
|
"mp4-muxer": "^5.2.1",
|
|
43
44
|
"pixi.js": "^8.10.1",
|
|
44
45
|
"wavesurfer.js": "^7.10.0",
|
package/s/demo/demo.css
CHANGED
|
@@ -10,7 +10,7 @@ export function setupTranscodeTest(driver: Driver, source: DecoderSource) {
|
|
|
10
10
|
const ctx = canvas.getContext("2d")
|
|
11
11
|
|
|
12
12
|
async function run() {
|
|
13
|
-
const
|
|
13
|
+
const video = driver.decodeVideo({
|
|
14
14
|
source,
|
|
15
15
|
async onFrame(frame) {
|
|
16
16
|
const composed = await driver.composite([
|
|
@@ -30,9 +30,11 @@ export function setupTranscodeTest(driver: Driver, source: DecoderSource) {
|
|
|
30
30
|
return composed
|
|
31
31
|
}
|
|
32
32
|
})
|
|
33
|
+
const audio = driver.decodeAudio({source})
|
|
33
34
|
|
|
34
35
|
await driver.encode({
|
|
35
|
-
|
|
36
|
+
video,
|
|
37
|
+
audio,
|
|
36
38
|
config: {
|
|
37
39
|
audio: {codec: "opus", bitrate: 128000},
|
|
38
40
|
video: {codec: "vp9", bitrate: 1000000}
|
|
@@ -15,7 +15,7 @@ export async function setupTransitionsTest(driver: Driver, source: DecoderSource
|
|
|
15
15
|
const transition = makeTransition({name: "circle", renderer: app.renderer})
|
|
16
16
|
|
|
17
17
|
async function run() {
|
|
18
|
-
const
|
|
18
|
+
const video = driver.decodeVideo({
|
|
19
19
|
source,
|
|
20
20
|
async onFrame(frame) {
|
|
21
21
|
const texture = transition.render({
|
|
@@ -31,7 +31,7 @@ export async function setupTransitionsTest(driver: Driver, source: DecoderSource
|
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
await driver.encode({
|
|
34
|
-
|
|
34
|
+
video,
|
|
35
35
|
config: {
|
|
36
36
|
audio: {codec: "opus", bitrate: 128000},
|
|
37
37
|
video: {codec: "vp9", bitrate: 1000000}
|
package/s/driver/driver.ts
CHANGED
|
@@ -50,7 +50,7 @@ export class Driver {
|
|
|
50
50
|
return await videoTrack?.computeDuration()
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
decodeVideo(input: DecoderInput) {
|
|
54
54
|
let lastFrame: VideoFrame | null = null
|
|
55
55
|
const videoTransform = new TransformStream<VideoFrame, VideoFrame>({
|
|
56
56
|
async transform(chunk, controller) {
|
|
@@ -61,19 +61,27 @@ export class Driver {
|
|
|
61
61
|
lastFrame = frame
|
|
62
62
|
}
|
|
63
63
|
})
|
|
64
|
-
|
|
65
|
-
this.thread.work.decode[tune]({transfer: [videoTransform.writable, audioTransform.writable]})({
|
|
64
|
+
this.thread.work.decodeVideo[tune]({transfer: [videoTransform.writable]})({
|
|
66
65
|
source: input.source,
|
|
67
66
|
video: videoTransform.writable,
|
|
67
|
+
start: input.start,
|
|
68
|
+
end: input.end
|
|
69
|
+
})
|
|
70
|
+
return videoTransform.readable
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
decodeAudio(input: DecoderInput) {
|
|
74
|
+
const audioTransform = new TransformStream<AudioData, AudioData>()
|
|
75
|
+
this.thread.work.decodeAudio[tune]({transfer: [audioTransform.writable]})({
|
|
76
|
+
source: input.source,
|
|
68
77
|
audio: audioTransform.writable,
|
|
78
|
+
start: input.start,
|
|
79
|
+
end: input.end
|
|
69
80
|
})
|
|
70
|
-
return
|
|
71
|
-
audio: audioTransform.readable,
|
|
72
|
-
video: videoTransform.readable
|
|
73
|
-
}
|
|
81
|
+
return audioTransform.readable
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
async encode({
|
|
84
|
+
async encode({video, audio, config}: EncoderInput) {
|
|
77
85
|
const handle = await window.showSaveFilePicker()
|
|
78
86
|
const writable = await handle.createWritable()
|
|
79
87
|
// making bridge because file picker writable is not transferable
|
|
@@ -85,7 +93,7 @@ export class Driver {
|
|
|
85
93
|
await writable.close()
|
|
86
94
|
}
|
|
87
95
|
})
|
|
88
|
-
return await this.thread.work.encode[tune]({transfer: [
|
|
96
|
+
return await this.thread.work.encode[tune]({transfer: [audio ?? [], video ?? [], bridge]})({video, audio, config, bridge})
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
async composite(
|
|
@@ -2,16 +2,26 @@
|
|
|
2
2
|
import {AsSchematic} from "@e280/comrade"
|
|
3
3
|
import type {AudioEncodingConfig, StreamTargetChunk, VideoEncodingConfig} from "mediabunny"
|
|
4
4
|
|
|
5
|
+
import {Mat6} from "../../timeline/utils/matrix.js"
|
|
6
|
+
|
|
5
7
|
export type DriverSchematic = AsSchematic<{
|
|
6
8
|
|
|
7
9
|
// happens on the web worker
|
|
8
10
|
work: {
|
|
9
11
|
hello(): Promise<void>
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
decodeAudio(input: {
|
|
12
14
|
source: DecoderSource
|
|
13
|
-
video: WritableStream<VideoFrame>
|
|
14
15
|
audio: WritableStream<AudioData>
|
|
16
|
+
start?: number
|
|
17
|
+
end?: number
|
|
18
|
+
}): Promise<void>
|
|
19
|
+
|
|
20
|
+
decodeVideo(input: {
|
|
21
|
+
source: DecoderSource
|
|
22
|
+
video: WritableStream<VideoFrame>
|
|
23
|
+
start?: number
|
|
24
|
+
end?: number
|
|
15
25
|
}): Promise<void>
|
|
16
26
|
|
|
17
27
|
encode(input: EncoderInput & {bridge: WritableStream<StreamTargetChunk>}): Promise<void>
|
|
@@ -26,20 +36,22 @@ export type DriverSchematic = AsSchematic<{
|
|
|
26
36
|
}>
|
|
27
37
|
|
|
28
38
|
export interface EncoderInput {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
video?: ReadableStream<VideoFrame>
|
|
40
|
+
audio?: ReadableStream<AudioData>
|
|
41
|
+
config: RenderConfig
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RenderConfig {
|
|
45
|
+
video: VideoEncodingConfig
|
|
46
|
+
audio: AudioEncodingConfig
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
export type DecoderSource = Blob | string | URL
|
|
40
50
|
|
|
41
51
|
export interface DecoderInput {
|
|
42
52
|
source: DecoderSource
|
|
53
|
+
start?: number
|
|
54
|
+
end?: number
|
|
43
55
|
onFrame?: (frame: VideoFrame) => Promise<VideoFrame>
|
|
44
56
|
}
|
|
45
57
|
|
|
@@ -59,25 +71,36 @@ export interface MuxOpts {
|
|
|
59
71
|
|
|
60
72
|
export type Composition = Layer | (Layer | Composition)[]
|
|
61
73
|
|
|
62
|
-
export type Transform = {
|
|
63
|
-
x?: number
|
|
64
|
-
y?: number
|
|
65
|
-
scale?: number
|
|
66
|
-
opacity?: number
|
|
67
|
-
anchor?: number
|
|
68
|
-
}
|
|
69
|
-
|
|
70
74
|
export type TextLayer = {
|
|
71
75
|
kind: 'text'
|
|
72
76
|
content: string
|
|
73
77
|
fontSize?: number
|
|
74
78
|
color?: string
|
|
75
|
-
|
|
79
|
+
matrix?: Mat6
|
|
80
|
+
}
|
|
76
81
|
|
|
77
82
|
export type ImageLayer = {
|
|
78
83
|
kind: 'image'
|
|
79
84
|
frame: VideoFrame
|
|
80
|
-
|
|
85
|
+
matrix?: Mat6
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type TransitionLayer = {
|
|
89
|
+
kind: 'transition'
|
|
90
|
+
name: string
|
|
91
|
+
progress: number
|
|
92
|
+
from: VideoFrame
|
|
93
|
+
to: VideoFrame
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type GapLayer = {
|
|
97
|
+
kind: 'gap'
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type Audio = {
|
|
101
|
+
kind: "audio"
|
|
102
|
+
data: AudioData
|
|
103
|
+
}
|
|
81
104
|
|
|
82
|
-
export type Layer = TextLayer | ImageLayer
|
|
105
|
+
export type Layer = TextLayer | ImageLayer | TransitionLayer | GapLayer
|
|
83
106
|
|
package/s/driver/fns/work.ts
CHANGED
|
@@ -1,88 +1,86 @@
|
|
|
1
1
|
import {Comrade} from "@e280/comrade"
|
|
2
|
-
import {autoDetectRenderer, Container, Renderer, Sprite, Text, Texture, DOMAdapter, WebWorkerAdapter} from "pixi.js"
|
|
2
|
+
import {autoDetectRenderer, Container, Renderer, Sprite, Text, Texture, DOMAdapter, WebWorkerAdapter, Matrix} from "pixi.js"
|
|
3
3
|
import {Input, ALL_FORMATS, VideoSampleSink, Output, Mp4OutputFormat, VideoSampleSource, VideoSample, AudioSampleSink, AudioSampleSource, AudioSample, StreamTarget, BlobSource, UrlSource} from "mediabunny"
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {Mat6, mat6ToMatrix} from "../../timeline/utils/matrix.js"
|
|
6
|
+
import {makeTransition} from "../../features/transition/transition.js"
|
|
7
|
+
import {Composition, DecoderSource, DriverSchematic, Layer} from "./schematic.js"
|
|
6
8
|
|
|
7
9
|
DOMAdapter.set(WebWorkerAdapter)
|
|
8
10
|
|
|
11
|
+
const loadSource = async (source: DecoderSource) => {
|
|
12
|
+
if(source instanceof Blob) {
|
|
13
|
+
return new BlobSource(source)
|
|
14
|
+
} else {
|
|
15
|
+
return new UrlSource(source)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
export const setupDriverWork = (
|
|
10
20
|
Comrade.work<DriverSchematic>(shell => ({
|
|
11
21
|
async hello() {
|
|
12
22
|
await shell.host.world()
|
|
13
23
|
},
|
|
14
24
|
|
|
15
|
-
async
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
async decodeAudio({source, audio, start, end}) {
|
|
26
|
+
const input = new Input({
|
|
27
|
+
source: await loadSource(source),
|
|
28
|
+
formats: ALL_FORMATS
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const audioTrack = await input.getPrimaryAudioTrack()
|
|
32
|
+
const audioDecodable = await audioTrack?.canDecode()
|
|
33
|
+
const audioWriter = audio.getWriter()
|
|
34
|
+
|
|
35
|
+
if (audioDecodable && audioTrack) {
|
|
36
|
+
const sink = new AudioSampleSink(audioTrack)
|
|
37
|
+
for await (const sample of sink.samples(start, end)) {
|
|
38
|
+
const frame = sample.toAudioData()
|
|
39
|
+
await audioWriter.write(frame)
|
|
40
|
+
sample.close()
|
|
41
|
+
frame.close()
|
|
21
42
|
}
|
|
43
|
+
await audioWriter.close()
|
|
22
44
|
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
async decodeVideo({source, video, start, end}) {
|
|
23
48
|
const input = new Input({
|
|
24
|
-
source: await loadSource(),
|
|
49
|
+
source: await loadSource(source),
|
|
25
50
|
formats: ALL_FORMATS
|
|
26
51
|
})
|
|
27
52
|
|
|
28
|
-
const
|
|
29
|
-
input.getPrimaryVideoTrack(),
|
|
30
|
-
input.getPrimaryAudioTrack()
|
|
31
|
-
])
|
|
32
|
-
|
|
53
|
+
const videoTrack = await input.getPrimaryVideoTrack()
|
|
33
54
|
const videoDecodable = await videoTrack?.canDecode()
|
|
34
|
-
const audioDecodable = await audioTrack?.canDecode()
|
|
35
|
-
|
|
36
55
|
const videoWriter = video.getWriter()
|
|
37
|
-
const audioWriter = audio.getWriter()
|
|
38
56
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
await videoWriter.close()
|
|
50
|
-
}
|
|
51
|
-
})(),
|
|
52
|
-
(async () => {
|
|
53
|
-
if (audioDecodable && audioTrack) {
|
|
54
|
-
const sink = new AudioSampleSink(audioTrack)
|
|
55
|
-
for await (const sample of sink.samples()) {
|
|
56
|
-
const frame = sample.toAudioData()
|
|
57
|
-
await audioWriter.write(frame)
|
|
58
|
-
sample.close()
|
|
59
|
-
frame.close()
|
|
60
|
-
}
|
|
61
|
-
await audioWriter.close()
|
|
62
|
-
}
|
|
63
|
-
})()
|
|
64
|
-
])
|
|
57
|
+
if (videoDecodable && videoTrack) {
|
|
58
|
+
const sink = new VideoSampleSink(videoTrack)
|
|
59
|
+
for await (const sample of sink.samples(start, end)) {
|
|
60
|
+
const frame = sample.toVideoFrame()
|
|
61
|
+
await videoWriter.write(frame)
|
|
62
|
+
sample.close()
|
|
63
|
+
frame.close()
|
|
64
|
+
}
|
|
65
|
+
await videoWriter.close()
|
|
66
|
+
}
|
|
65
67
|
},
|
|
66
68
|
|
|
67
|
-
async encode({
|
|
69
|
+
async encode({video, audio, config, bridge}) {
|
|
68
70
|
const output = new Output({
|
|
69
71
|
format: new Mp4OutputFormat(),
|
|
70
72
|
target: new StreamTarget(bridge, {chunked: true})
|
|
71
73
|
})
|
|
72
|
-
const videoSource = new VideoSampleSource(config.video)
|
|
73
|
-
output.addVideoTrack(videoSource)
|
|
74
74
|
// since AudioSample is not transferable it fails to transfer encoder bitrate config
|
|
75
75
|
// so it needs to be hardcoded not set through constants eg QUALITY_LOW
|
|
76
|
-
const audioSource = new AudioSampleSource(config.audio)
|
|
77
|
-
output.addAudioTrack(audioSource)
|
|
78
76
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const videoReader = readables.video.getReader()
|
|
82
|
-
const audioReader = readables.audio.getReader()
|
|
77
|
+
const promises = []
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
if(video) {
|
|
80
|
+
const videoSource = new VideoSampleSource(config.video)
|
|
81
|
+
output.addVideoTrack(videoSource)
|
|
82
|
+
const videoReader = video.getReader()
|
|
83
|
+
promises.push((async () => {
|
|
86
84
|
while (true) {
|
|
87
85
|
const {done, value} = await videoReader.read()
|
|
88
86
|
if (done) break
|
|
@@ -90,8 +88,14 @@ export const setupDriverWork = (
|
|
|
90
88
|
await videoSource.add(sample)
|
|
91
89
|
sample.close()
|
|
92
90
|
}
|
|
93
|
-
})()
|
|
94
|
-
|
|
91
|
+
})())
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if(audio) {
|
|
95
|
+
const audioSource = new AudioSampleSource(config.audio)
|
|
96
|
+
output.addAudioTrack(audioSource)
|
|
97
|
+
const audioReader = audio.getReader()
|
|
98
|
+
promises.push((async () => {
|
|
95
99
|
while (true) {
|
|
96
100
|
const {done, value} = await audioReader.read()
|
|
97
101
|
if (done) break
|
|
@@ -100,9 +104,11 @@ export const setupDriverWork = (
|
|
|
100
104
|
sample.close()
|
|
101
105
|
value.close()
|
|
102
106
|
}
|
|
103
|
-
})()
|
|
104
|
-
|
|
107
|
+
})())
|
|
108
|
+
}
|
|
105
109
|
|
|
110
|
+
await output.start()
|
|
111
|
+
await Promise.all(promises)
|
|
106
112
|
await output.finalize()
|
|
107
113
|
},
|
|
108
114
|
|
|
@@ -110,22 +116,18 @@ export const setupDriverWork = (
|
|
|
110
116
|
const {stage, renderer} = await renderPIXI(1920, 1080)
|
|
111
117
|
stage.removeChildren()
|
|
112
118
|
|
|
113
|
-
const {
|
|
119
|
+
const {dispose} = await renderLayer(composition, stage)
|
|
114
120
|
renderer.render(stage)
|
|
115
121
|
|
|
116
122
|
// make sure browser support webgl/webgpu otherwise it might take much longer to construct frame
|
|
117
123
|
// if its very slow on eg edge try chrome
|
|
118
124
|
const frame = new VideoFrame(renderer.canvas, {
|
|
119
|
-
timestamp:
|
|
120
|
-
duration:
|
|
125
|
+
timestamp: 0,
|
|
126
|
+
duration: 0,
|
|
121
127
|
})
|
|
122
128
|
|
|
123
|
-
baseFrame?.close()
|
|
124
129
|
renderer.clear()
|
|
125
|
-
|
|
126
|
-
for (const disposable of disposables) {
|
|
127
|
-
disposable.destroy(true)
|
|
128
|
-
}
|
|
130
|
+
dispose()
|
|
129
131
|
|
|
130
132
|
shell.transfer = [frame]
|
|
131
133
|
return frame
|
|
@@ -157,46 +159,43 @@ async function renderPIXI(width: number, height: number) {
|
|
|
157
159
|
return pixi
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
const transitions: Map<string, ReturnType<typeof makeTransition>> = new Map()
|
|
163
|
+
|
|
160
164
|
type RenderableObject = Sprite | Text | Texture
|
|
161
165
|
|
|
162
166
|
async function renderLayer(
|
|
163
167
|
layer: Layer | Composition,
|
|
164
168
|
parent: Container,
|
|
165
|
-
disposables: RenderableObject[] = []
|
|
166
169
|
) {
|
|
167
170
|
if (Array.isArray(layer)) {
|
|
168
|
-
|
|
171
|
+
const disposers: (() => void)[] = []
|
|
169
172
|
for (const child of layer) {
|
|
170
|
-
const result = await renderLayer(child, parent
|
|
171
|
-
|
|
173
|
+
const result = await renderLayer(child, parent)
|
|
174
|
+
disposers.push(result.dispose)
|
|
172
175
|
}
|
|
173
|
-
return {
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (!isRenderableLayer(layer)) {
|
|
177
|
-
console.warn('Invalid layer', layer)
|
|
178
|
-
return {disposables}
|
|
176
|
+
return {dispose: () => disposers.forEach(d => d())}
|
|
179
177
|
}
|
|
180
178
|
|
|
181
179
|
switch (layer.kind) {
|
|
182
180
|
case 'text':
|
|
183
|
-
return renderTextLayer(layer, parent
|
|
181
|
+
return renderTextLayer(layer, parent)
|
|
184
182
|
case 'image':
|
|
185
|
-
return renderImageLayer(layer, parent
|
|
183
|
+
return renderImageLayer(layer, parent)
|
|
184
|
+
case 'transition':
|
|
185
|
+
return renderTransitionLayer(layer, parent)
|
|
186
|
+
case 'gap': {
|
|
187
|
+
pixi?.renderer.clear()
|
|
188
|
+
return {dispose: () => {}}
|
|
189
|
+
}
|
|
186
190
|
default:
|
|
187
191
|
console.warn('Unknown layer kind', (layer as any).kind)
|
|
188
|
-
return {
|
|
192
|
+
return {dispose: () => {}}
|
|
189
193
|
}
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
function isRenderableLayer(layer: any): layer is Layer {
|
|
193
|
-
return !!layer && typeof layer === 'object' && typeof layer.kind === 'string'
|
|
194
|
-
}
|
|
195
|
-
|
|
196
196
|
function renderTextLayer(
|
|
197
197
|
layer: Extract<Layer, {kind: 'text'}>,
|
|
198
198
|
parent: Container,
|
|
199
|
-
disposables: RenderableObject[]
|
|
200
199
|
) {
|
|
201
200
|
const text = new Text({
|
|
202
201
|
text: layer.content,
|
|
@@ -206,29 +205,45 @@ function renderTextLayer(
|
|
|
206
205
|
fill: layer.color ?? 'white'
|
|
207
206
|
}
|
|
208
207
|
})
|
|
209
|
-
applyTransform(text, layer)
|
|
208
|
+
applyTransform(text, layer.matrix)
|
|
210
209
|
parent.addChild(text)
|
|
211
|
-
|
|
212
|
-
return {disposables}
|
|
210
|
+
return {dispose: () => text.destroy(true)}
|
|
213
211
|
}
|
|
214
212
|
|
|
215
213
|
function renderImageLayer(
|
|
216
214
|
layer: Extract<Layer, {kind: 'image'}>,
|
|
217
215
|
parent: Container,
|
|
218
|
-
disposables: RenderableObject[]
|
|
219
216
|
) {
|
|
220
217
|
const texture = Texture.from(layer.frame)
|
|
221
218
|
const sprite = new Sprite(texture)
|
|
222
|
-
applyTransform(sprite, layer)
|
|
219
|
+
applyTransform(sprite, layer.matrix)
|
|
220
|
+
parent.addChild(sprite)
|
|
221
|
+
return {dispose: () => {
|
|
222
|
+
sprite.destroy(true)
|
|
223
|
+
texture.destroy(true)
|
|
224
|
+
layer.frame.close()
|
|
225
|
+
}}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function renderTransitionLayer(
|
|
229
|
+
{from, to, progress, name}: Extract<Layer, {kind: 'transition'}>,
|
|
230
|
+
parent: Container,
|
|
231
|
+
) {
|
|
232
|
+
const transition = transitions.get(name) ??
|
|
233
|
+
(transitions.set(name, makeTransition({
|
|
234
|
+
name: "circle",
|
|
235
|
+
renderer: pixi!.renderer
|
|
236
|
+
})),
|
|
237
|
+
transitions.get(name)!
|
|
238
|
+
)
|
|
239
|
+
const texture = transition.render({from, to, progress, width: from.displayWidth, height: from.displayHeight})
|
|
240
|
+
const sprite = new Sprite(texture)
|
|
223
241
|
parent.addChild(sprite)
|
|
224
|
-
|
|
225
|
-
return {baseFrame: layer.frame, disposables}
|
|
242
|
+
return {dispose: () => sprite.destroy(false)}
|
|
226
243
|
}
|
|
227
244
|
|
|
228
|
-
function applyTransform(target: Sprite | Text,
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if(t.opacity) target.alpha = t.opacity
|
|
233
|
-
if(t.anchor && 'anchor' in target) target.anchor.set(t.anchor)
|
|
245
|
+
function applyTransform(target: Sprite | Text, worldMatrix?: Mat6) {
|
|
246
|
+
if (!worldMatrix) return
|
|
247
|
+
const mx = mat6ToMatrix(worldMatrix)
|
|
248
|
+
target.setFromMatrix(mx)
|
|
234
249
|
}
|
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
|
-
<
|
|
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
|
}))
|
package/s/timeline/index.ts
CHANGED
|
@@ -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
|
+
}
|