@omnimedia/omnitool 1.1.0-52 → 1.1.0-55
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 +1 -1
- package/s/demo/routines/transcode-test.ts +2 -2
- package/s/demo/routines/transitions-test.ts +2 -2
- package/s/driver/driver.ts +26 -5
- package/s/driver/fns/schematic.ts +2 -0
- package/s/driver/fns/work.ts +41 -20
- package/s/timeline/parts/waveform.ts +1 -1
- package/s/timeline/renderers/export/parts/cursor.ts +75 -56
- package/s/timeline/renderers/export/parts/produce-video.ts +2 -3
- package/s/timeline/renderers/player/parts/playback.ts +33 -6
- package/s/timeline/renderers/player/player.ts +2 -14
- package/s/timeline/renderers/renderers.test.ts +1 -3
- package/s/timeline/sugar/o.ts +10 -10
- package/x/demo/demo.bundle.min.js +9 -9
- package/x/demo/demo.bundle.min.js.map +4 -4
- package/x/demo/routines/transcode-test.js +2 -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 +14 -2
- package/x/driver/driver.js +26 -5
- package/x/driver/driver.js.map +1 -1
- package/x/driver/driver.worker.bundle.min.js +1 -1
- package/x/driver/driver.worker.bundle.min.js.map +3 -3
- package/x/driver/fns/host.d.ts +2 -0
- package/x/driver/fns/schematic.d.ts +2 -0
- package/x/driver/fns/work.d.ts +2 -0
- package/x/driver/fns/work.js +33 -20
- package/x/driver/fns/work.js.map +1 -1
- package/x/index.html +2 -2
- package/x/tests.bundle.min.js +16 -16
- package/x/tests.bundle.min.js.map +4 -4
- package/x/tests.html +1 -1
- package/x/timeline/parts/waveform.js +1 -1
- package/x/timeline/parts/waveform.js.map +1 -1
- package/x/timeline/renderers/export/parts/cursor.d.ts +4 -5
- package/x/timeline/renderers/export/parts/cursor.js +68 -50
- package/x/timeline/renderers/export/parts/cursor.js.map +1 -1
- package/x/timeline/renderers/export/parts/produce-video.js +2 -3
- package/x/timeline/renderers/export/parts/produce-video.js.map +1 -1
- package/x/timeline/renderers/player/parts/playback.d.ts +9 -5
- package/x/timeline/renderers/player/parts/playback.js +25 -6
- package/x/timeline/renderers/player/parts/playback.js.map +1 -1
- package/x/timeline/renderers/player/player.js +2 -9
- package/x/timeline/renderers/player/player.js.map +1 -1
- package/x/timeline/renderers/renderers.test.js +1 -2
- package/x/timeline/renderers/renderers.test.js.map +1 -1
- package/x/timeline/sugar/o.d.ts +1 -0
- package/x/timeline/sugar/o.js +10 -10
- package/x/timeline/sugar/o.js.map +1 -1
package/package.json
CHANGED
|
@@ -38,8 +38,8 @@ export function setupTranscodeTest(driver: Driver, source: DecoderSource) {
|
|
|
38
38
|
const audio = driver.decodeAudio({source})
|
|
39
39
|
|
|
40
40
|
const {readable, done} = driver.encode({
|
|
41
|
-
video,
|
|
42
|
-
audio,
|
|
41
|
+
video: video.readable,
|
|
42
|
+
audio: audio.readable,
|
|
43
43
|
config: {
|
|
44
44
|
audio: {codec: "opus", bitrate: 128000},
|
|
45
45
|
video: {codec: "vp9", bitrate: 1000000}
|
|
@@ -30,8 +30,8 @@ export async function setupTransitionsTest(driver: Driver, source: DecoderSource
|
|
|
30
30
|
}
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
video,
|
|
33
|
+
driver.encode({
|
|
34
|
+
video: video.readable,
|
|
35
35
|
config: {
|
|
36
36
|
audio: {codec: "opus", bitrate: 128000},
|
|
37
37
|
video: {codec: "vp9", bitrate: 1000000}
|
package/s/driver/driver.ts
CHANGED
|
@@ -58,33 +58,54 @@ export class Driver {
|
|
|
58
58
|
|
|
59
59
|
decodeVideo(input: DecoderInput) {
|
|
60
60
|
let lastFrame: VideoFrame | null = null
|
|
61
|
+
const {port1, port2} = new MessageChannel()
|
|
61
62
|
const videoTransform = new TransformStream<VideoFrame, VideoFrame>({
|
|
62
63
|
async transform(chunk, controller) {
|
|
63
64
|
const frame = await input.onFrame?.(chunk) ?? chunk
|
|
64
|
-
// below code is to prevent mem leaks and hardware accelerated decoder stall
|
|
65
65
|
lastFrame?.close()
|
|
66
66
|
controller.enqueue(frame)
|
|
67
67
|
lastFrame = frame
|
|
68
68
|
}
|
|
69
69
|
})
|
|
70
|
-
this.thread.work.decodeVideo[tune]({transfer: [videoTransform.writable]})({
|
|
70
|
+
this.thread.work.decodeVideo[tune]({transfer: [videoTransform.writable, port2]})({
|
|
71
71
|
source: input.source,
|
|
72
|
+
cancel: port2,
|
|
72
73
|
video: videoTransform.writable,
|
|
73
74
|
start: input.start,
|
|
74
75
|
end: input.end
|
|
75
76
|
})
|
|
76
|
-
return
|
|
77
|
+
return {
|
|
78
|
+
readable: videoTransform.readable,
|
|
79
|
+
/**
|
|
80
|
+
* use this to stop decoding (premature interruption)
|
|
81
|
+
* */
|
|
82
|
+
cancel() {
|
|
83
|
+
port1.postMessage("close")
|
|
84
|
+
port1.close()
|
|
85
|
+
}
|
|
86
|
+
}
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
decodeAudio(input: DecoderInput) {
|
|
80
90
|
const audioTransform = new TransformStream<AudioData, AudioData>()
|
|
81
|
-
|
|
91
|
+
const {port1, port2} = new MessageChannel()
|
|
92
|
+
this.thread.work.decodeAudio[tune]({transfer: [audioTransform.writable, port2]})({
|
|
82
93
|
source: input.source,
|
|
94
|
+
cancel: port2,
|
|
83
95
|
audio: audioTransform.writable,
|
|
84
96
|
start: input.start,
|
|
85
97
|
end: input.end
|
|
86
98
|
})
|
|
87
|
-
return
|
|
99
|
+
return {
|
|
100
|
+
readable: audioTransform.readable,
|
|
101
|
+
/**
|
|
102
|
+
* use this to stop decoding (premature interruption)
|
|
103
|
+
* */
|
|
104
|
+
cancel() {
|
|
105
|
+
port1.postMessage("close")
|
|
106
|
+
port1.close()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
88
109
|
}
|
|
89
110
|
|
|
90
111
|
encode({audio, video, config}: EncoderInput) {
|
|
@@ -14,6 +14,7 @@ export type DriverSchematic = AsSchematic<{
|
|
|
14
14
|
|
|
15
15
|
decodeAudio(input: {
|
|
16
16
|
source: DecoderSource
|
|
17
|
+
cancel: MessagePort
|
|
17
18
|
audio: WritableStream<AudioData>
|
|
18
19
|
start?: number
|
|
19
20
|
end?: number
|
|
@@ -21,6 +22,7 @@ export type DriverSchematic = AsSchematic<{
|
|
|
21
22
|
|
|
22
23
|
decodeVideo(input: {
|
|
23
24
|
source: DecoderSource
|
|
25
|
+
cancel: MessagePort
|
|
24
26
|
video: WritableStream<VideoFrame>
|
|
25
27
|
start?: number
|
|
26
28
|
end?: number
|
package/s/driver/fns/work.ts
CHANGED
|
@@ -20,7 +20,7 @@ export const setupDriverWork = (
|
|
|
20
20
|
await shell.host.world()
|
|
21
21
|
},
|
|
22
22
|
|
|
23
|
-
async decodeAudio({source, audio, start, end}) {
|
|
23
|
+
async decodeAudio({source, audio, start, end, cancel}) {
|
|
24
24
|
const input = new Input({
|
|
25
25
|
source: await loadSource(source),
|
|
26
26
|
formats: ALL_FORMATS
|
|
@@ -30,19 +30,29 @@ export const setupDriverWork = (
|
|
|
30
30
|
const audioDecodable = await audioTrack?.canDecode()
|
|
31
31
|
const audioWriter = audio.getWriter()
|
|
32
32
|
|
|
33
|
-
if
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
if(!audioDecodable || !audioTrack)
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
const sink = new AudioSampleSink(audioTrack)
|
|
37
|
+
const samples = sink.samples(start, end)
|
|
38
|
+
|
|
39
|
+
cancel.onmessage = async () => {
|
|
40
|
+
samples.return()
|
|
41
|
+
input.dispose()
|
|
42
|
+
cancel.close()
|
|
42
43
|
}
|
|
44
|
+
|
|
45
|
+
for await (const sample of samples) {
|
|
46
|
+
const frame = sample.toAudioData()
|
|
47
|
+
sample.close()
|
|
48
|
+
await audioWriter.write(frame)
|
|
49
|
+
frame.close()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
await audioWriter.close()
|
|
43
53
|
},
|
|
44
54
|
|
|
45
|
-
async decodeVideo({source, video, start, end}) {
|
|
55
|
+
async decodeVideo({source, video, start, end, cancel}) {
|
|
46
56
|
const input = new Input({
|
|
47
57
|
source: await loadSource(source),
|
|
48
58
|
formats: ALL_FORMATS
|
|
@@ -52,16 +62,26 @@ export const setupDriverWork = (
|
|
|
52
62
|
const videoDecodable = await videoTrack?.canDecode()
|
|
53
63
|
const videoWriter = video.getWriter()
|
|
54
64
|
|
|
55
|
-
if
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
if(!videoDecodable || !videoTrack)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
const sink = new VideoSampleSink(videoTrack)
|
|
69
|
+
const samples = sink.samples(start, end)
|
|
70
|
+
|
|
71
|
+
cancel.onmessage = async () => {
|
|
72
|
+
samples.return()
|
|
73
|
+
input.dispose()
|
|
74
|
+
cancel.close()
|
|
64
75
|
}
|
|
76
|
+
|
|
77
|
+
for await (const sample of samples) {
|
|
78
|
+
const frame = sample.toVideoFrame()
|
|
79
|
+
sample.close()
|
|
80
|
+
await videoWriter.write(frame)
|
|
81
|
+
frame.close()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await videoWriter.close()
|
|
65
85
|
},
|
|
66
86
|
|
|
67
87
|
async encode({video, audio, config, writable}) {
|
|
@@ -78,6 +98,7 @@ export const setupDriverWork = (
|
|
|
78
98
|
const sample = new VideoSample(frame)
|
|
79
99
|
await videoSource.add(sample)
|
|
80
100
|
sample.close()
|
|
101
|
+
frame.close()
|
|
81
102
|
}
|
|
82
103
|
}
|
|
83
104
|
|
|
@@ -20,7 +20,7 @@ export class Waveform {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
static async init(driver: Driver, source: DecoderSource, container: HTMLElement) {
|
|
23
|
-
const reader = driver.decodeAudio({source}).getReader()
|
|
23
|
+
const reader = driver.decodeAudio({source}).readable.getReader()
|
|
24
24
|
|
|
25
25
|
const peaks: number[] = []
|
|
26
26
|
let buffer: number[] = []
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
|
|
2
2
|
import {ms, Ms} from "../../../../units/ms.js"
|
|
3
|
-
import {Item} from "../../../parts/item.js"
|
|
4
3
|
import {Driver} from "../../../../driver/driver.js"
|
|
5
4
|
import {TimelineFile} from "../../../parts/basics.js"
|
|
6
5
|
import {DecoderSource} from "../../../../driver/fns/schematic.js"
|
|
@@ -13,87 +12,107 @@ import {createVisualSampler} from "../../parts/samplers/visual/sampler.js"
|
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
export class CursorVisualSampler {
|
|
16
|
-
#
|
|
15
|
+
#lastTimecode = -Infinity
|
|
17
16
|
#videoCursors = new Map<number, VideoFrameCursor>()
|
|
17
|
+
#sampler
|
|
18
18
|
|
|
19
19
|
constructor(
|
|
20
20
|
private driver: Driver,
|
|
21
|
-
private resolveMedia: (hash: string) => DecoderSource
|
|
21
|
+
private resolveMedia: (hash: string) => DecoderSource,
|
|
22
|
+
private timeline: TimelineFile
|
|
22
23
|
) {
|
|
23
|
-
this.#sampler = createVisualSampler(resolveMedia, (item, time) => {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
this.#sampler = createVisualSampler(this.resolveMedia, (item, time) => {
|
|
25
|
+
const targetUs = toUs(time)
|
|
26
|
+
let cursor = this.#videoCursors.get(item.id)
|
|
27
|
+
|
|
28
|
+
if (!cursor) {
|
|
29
|
+
const source = this.resolveMedia(item.mediaHash)
|
|
30
|
+
const endUs = toUs(ms(item.start + item.duration))
|
|
31
|
+
cursor = this.#createVideoCursor(source, targetUs, endUs)
|
|
32
|
+
this.#videoCursors.set(item.id, cursor)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return cursor.next(targetUs)
|
|
27
36
|
})
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
next: (timecode: Ms) => {
|
|
34
|
-
if (timecode < lastTimecode)
|
|
35
|
-
throw new Error(`CursorVisualSampler is forward-only: requested ${timecode}ms after ${lastTimecode}ms`)
|
|
36
|
-
lastTimecode = timecode
|
|
37
|
-
return this.#sampler.sample(timeline, timecode)
|
|
38
|
-
},
|
|
39
|
-
cancel: () => this.#cancel(),
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
#getCursorForVideo(videoItem: Item.Video) {
|
|
44
|
-
const existing = this.#videoCursors.get(videoItem.id)
|
|
45
|
-
if (existing)
|
|
46
|
-
return existing
|
|
47
|
-
|
|
48
|
-
const source = this.resolveMedia(videoItem.mediaHash)
|
|
49
|
-
const video = this.driver.decodeVideo({ source })
|
|
50
|
-
const cursor = this.#cursor(video.getReader())
|
|
39
|
+
next(timecode: Ms) {
|
|
40
|
+
if (timecode < this.#lastTimecode)
|
|
41
|
+
throw new Error(`Forward-only cursor regression: ${timecode}ms < ${this.#lastTimecode}ms`)
|
|
51
42
|
|
|
52
|
-
this.#
|
|
53
|
-
return
|
|
43
|
+
this.#lastTimecode = timecode
|
|
44
|
+
return this.#sampler.sample(this.timeline, timecode)
|
|
54
45
|
}
|
|
55
46
|
|
|
56
|
-
async
|
|
57
|
-
await Promise.all(
|
|
58
|
-
[...this.#videoCursors.values()].map(cursor => cursor.cancel())
|
|
59
|
-
)
|
|
47
|
+
async cancel() {
|
|
48
|
+
await Promise.all([...this.#videoCursors.values()].map(c => c.cancel()))
|
|
60
49
|
this.#videoCursors.clear()
|
|
61
50
|
}
|
|
62
51
|
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
#createVideoCursor(source: DecoderSource, startUs: number, endUs: number): VideoFrameCursor {
|
|
53
|
+
const video = this.driver.decodeVideo({source, start: startUs / 1_000_000, end: endUs / 1_000_000})
|
|
54
|
+
const reader = video.readable.getReader()
|
|
55
|
+
|
|
56
|
+
let current: VideoFrame | null = null
|
|
57
|
+
let nextPromise: Promise<VideoFrame | null> | null = null
|
|
58
|
+
let ended = false
|
|
59
|
+
|
|
60
|
+
const readNext = async () => {
|
|
61
|
+
if (ended) return null
|
|
62
|
+
const {done, value} = await reader.read()
|
|
63
|
+
if (done) return (ended = true, null)
|
|
64
|
+
|
|
65
|
+
const frame = new VideoFrame(value)
|
|
66
|
+
value.close()
|
|
67
|
+
return frame
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
return {
|
|
66
71
|
async next(targetUs: number): Promise<VideoFrame | undefined> {
|
|
67
|
-
|
|
72
|
+
current ??= await readNext()
|
|
73
|
+
if (!current) return undefined
|
|
74
|
+
|
|
68
75
|
while (true) {
|
|
69
|
-
|
|
76
|
+
nextPromise ??= readNext()
|
|
77
|
+
const nextFrame = await nextPromise
|
|
70
78
|
|
|
71
|
-
if (
|
|
72
|
-
const out = prev ? new VideoFrame(prev) : undefined
|
|
73
|
-
prev?.close()
|
|
74
|
-
return out
|
|
75
|
-
}
|
|
79
|
+
if (!nextFrame) return new VideoFrame(current)
|
|
76
80
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const prevUs = prev?.timestamp ?? Number.NEGATIVE_INFINITY
|
|
80
|
-
const usePrev = !!prev && Math.abs(prevUs - targetUs) < Math.abs(hitUs - targetUs)
|
|
81
|
+
const currentUs = current.timestamp ?? -Infinity
|
|
82
|
+
const nextUs = nextFrame.timestamp ?? currentUs
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
if (nextUs < targetUs) {
|
|
85
|
+
current.close()
|
|
86
|
+
current = nextFrame
|
|
87
|
+
nextPromise = null
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const useNext = Math.abs(nextUs - targetUs) < Math.abs(currentUs - targetUs)
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
if (useNext) {
|
|
94
|
+
current.close()
|
|
95
|
+
current = nextFrame
|
|
96
|
+
nextPromise = null
|
|
97
|
+
continue
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
|
|
92
|
-
prev = hit
|
|
100
|
+
return new VideoFrame(current)
|
|
93
101
|
}
|
|
94
102
|
},
|
|
95
103
|
|
|
96
|
-
|
|
104
|
+
async cancel() {
|
|
105
|
+
const pending = nextPromise
|
|
106
|
+
nextPromise = null
|
|
107
|
+
|
|
108
|
+
current?.close()
|
|
109
|
+
current = null
|
|
110
|
+
|
|
111
|
+
video.cancel()
|
|
112
|
+
|
|
113
|
+
const buffered = await pending?.catch(() => null)
|
|
114
|
+
buffered?.close()
|
|
115
|
+
}
|
|
97
116
|
}
|
|
98
117
|
}
|
|
99
118
|
}
|
|
@@ -21,14 +21,13 @@ export function produceVideo({
|
|
|
21
21
|
|
|
22
22
|
const stream = new TransformStream<VideoFrame, VideoFrame>()
|
|
23
23
|
const writer = stream.writable.getWriter()
|
|
24
|
-
const sampler = new CursorVisualSampler(driver, resolveMedia)
|
|
25
|
-
const cursor = sampler.cursor(timeline)
|
|
24
|
+
const sampler = new CursorVisualSampler(driver, resolveMedia, timeline)
|
|
26
25
|
const dt = 1 / fps
|
|
27
26
|
const duration = computeItemDuration(timeline.rootId, timeline)
|
|
28
27
|
|
|
29
28
|
async function produce() {
|
|
30
29
|
await fixedStep({fps, duration}, async (timecode, i) => {
|
|
31
|
-
const layers = await
|
|
30
|
+
const layers = await sampler.next(timecode)
|
|
32
31
|
const composed = await driver.composite(layers)
|
|
33
32
|
|
|
34
33
|
const frame = new VideoFrame(composed, {
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
|
|
2
|
-
import {pub} from '@e280/stz'
|
|
3
2
|
import {Fps} from '../../../../units/fps.js'
|
|
4
3
|
import {ms, Ms} from '../../../../units/ms.js'
|
|
4
|
+
import {Driver} from '../../../../driver/driver.js'
|
|
5
5
|
import {realtime} from '../../parts/schedulers.js'
|
|
6
6
|
import {TimelineFile} from '../../../parts/basics.js'
|
|
7
|
+
import {computeItemDuration} from '../../parts/handy.js'
|
|
7
8
|
import {seconds, Seconds} from '../../../../units/seconds.js'
|
|
9
|
+
import {CursorVisualSampler} from '../../export/parts/cursor.js'
|
|
8
10
|
import {DecoderSource} from '../../../../driver/fns/schematic.js'
|
|
9
11
|
import {createAudioSampler} from '../../parts/samplers/audio/sampler.js'
|
|
10
12
|
import {createVisualSampler} from '../../parts/samplers/visual/sampler.js'
|
|
11
13
|
|
|
12
14
|
export class Playback {
|
|
13
|
-
visualSampler
|
|
14
15
|
audioSampler
|
|
16
|
+
seekVisualSampler
|
|
17
|
+
playVisualSampler: CursorVisualSampler | null = null
|
|
15
18
|
|
|
16
19
|
#playbackStart = ms(0)
|
|
17
20
|
#audioStartSec: number | null = null
|
|
@@ -25,28 +28,39 @@ export class Playback {
|
|
|
25
28
|
#audioAbort: AbortController | null = null
|
|
26
29
|
|
|
27
30
|
constructor(
|
|
31
|
+
private driver: Driver,
|
|
28
32
|
private timeline: TimelineFile,
|
|
29
33
|
private resolveMedia: (hash: string) => DecoderSource
|
|
30
34
|
) {
|
|
31
35
|
this.audioGain.connect(this.audioContext.destination)
|
|
32
36
|
this.audioGain.gain.value = 0.7 ** 2
|
|
33
|
-
this.
|
|
37
|
+
this.seekVisualSampler = createVisualSampler(this.resolveMedia)
|
|
34
38
|
this.audioSampler = createAudioSampler(this.resolveMedia)
|
|
39
|
+
this.#samples()
|
|
35
40
|
}
|
|
36
41
|
|
|
37
|
-
async
|
|
42
|
+
async #samples() {
|
|
38
43
|
for await (const _ of this.#controller.ticks()) {
|
|
39
|
-
|
|
44
|
+
const layers = await this.playVisualSampler?.next(this.currentTime) ?? []
|
|
45
|
+
|
|
46
|
+
const frame = await this.driver.composite(layers)
|
|
47
|
+
frame.close()
|
|
48
|
+
|
|
49
|
+
if (this.currentTime >= this.duration)
|
|
50
|
+
this.pause()
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
async seek(time: Ms) {
|
|
44
55
|
this.pause()
|
|
45
56
|
this.#playbackStart = time
|
|
46
|
-
return await this.
|
|
57
|
+
return await this.seekVisualSampler.sample(this.timeline, time)
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
async start(timeline: TimelineFile) {
|
|
61
|
+
if(this.#controller.isPlaying())
|
|
62
|
+
return
|
|
63
|
+
|
|
50
64
|
this.timeline = timeline
|
|
51
65
|
await this.audioContext.resume()
|
|
52
66
|
|
|
@@ -61,6 +75,9 @@ export class Playback {
|
|
|
61
75
|
|
|
62
76
|
this.audioNodes.clear()
|
|
63
77
|
|
|
78
|
+
this.playVisualSampler?.cancel()
|
|
79
|
+
this.playVisualSampler = new CursorVisualSampler(this.driver, this.resolveMedia, this.timeline)
|
|
80
|
+
|
|
64
81
|
this.#controller.play()
|
|
65
82
|
this.#startAudio(this.#audioAbort.signal, seconds(this.#playbackStart / 1000))
|
|
66
83
|
}
|
|
@@ -74,6 +91,16 @@ export class Playback {
|
|
|
74
91
|
node.stop()
|
|
75
92
|
|
|
76
93
|
this.audioNodes.clear()
|
|
94
|
+
|
|
95
|
+
this.playVisualSampler?.cancel()
|
|
96
|
+
this.playVisualSampler = null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get duration() {
|
|
100
|
+
return computeItemDuration(
|
|
101
|
+
this.timeline.rootId,
|
|
102
|
+
this.timeline
|
|
103
|
+
)
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
get currentTime() {
|
|
@@ -4,7 +4,6 @@ import {fps} from "../../../units/fps.js"
|
|
|
4
4
|
import {Playback} from "./parts/playback.js"
|
|
5
5
|
import {Driver} from "../../../driver/driver.js"
|
|
6
6
|
import {TimelineFile} from "../../parts/basics.js"
|
|
7
|
-
import {computeItemDuration} from "../parts/handy.js"
|
|
8
7
|
import {DecoderSource} from "../../../driver/fns/schematic.js"
|
|
9
8
|
|
|
10
9
|
type ResolveMedia = (hash: string) => DecoderSource
|
|
@@ -18,20 +17,12 @@ export class VideoPlayer {
|
|
|
18
17
|
resolveMedia: ResolveMedia,
|
|
19
18
|
private timeline: TimelineFile,
|
|
20
19
|
) {
|
|
21
|
-
this.playback = new Playback(timeline, resolveMedia)
|
|
20
|
+
this.playback = new Playback(driver, timeline, resolveMedia)
|
|
22
21
|
this.canvas = driver.compositor.pixi.renderer.canvas
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
async play() {
|
|
26
25
|
await this.playback.start(this.timeline)
|
|
27
|
-
|
|
28
|
-
for await (const layers of this.playback.samples()) {
|
|
29
|
-
const frame = await this.driver.composite(layers)
|
|
30
|
-
frame.close()
|
|
31
|
-
|
|
32
|
-
if (this.currentTime >= this.duration)
|
|
33
|
-
this.pause()
|
|
34
|
-
}
|
|
35
26
|
}
|
|
36
27
|
|
|
37
28
|
pause() {
|
|
@@ -49,10 +40,7 @@ export class VideoPlayer {
|
|
|
49
40
|
}
|
|
50
41
|
|
|
51
42
|
get duration() {
|
|
52
|
-
return
|
|
53
|
-
this.timeline.rootId,
|
|
54
|
-
this.timeline
|
|
55
|
-
)
|
|
43
|
+
return this.playback.duration
|
|
56
44
|
}
|
|
57
45
|
|
|
58
46
|
get currentTime() {
|
|
@@ -49,8 +49,7 @@ export default Science.suite({
|
|
|
49
49
|
o.video(videoA, {duration: 2000}),
|
|
50
50
|
o.audio(videoA, {duration: 500}),
|
|
51
51
|
))})
|
|
52
|
-
const
|
|
53
|
-
const cursor = sampler.cursor(timeline)
|
|
52
|
+
const cursor = new CursorVisualSampler(driver, resolveMedia, timeline)
|
|
54
53
|
await cursor.next(ms(1000))
|
|
55
54
|
await expect(async () => await cursor.next(ms(100))).throwsAsync()
|
|
56
55
|
}),
|
|
@@ -384,4 +383,3 @@ export default Science.suite({
|
|
|
384
383
|
expect(totalFrames).is(240000)
|
|
385
384
|
})
|
|
386
385
|
})
|
|
387
|
-
|
package/s/timeline/sugar/o.ts
CHANGED
|
@@ -20,7 +20,7 @@ export class O {
|
|
|
20
20
|
return this.state.timeline
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
getId() {
|
|
24
24
|
return hex.toInteger(hex.random())
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -37,7 +37,7 @@ export class O {
|
|
|
37
37
|
|
|
38
38
|
textStyle = (style: TextStyleOptions): Item.TextStyle => {
|
|
39
39
|
const item = {
|
|
40
|
-
id: this
|
|
40
|
+
id: this.getId(),
|
|
41
41
|
kind: Kind.TextStyle,
|
|
42
42
|
style
|
|
43
43
|
} as Item.TextStyle
|
|
@@ -47,7 +47,7 @@ export class O {
|
|
|
47
47
|
|
|
48
48
|
spatial = (transform: Transform): Item.Spatial => {
|
|
49
49
|
const item: Item.Spatial = {
|
|
50
|
-
id: this
|
|
50
|
+
id: this.getId(),
|
|
51
51
|
kind: Kind.Spatial,
|
|
52
52
|
transform,
|
|
53
53
|
enabled: true
|
|
@@ -58,7 +58,7 @@ export class O {
|
|
|
58
58
|
|
|
59
59
|
sequence = (...items: Item.Any[]): Item.Sequence => {
|
|
60
60
|
const item = {
|
|
61
|
-
id: this
|
|
61
|
+
id: this.getId(),
|
|
62
62
|
kind: Kind.Sequence,
|
|
63
63
|
childrenIds: items.map(item => item.id)
|
|
64
64
|
} as Item.Sequence
|
|
@@ -69,7 +69,7 @@ export class O {
|
|
|
69
69
|
stack = (...items: Item.Any[]): Item.Stack => {
|
|
70
70
|
const item = {
|
|
71
71
|
kind: Kind.Stack,
|
|
72
|
-
id: this
|
|
72
|
+
id: this.getId(),
|
|
73
73
|
childrenIds: items.map(item => item.id)
|
|
74
74
|
} as Item.Stack
|
|
75
75
|
this.register(item)
|
|
@@ -88,7 +88,7 @@ export class O {
|
|
|
88
88
|
|
|
89
89
|
const item: Item.Video = {
|
|
90
90
|
kind: Kind.Video,
|
|
91
|
-
id: this
|
|
91
|
+
id: this.getId(),
|
|
92
92
|
mediaHash: media.datafile.checksum.hash,
|
|
93
93
|
start: options?.start ?? 0,
|
|
94
94
|
duration: options?.duration ?? media.duration
|
|
@@ -110,7 +110,7 @@ export class O {
|
|
|
110
110
|
|
|
111
111
|
const item: Item.Audio = {
|
|
112
112
|
kind: Kind.Audio,
|
|
113
|
-
id: this
|
|
113
|
+
id: this.getId(),
|
|
114
114
|
mediaHash: media.datafile.checksum.hash,
|
|
115
115
|
start: options?.start ?? 0,
|
|
116
116
|
duration: options?.duration ?? media.duration,
|
|
@@ -126,7 +126,7 @@ export class O {
|
|
|
126
126
|
}): Item.Text => {
|
|
127
127
|
|
|
128
128
|
const item = {
|
|
129
|
-
id: this
|
|
129
|
+
id: this.getId(),
|
|
130
130
|
content,
|
|
131
131
|
kind: Kind.Text,
|
|
132
132
|
duration: options?.duration ?? 2000
|
|
@@ -141,7 +141,7 @@ export class O {
|
|
|
141
141
|
|
|
142
142
|
gap = (duration: number): Item.Gap => {
|
|
143
143
|
const item = {
|
|
144
|
-
id: this
|
|
144
|
+
id: this.getId(),
|
|
145
145
|
kind: Kind.Gap,
|
|
146
146
|
duration
|
|
147
147
|
} as Item.Gap
|
|
@@ -152,7 +152,7 @@ export class O {
|
|
|
152
152
|
transition = {
|
|
153
153
|
crossfade: (duration: number): Item.Transition => {
|
|
154
154
|
const item = {
|
|
155
|
-
id: this
|
|
155
|
+
id: this.getId(),
|
|
156
156
|
kind: Kind.Transition,
|
|
157
157
|
effect: Effect.Crossfade,
|
|
158
158
|
duration,
|