@ain1084/audio-worklet-stream 0.1.2
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 +222 -0
- package/README.md +243 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +15 -0
- package/dist/constants.js.map +1 -0
- package/dist/events.d.ts +37 -0
- package/dist/events.js +35 -0
- package/dist/events.js.map +1 -0
- package/dist/frame-buffer/buffer-factory.d.ts +77 -0
- package/dist/frame-buffer/buffer-factory.js +52 -0
- package/dist/frame-buffer/buffer-factory.js.map +1 -0
- package/dist/frame-buffer/buffer-filler.d.ts +13 -0
- package/dist/frame-buffer/buffer-filler.js +2 -0
- package/dist/frame-buffer/buffer-filler.js.map +1 -0
- package/dist/frame-buffer/buffer-reader.d.ts +37 -0
- package/dist/frame-buffer/buffer-reader.js +51 -0
- package/dist/frame-buffer/buffer-reader.js.map +1 -0
- package/dist/frame-buffer/buffer-utils.d.ts +34 -0
- package/dist/frame-buffer/buffer-utils.js +34 -0
- package/dist/frame-buffer/buffer-utils.js.map +1 -0
- package/dist/frame-buffer/buffer-writer.d.ts +37 -0
- package/dist/frame-buffer/buffer-writer.js +51 -0
- package/dist/frame-buffer/buffer-writer.js.map +1 -0
- package/dist/frame-buffer/buffer.d.ts +40 -0
- package/dist/frame-buffer/buffer.js +65 -0
- package/dist/frame-buffer/buffer.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/output-message.d.ts +42 -0
- package/dist/output-message.js +2 -0
- package/dist/output-message.js.map +1 -0
- package/dist/output-stream-node.d.ts +93 -0
- package/dist/output-stream-node.js +166 -0
- package/dist/output-stream-node.js.map +1 -0
- package/dist/output-stream-processor.d.ts +13 -0
- package/dist/output-stream-processor.js +99 -0
- package/dist/output-stream-processor.js.map +1 -0
- package/dist/stream-node-factory.d.ts +87 -0
- package/dist/stream-node-factory.js +113 -0
- package/dist/stream-node-factory.js.map +1 -0
- package/dist/write-strategy/manual.d.ts +36 -0
- package/dist/write-strategy/manual.js +43 -0
- package/dist/write-strategy/manual.js.map +1 -0
- package/dist/write-strategy/strategy.d.ts +23 -0
- package/dist/write-strategy/strategy.js +2 -0
- package/dist/write-strategy/strategy.js.map +1 -0
- package/dist/write-strategy/timed.d.ts +36 -0
- package/dist/write-strategy/timed.js +92 -0
- package/dist/write-strategy/timed.js.map +1 -0
- package/dist/write-strategy/worker/message.d.ts +54 -0
- package/dist/write-strategy/worker/message.js +2 -0
- package/dist/write-strategy/worker/message.js.map +1 -0
- package/dist/write-strategy/worker/strategy.d.ts +34 -0
- package/dist/write-strategy/worker/strategy.js +125 -0
- package/dist/write-strategy/worker/strategy.js.map +1 -0
- package/dist/write-strategy/worker/worker.d.ts +35 -0
- package/dist/write-strategy/worker/worker.js +135 -0
- package/dist/write-strategy/worker/worker.js.map +1 -0
- package/package.json +54 -0
- package/src/constants.ts +18 -0
- package/src/events.ts +43 -0
- package/src/frame-buffer/buffer-factory.ts +115 -0
- package/src/frame-buffer/buffer-filler.ts +14 -0
- package/src/frame-buffer/buffer-reader.ts +56 -0
- package/src/frame-buffer/buffer-utils.ts +48 -0
- package/src/frame-buffer/buffer-writer.ts +56 -0
- package/src/frame-buffer/buffer.ts +68 -0
- package/src/index.ts +9 -0
- package/src/output-message.ts +37 -0
- package/src/output-stream-node.ts +197 -0
- package/src/output-stream-processor.ts +124 -0
- package/src/stream-node-factory.ts +161 -0
- package/src/write-strategy/manual.ts +50 -0
- package/src/write-strategy/strategy.ts +26 -0
- package/src/write-strategy/timed.ts +103 -0
- package/src/write-strategy/worker/message.ts +48 -0
- package/src/write-strategy/worker/strategy.ts +154 -0
- package/src/write-strategy/worker/worker.ts +149 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { FrameBufferReader } from './frame-buffer/buffer-reader'
|
|
2
|
+
import { PROCESSOR_NAME } from './constants'
|
|
3
|
+
import type { MessageToAudioNode, MessageToProcessor } from './output-message'
|
|
4
|
+
import { FrameBuffer } from './frame-buffer/buffer'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for the OutputStreamProcessor
|
|
8
|
+
* @property sampleBuffer - The shared buffer for audio data frames.
|
|
9
|
+
* @property samplesPerFrame - The number of samples per frame.
|
|
10
|
+
* @property usedFramesInBuffer - The usage count of the frames in the buffer.
|
|
11
|
+
* @property totalFrames - The current position in the buffer, in frames.
|
|
12
|
+
*/
|
|
13
|
+
export type OutputStreamProcessorOptions = Readonly<{
|
|
14
|
+
sampleBuffer: Float32Array
|
|
15
|
+
samplesPerFrame: number
|
|
16
|
+
usedFramesInBuffer: Uint32Array
|
|
17
|
+
totalFrames: BigUint64Array
|
|
18
|
+
}>
|
|
19
|
+
|
|
20
|
+
const createFrameBufferReader = (options: OutputStreamProcessorOptions) => {
|
|
21
|
+
return new FrameBufferReader(
|
|
22
|
+
new FrameBuffer(options.sampleBuffer, options.samplesPerFrame),
|
|
23
|
+
options.usedFramesInBuffer, options.totalFrames,
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* OutputStreamProcessor class
|
|
29
|
+
* This class extends AudioWorkletProcessor to process audio data.
|
|
30
|
+
* It uses a FrameBufferReader instance to read from a shared buffer and manage the audio data.
|
|
31
|
+
*/
|
|
32
|
+
class OutputStreamProcessor extends AudioWorkletProcessor {
|
|
33
|
+
private readonly _frameReader: FrameBufferReader
|
|
34
|
+
private _shouldStop = false
|
|
35
|
+
private _stopFrames: bigint | undefined
|
|
36
|
+
private _underrunFrames = 0
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Creates an instance of OutputStreamProcessor.
|
|
40
|
+
* @param options - The options for the processor, including shared buffers.
|
|
41
|
+
*/
|
|
42
|
+
constructor(options: AudioWorkletNodeOptions) {
|
|
43
|
+
super()
|
|
44
|
+
this._frameReader = createFrameBufferReader(options.processorOptions as OutputStreamProcessorOptions)
|
|
45
|
+
this.port.onmessage = this.handleMessage.bind(this)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Handles incoming messages from the audio node.
|
|
50
|
+
* @param event - The message event from the audio node.
|
|
51
|
+
*/
|
|
52
|
+
private handleMessage(event: MessageEvent<MessageToProcessor>): void {
|
|
53
|
+
if (event.data.type !== 'stop') {
|
|
54
|
+
throw new Error(`Unexpected message type: ${event.data.type}`)
|
|
55
|
+
}
|
|
56
|
+
const frames = event.data.frames
|
|
57
|
+
if (frames <= 0) {
|
|
58
|
+
this._shouldStop = true
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this._stopFrames = frames
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Updates the underrun frame count.
|
|
67
|
+
* If underrunFrames is provided and not zero, it adds to the current underrun frame count.
|
|
68
|
+
* If underrunFrames is 0, it indicates that the underrun state has been resolved,
|
|
69
|
+
* and the total underrun frame count is sent to the main thread before being reset.
|
|
70
|
+
* @param underrunFrames - The number of underrun frames to add (default is 0).
|
|
71
|
+
*/
|
|
72
|
+
private updateUnderrun(underrunFrames: number = 0): void {
|
|
73
|
+
if (underrunFrames !== 0) {
|
|
74
|
+
this._underrunFrames += underrunFrames
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
if (this._underrunFrames === 0) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
this.port.postMessage({ type: 'underrun', frames: this._underrunFrames } as MessageToAudioNode)
|
|
81
|
+
this._underrunFrames = 0
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Checks the stop condition.
|
|
86
|
+
* @param totalFrames - The total number of frames processed so far.
|
|
87
|
+
*/
|
|
88
|
+
private checkStopCondition(totalFrames: bigint) {
|
|
89
|
+
if (this._stopFrames !== undefined && totalFrames >= this._stopFrames) {
|
|
90
|
+
this._shouldStop = true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Processes the audio data.
|
|
96
|
+
* @param _inputs - The input audio data.
|
|
97
|
+
* @param outputs - The output audio data.
|
|
98
|
+
* @returns A boolean indicating whether to continue processing.
|
|
99
|
+
*/
|
|
100
|
+
process(_inputs: Float32Array[][], outputs: Float32Array[][]): boolean {
|
|
101
|
+
const output = outputs[0]
|
|
102
|
+
const readFrames = this._frameReader.read((frame, offset) => {
|
|
103
|
+
const frames = Math.min(frame.frames, output[0].length - offset)
|
|
104
|
+
frame.buffer.convertToOutput(frame.index, frames, output, offset)
|
|
105
|
+
return frames
|
|
106
|
+
})
|
|
107
|
+
const totalFrames = this._frameReader.totalFrames
|
|
108
|
+
this.checkStopCondition(totalFrames)
|
|
109
|
+
if (this._shouldStop) {
|
|
110
|
+
this.updateUnderrun()
|
|
111
|
+
this.port.postMessage({ type: 'stop', frames: totalFrames } as MessageToAudioNode)
|
|
112
|
+
this.port.close()
|
|
113
|
+
return false
|
|
114
|
+
}
|
|
115
|
+
const underrunFrames = output[0].length - readFrames
|
|
116
|
+
// It seems process may be called before connect?
|
|
117
|
+
if (totalFrames !== BigInt(0)) {
|
|
118
|
+
this.updateUnderrun(underrunFrames)
|
|
119
|
+
}
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
registerProcessor(PROCESSOR_NAME, OutputStreamProcessor)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import processor from './output-stream-processor?worker&url'
|
|
2
|
+
import { ManualBufferWriteStrategy } from './write-strategy/manual'
|
|
3
|
+
import { FrameBufferFactory } from './frame-buffer/buffer-factory'
|
|
4
|
+
import type { OutputStreamNode, OutputStreamNodeFactory } from './output-stream-node'
|
|
5
|
+
import { TimedBufferWriteStrategy } from './write-strategy/timed'
|
|
6
|
+
import type { FrameBufferFiller } from './frame-buffer/buffer-filler'
|
|
7
|
+
import { FrameBufferWriter } from './frame-buffer/buffer-writer'
|
|
8
|
+
import { WorkerBufferWriteStrategy } from './write-strategy/worker/strategy'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parameters for creating a manual buffer node.
|
|
12
|
+
*/
|
|
13
|
+
export type ManualBufferNodeParams = Readonly<{
|
|
14
|
+
/** The number of audio channels. */
|
|
15
|
+
channelCount: number
|
|
16
|
+
/** The size of the frame buffer in frames. */
|
|
17
|
+
frameBufferSize: number
|
|
18
|
+
}>
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parameters for creating a timed buffer node.
|
|
22
|
+
*/
|
|
23
|
+
export type TimedBufferNodeParams = Readonly<{
|
|
24
|
+
/** The number of audio channels. */
|
|
25
|
+
channelCount: number
|
|
26
|
+
/** Optional. The number of chunks in the frame buffer. */
|
|
27
|
+
frameBufferChunks?: number
|
|
28
|
+
/** Optional. The interval in milliseconds for filling the buffer. */
|
|
29
|
+
fillInterval?: number
|
|
30
|
+
/** Optional. The sample rate of the audio context. */
|
|
31
|
+
sampleRate?: number
|
|
32
|
+
}>
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parameters for creating a worker buffer node.
|
|
36
|
+
*/
|
|
37
|
+
export type WorkerBufferNodeParams<T> = TimedBufferNodeParams & Readonly<{
|
|
38
|
+
/** Parameters specific to the filler used in the worker. */
|
|
39
|
+
fillerParams: T
|
|
40
|
+
}>
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* StreamNodeFactory class
|
|
44
|
+
* Factory class to create instances of OutputStreamNode with specific BufferWriteStrategy.
|
|
45
|
+
* This class provides methods to create different types of OutputStreamNodes,
|
|
46
|
+
* such as those using manual, timed, or worker-based buffer writing strategies.
|
|
47
|
+
*/
|
|
48
|
+
export class StreamNodeFactory {
|
|
49
|
+
private _audioContext: AudioContext
|
|
50
|
+
private readonly _outputStreamNodeFactory: typeof OutputStreamNodeFactory
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Constructor for StreamNodeFactory.
|
|
54
|
+
* @param context - The AudioContext to use.
|
|
55
|
+
* @param outputStreamNodeFactory - The factory for creating OutputStreamNode instances.
|
|
56
|
+
*/
|
|
57
|
+
private constructor(context: AudioContext, outputStreamNodeFactory: typeof OutputStreamNodeFactory) {
|
|
58
|
+
this._audioContext = context
|
|
59
|
+
this._outputStreamNodeFactory = outputStreamNodeFactory
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create an instance of StreamNodeFactory.
|
|
64
|
+
* This method loads necessary modules and creates node creators.
|
|
65
|
+
* @param context - The AudioContext to use.
|
|
66
|
+
* @returns A promise that resolves to an instance of StreamNodeFactory.
|
|
67
|
+
* @throws Error - If module loading fails.
|
|
68
|
+
*/
|
|
69
|
+
public static async create(context: AudioContext): Promise<StreamNodeFactory> {
|
|
70
|
+
try {
|
|
71
|
+
const loadResults = await Promise.all([
|
|
72
|
+
context.audioWorklet.addModule(processor),
|
|
73
|
+
import('./output-stream-node'),
|
|
74
|
+
])
|
|
75
|
+
return new StreamNodeFactory(context, loadResults[1].OutputStreamNodeFactory)
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new Error('Failed to load modules: ' + error)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates an OutputStreamNode with manual buffer writing strategy.
|
|
84
|
+
* @param params - The parameters for manual buffer node creation.
|
|
85
|
+
* @returns A promise that resolves to an OutputStreamNode and FrameBufferWriter instance.
|
|
86
|
+
*/
|
|
87
|
+
public async createManualBufferNode(params: ManualBufferNodeParams): Promise<[OutputStreamNode, FrameBufferWriter]> {
|
|
88
|
+
if (params.channelCount <= 0 || !Number.isInteger(params.channelCount)) {
|
|
89
|
+
throw new Error('Invalid channelCount: must be a positive integer.')
|
|
90
|
+
}
|
|
91
|
+
if (params.frameBufferSize <= 0 || !Number.isInteger(params.frameBufferSize)) {
|
|
92
|
+
throw new Error('Invalid frameBufferSize: must be a positive integer.')
|
|
93
|
+
}
|
|
94
|
+
const bufferConfig = FrameBufferFactory.createFrameBufferConfig({ ...params })
|
|
95
|
+
const strategy = new ManualBufferWriteStrategy(bufferConfig)
|
|
96
|
+
return [await this._outputStreamNodeFactory.create(this.audioContext, bufferConfig, strategy), strategy.writer]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Creates an OutputStreamNode with timed buffer writing strategy.
|
|
101
|
+
* @param frameFiller - The FrameBufferFiller instance.
|
|
102
|
+
* @param params - The parameters for timed buffer node creation.
|
|
103
|
+
* @returns A promise that resolves to an OutputStreamNode instance.
|
|
104
|
+
*/
|
|
105
|
+
public async createTimedBufferNode(
|
|
106
|
+
frameFiller: FrameBufferFiller, params: TimedBufferNodeParams,
|
|
107
|
+
): Promise<OutputStreamNode> {
|
|
108
|
+
StreamNodeFactory.validateTimedBufferNodeParams(params)
|
|
109
|
+
if (typeof frameFiller !== 'object' || frameFiller === null) {
|
|
110
|
+
throw new Error('Invalid frameFiller: must be an object.')
|
|
111
|
+
}
|
|
112
|
+
const config = FrameBufferFactory.createFillerFrameBufferConfig(this._audioContext.sampleRate, { ...params })
|
|
113
|
+
const strategy = new TimedBufferWriteStrategy(config, frameFiller)
|
|
114
|
+
return this._outputStreamNodeFactory.create(this.audioContext, config, strategy)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates an OutputStreamNode with worker buffer writing strategy.
|
|
119
|
+
* @param worker - The worker instance.
|
|
120
|
+
* @param params - The parameters for worker buffer node creation.
|
|
121
|
+
* @returns A promise that resolves to an OutputStreamNode instance.
|
|
122
|
+
*/
|
|
123
|
+
public async createWorkerBufferNode<FillerParams>(
|
|
124
|
+
worker: new () => Worker, params: WorkerBufferNodeParams<FillerParams>,
|
|
125
|
+
): Promise<OutputStreamNode> {
|
|
126
|
+
StreamNodeFactory.validateTimedBufferNodeParams(params)
|
|
127
|
+
const config = FrameBufferFactory.createFillerFrameBufferConfig(this._audioContext.sampleRate, { ...params })
|
|
128
|
+
const strategy = new WorkerBufferWriteStrategy<FillerParams>(config, worker, params.fillerParams)
|
|
129
|
+
return this._outputStreamNodeFactory.create(this._audioContext, config, strategy)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Validates the parameters for timed buffer node creation.
|
|
134
|
+
* @param params - The parameters to validate.
|
|
135
|
+
* @throws Error - If validation fails.
|
|
136
|
+
*/
|
|
137
|
+
private static validateTimedBufferNodeParams(params: TimedBufferNodeParams): void {
|
|
138
|
+
// Check if 'channelCount' is a positive integer
|
|
139
|
+
if (!Number.isInteger(params.channelCount) || params.channelCount <= 0) {
|
|
140
|
+
throw new Error('Invalid channelCount: must be a positive integer.')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check if 'fillInterval' is a positive number if provided
|
|
144
|
+
if (params.fillInterval !== undefined && (typeof params.fillInterval !== 'number' || params.fillInterval <= 0)) {
|
|
145
|
+
throw new Error('Invalid fillInterval: must be a positive number.')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check if 'frameBufferChunks' is a positive integer if provided
|
|
149
|
+
if (params.frameBufferChunks !== undefined && (!Number.isInteger(params.frameBufferChunks) || params.frameBufferChunks <= 0)) {
|
|
150
|
+
throw new Error('Invalid frameBufferChunks: must be a positive integer.')
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get the AudioContext associated with this factory.
|
|
156
|
+
* @returns The AudioContext instance.
|
|
157
|
+
*/
|
|
158
|
+
public get audioContext(): BaseAudioContext {
|
|
159
|
+
return this._audioContext
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { BufferWriteStrategy } from './strategy'
|
|
2
|
+
import { createFrameBufferWriter, FrameBufferConfig } from '../frame-buffer/buffer-factory'
|
|
3
|
+
import { FrameBufferWriter } from '../frame-buffer/buffer-writer'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ManualBufferWriteStrategy class
|
|
7
|
+
* Implements the BufferWriteStrategy interface for manually writing to the buffer.
|
|
8
|
+
*/
|
|
9
|
+
export class ManualBufferWriteStrategy implements BufferWriteStrategy {
|
|
10
|
+
private readonly _writer: FrameBufferWriter
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates an instance of ManualBufferWriteStrategy.
|
|
14
|
+
* @param config - The configuration for the frame buffer.
|
|
15
|
+
*/
|
|
16
|
+
constructor(config: FrameBufferConfig) {
|
|
17
|
+
this._writer = createFrameBufferWriter(config)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Gets the FrameBufferWriter instance.
|
|
22
|
+
* @returns The FrameBufferWriter instance.
|
|
23
|
+
*/
|
|
24
|
+
public get writer(): FrameBufferWriter {
|
|
25
|
+
return this._writer
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Initializes the strategy.
|
|
30
|
+
* @param node - The OutputStreamNode instance.
|
|
31
|
+
* @returns A promise that resolves to true when initialization is complete.
|
|
32
|
+
*/
|
|
33
|
+
async onInit(/* node: OutputStreamNode */): Promise<boolean> {
|
|
34
|
+
return true
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Starts the strategy.
|
|
39
|
+
* @param node - The OutputStreamNode instance.
|
|
40
|
+
* @returns A boolean indicating whether the strategy started successfully.
|
|
41
|
+
*/
|
|
42
|
+
onStart(/* node: OutputStreamNode */): boolean {
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Stops the strategy.
|
|
48
|
+
*/
|
|
49
|
+
onStopped(): void {}
|
|
50
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { OutputStreamNode } from '../output-stream-node'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BufferWriteStrategy interface
|
|
5
|
+
* Defines the methods for buffer writing strategies.
|
|
6
|
+
*/
|
|
7
|
+
export interface BufferWriteStrategy {
|
|
8
|
+
/**
|
|
9
|
+
* Initializes the strategy.
|
|
10
|
+
* @param node - The OutputStreamNode instance.
|
|
11
|
+
* @returns A promise that resolves to true when initialization is complete.
|
|
12
|
+
*/
|
|
13
|
+
onInit(node: OutputStreamNode): Promise<boolean>
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Starts the strategy.
|
|
17
|
+
* @param node - The OutputStreamNode instance.
|
|
18
|
+
* @returns A boolean indicating whether the strategy started successfully.
|
|
19
|
+
*/
|
|
20
|
+
onStart(node: OutputStreamNode): boolean
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Stops the strategy.
|
|
24
|
+
*/
|
|
25
|
+
onStopped(): void
|
|
26
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { BufferWriteStrategy } from './strategy'
|
|
2
|
+
import { createFrameBufferWriter, FillerFrameBufferConfig } from '../frame-buffer/buffer-factory'
|
|
3
|
+
import { FrameBufferWriter } from '../frame-buffer/buffer-writer'
|
|
4
|
+
import { FrameBufferFiller } from '../frame-buffer/buffer-filler'
|
|
5
|
+
import { OutputStreamNode } from '../output-stream-node'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PlayContext class
|
|
9
|
+
* Manages the buffer filling process using a timer.
|
|
10
|
+
*/
|
|
11
|
+
class PlayContext {
|
|
12
|
+
private _timerId: number = 0
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates an instance of PlayContext.
|
|
16
|
+
* @param node - The OutputStreamNode instance.
|
|
17
|
+
* @param writer - The FrameBufferWriter instance.
|
|
18
|
+
* @param filler - The FrameBufferFiller instance.
|
|
19
|
+
* @param fillInterval - The interval in milliseconds for filling the buffer.
|
|
20
|
+
*/
|
|
21
|
+
constructor(node: OutputStreamNode, writer: FrameBufferWriter, filler: FrameBufferFiller, fillInterval: number) {
|
|
22
|
+
this._timerId = window.setInterval(() => {
|
|
23
|
+
if (!this._timerId || !node.isStart) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
if (!filler.fill(writer)) {
|
|
28
|
+
node.stop(writer.totalFrames)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
this.cleanup()
|
|
33
|
+
node.stop()
|
|
34
|
+
}
|
|
35
|
+
}, fillInterval)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Cleans up the timer.
|
|
40
|
+
*/
|
|
41
|
+
public cleanup() {
|
|
42
|
+
if (this._timerId) {
|
|
43
|
+
clearInterval(this._timerId)
|
|
44
|
+
this._timerId = 0
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* TimedBufferWriteStrategy class
|
|
51
|
+
* Implements the BufferWriteStrategy interface using a timer to manage buffer filling.
|
|
52
|
+
*/
|
|
53
|
+
export class TimedBufferWriteStrategy implements BufferWriteStrategy {
|
|
54
|
+
private readonly _writer: FrameBufferWriter
|
|
55
|
+
private readonly _filler: FrameBufferFiller
|
|
56
|
+
private readonly _interval: number
|
|
57
|
+
private readonly _isContinuePlayback: boolean
|
|
58
|
+
private _context: PlayContext | null = null
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Creates an instance of TimedBufferWriteStrategy.
|
|
62
|
+
* @param config - The configuration for the filler frame buffer.
|
|
63
|
+
* @param filler - The FrameBufferFiller instance.
|
|
64
|
+
*/
|
|
65
|
+
constructor(config: FillerFrameBufferConfig, filler: FrameBufferFiller) {
|
|
66
|
+
this._writer = createFrameBufferWriter(config)
|
|
67
|
+
this._filler = filler
|
|
68
|
+
this._interval = config.fillInterval
|
|
69
|
+
this._isContinuePlayback = this._filler.fill(this._writer)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Initializes the strategy.
|
|
74
|
+
* @returns A promise that resolves to true when initialization is complete.
|
|
75
|
+
*/
|
|
76
|
+
async onInit(/* node: OutputStreamNode */): Promise<boolean> {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Starts the strategy.
|
|
82
|
+
* @param node - The OutputStreamNode instance.
|
|
83
|
+
* @returns A boolean indicating whether the strategy started successfully.
|
|
84
|
+
*/
|
|
85
|
+
onStart(node: OutputStreamNode): boolean {
|
|
86
|
+
if (this._context) {
|
|
87
|
+
throw new Error('Invalid state: context is not null.')
|
|
88
|
+
}
|
|
89
|
+
if (!this._isContinuePlayback) {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
this._context = new PlayContext(node, this._writer, this._filler, this._interval)
|
|
93
|
+
return true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Stops the strategy.
|
|
98
|
+
*/
|
|
99
|
+
onStopped(): void {
|
|
100
|
+
this._context?.cleanup()
|
|
101
|
+
this._context = null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { FillerFrameBufferConfig } from '../../frame-buffer/buffer-factory'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Message sent from the main thread to the worker.
|
|
5
|
+
*
|
|
6
|
+
* @typeParam FillerParams - The type of the parameters for the FrameBufferFiller.
|
|
7
|
+
*
|
|
8
|
+
* For 'init' messages:
|
|
9
|
+
* @property config - The configuration for the filler frame buffer.
|
|
10
|
+
* @property fillerParams - The parameters for the FrameBufferFiller.
|
|
11
|
+
*
|
|
12
|
+
* For 'start' messages:
|
|
13
|
+
*
|
|
14
|
+
* For 'stop' messages:
|
|
15
|
+
*
|
|
16
|
+
* Example:
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const message: MessageToWorker<YourFillerParams> = {
|
|
19
|
+
* type: 'init',
|
|
20
|
+
* config: yourConfig,
|
|
21
|
+
* fillerParams: yourFillerParams,
|
|
22
|
+
* };
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export type MessageToWorker<FillerParams> =
|
|
26
|
+
| { type: 'init', config: FillerFrameBufferConfig, fillerParams: FillerParams }
|
|
27
|
+
| { type: 'start' }
|
|
28
|
+
| { type: 'stop' }
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Message sent from the worker to the main thread.
|
|
32
|
+
*
|
|
33
|
+
* For 'init-done' messages:
|
|
34
|
+
*
|
|
35
|
+
* For 'stop' messages:
|
|
36
|
+
* @property stopFrames - The position in frames to stop at.
|
|
37
|
+
*
|
|
38
|
+
* Example:
|
|
39
|
+
* ```typescript
|
|
40
|
+
* const message: MessageToStrategy = {
|
|
41
|
+
* type: 'stop',
|
|
42
|
+
* stopFrames: 1000n,
|
|
43
|
+
* };
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export type MessageToStrategy =
|
|
47
|
+
| { type: 'init-done' }
|
|
48
|
+
| { type: 'stop', stopFrames: bigint }
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { BufferWriteStrategy } from '../strategy'
|
|
2
|
+
import type { FillerFrameBufferConfig } from '../../frame-buffer/buffer-factory'
|
|
3
|
+
import type { MessageToStrategy, MessageToWorker } from './message'
|
|
4
|
+
import type { OutputStreamNode } from '../../output-stream-node'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parameters for creating a PlayContext.
|
|
8
|
+
* @property node - The OutputStreamNode instance.
|
|
9
|
+
* @property config - The FillerFrameBufferConfig instance.
|
|
10
|
+
* @property workerConstructor - The constructor for the Worker.
|
|
11
|
+
* @property fillerParam - The parameters for the FrameBufferFiller.
|
|
12
|
+
*/
|
|
13
|
+
type PlayerContextParams<FillerParams> = Readonly<{
|
|
14
|
+
node: OutputStreamNode
|
|
15
|
+
config: FillerFrameBufferConfig
|
|
16
|
+
workerConstructor: new () => Worker
|
|
17
|
+
fillerParam: FillerParams
|
|
18
|
+
}>
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* PlayContext class
|
|
22
|
+
* Manages the buffer filling process within a web worker.
|
|
23
|
+
*/
|
|
24
|
+
class Context<FillerParams> {
|
|
25
|
+
private readonly _node: OutputStreamNode
|
|
26
|
+
private readonly _config: FillerFrameBufferConfig
|
|
27
|
+
private readonly _fillerParam: FillerParams
|
|
28
|
+
private readonly _worker: Worker
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates an instance of PlayContext.
|
|
32
|
+
* @param params - The parameters for the PlayContext.
|
|
33
|
+
*/
|
|
34
|
+
private constructor(params: PlayerContextParams<FillerParams>) {
|
|
35
|
+
this._node = params.node
|
|
36
|
+
this._config = params.config
|
|
37
|
+
this._worker = new params.workerConstructor()
|
|
38
|
+
this._fillerParam = params.fillerParam
|
|
39
|
+
this._worker.onmessage = this.handleWorkerMessage.bind(this)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates and initializes an instance of PlayContext.
|
|
44
|
+
* @param params - The parameters for the PlayContext.
|
|
45
|
+
* @returns A promise that resolves to an instance of PlayContext.
|
|
46
|
+
*/
|
|
47
|
+
public static async create<FillerParams>(params: PlayerContextParams<FillerParams>) {
|
|
48
|
+
const instance = new Context(params)
|
|
49
|
+
await instance.init()
|
|
50
|
+
return instance
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handles messages from the worker.
|
|
55
|
+
* @param ev - The message event from the worker.
|
|
56
|
+
*/
|
|
57
|
+
private handleWorkerMessage(ev: MessageEvent<MessageToStrategy>): void {
|
|
58
|
+
if (ev.data.type === 'stop') {
|
|
59
|
+
this._node.stop(ev.data.stopFrames)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Initializes the PlayContext by sending the 'init' message to the worker.
|
|
65
|
+
* @returns A promise that resolves when initialization is complete.
|
|
66
|
+
*/
|
|
67
|
+
private init() {
|
|
68
|
+
return new Promise<void>((resolve) => {
|
|
69
|
+
const message: MessageToWorker<FillerParams> = {
|
|
70
|
+
type: 'init',
|
|
71
|
+
config: this._config,
|
|
72
|
+
fillerParams: this._fillerParam,
|
|
73
|
+
}
|
|
74
|
+
this._worker.postMessage(message)
|
|
75
|
+
const listener = (ev: MessageEvent<MessageToStrategy>) => {
|
|
76
|
+
if (ev.data.type === 'init-done') {
|
|
77
|
+
this._worker.removeEventListener('message', listener)
|
|
78
|
+
resolve()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
this._worker.addEventListener('message', listener)
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Starts the buffer filling process by sending the 'start' message to the worker.
|
|
87
|
+
*/
|
|
88
|
+
public start() {
|
|
89
|
+
const message: MessageToWorker<FillerParams> = {
|
|
90
|
+
type: 'start',
|
|
91
|
+
}
|
|
92
|
+
this._worker.postMessage(message)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Stops the worker and terminates it.
|
|
97
|
+
*/
|
|
98
|
+
public stopped() {
|
|
99
|
+
this._worker.onmessage = null
|
|
100
|
+
this._worker.terminate()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* WorkerBufferWriteStrategy class
|
|
106
|
+
* Implements the BufferWriteStrategy interface using a web worker to manage buffer filling.
|
|
107
|
+
*/
|
|
108
|
+
export class WorkerBufferWriteStrategy<FillerParam> implements BufferWriteStrategy {
|
|
109
|
+
private readonly _createPlayContext: (node: OutputStreamNode) => Promise<Context<FillerParam>>
|
|
110
|
+
private _context: Context<FillerParam> | null = null
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates an instance of WorkerBufferWriteStrategy.
|
|
114
|
+
* @param config - The configuration for the filler frame buffer.
|
|
115
|
+
* @param workerConstructor - The constructor for the Worker.
|
|
116
|
+
* @param fillerParam - The parameters for the FrameBufferFiller.
|
|
117
|
+
*/
|
|
118
|
+
constructor(config: FillerFrameBufferConfig, workerConstructor: new () => Worker, fillerParam: FillerParam) {
|
|
119
|
+
this._createPlayContext = (node: OutputStreamNode) => Context.create<FillerParam>({ node, config, workerConstructor, fillerParam })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Initializes the strategy.
|
|
124
|
+
* @param node - The OutputStreamNode instance.
|
|
125
|
+
* @returns A promise that resolves to true when initialization is complete.
|
|
126
|
+
*/
|
|
127
|
+
async onInit(node: OutputStreamNode): Promise<boolean> {
|
|
128
|
+
this._context = await this._createPlayContext(node)
|
|
129
|
+
return true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Starts the strategy.
|
|
134
|
+
* @param node - The OutputStreamNode instance.
|
|
135
|
+
* @returns A boolean indicating whether the strategy started successfully.
|
|
136
|
+
*/
|
|
137
|
+
onStart(/* node: OutputStreamNode */): boolean {
|
|
138
|
+
if (!this._context) {
|
|
139
|
+
throw new Error('Invalid state: context is null.')
|
|
140
|
+
}
|
|
141
|
+
this._context.start()
|
|
142
|
+
return true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Stops the strategy.
|
|
147
|
+
*/
|
|
148
|
+
onStopped(): void {
|
|
149
|
+
if (!this._context) {
|
|
150
|
+
throw new Error('Invalid state: context is null.')
|
|
151
|
+
}
|
|
152
|
+
this._context.stopped()
|
|
153
|
+
}
|
|
154
|
+
}
|