@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.
Files changed (79) hide show
  1. package/LICENSE +222 -0
  2. package/README.md +243 -0
  3. package/dist/constants.d.ts +12 -0
  4. package/dist/constants.js +15 -0
  5. package/dist/constants.js.map +1 -0
  6. package/dist/events.d.ts +37 -0
  7. package/dist/events.js +35 -0
  8. package/dist/events.js.map +1 -0
  9. package/dist/frame-buffer/buffer-factory.d.ts +77 -0
  10. package/dist/frame-buffer/buffer-factory.js +52 -0
  11. package/dist/frame-buffer/buffer-factory.js.map +1 -0
  12. package/dist/frame-buffer/buffer-filler.d.ts +13 -0
  13. package/dist/frame-buffer/buffer-filler.js +2 -0
  14. package/dist/frame-buffer/buffer-filler.js.map +1 -0
  15. package/dist/frame-buffer/buffer-reader.d.ts +37 -0
  16. package/dist/frame-buffer/buffer-reader.js +51 -0
  17. package/dist/frame-buffer/buffer-reader.js.map +1 -0
  18. package/dist/frame-buffer/buffer-utils.d.ts +34 -0
  19. package/dist/frame-buffer/buffer-utils.js +34 -0
  20. package/dist/frame-buffer/buffer-utils.js.map +1 -0
  21. package/dist/frame-buffer/buffer-writer.d.ts +37 -0
  22. package/dist/frame-buffer/buffer-writer.js +51 -0
  23. package/dist/frame-buffer/buffer-writer.js.map +1 -0
  24. package/dist/frame-buffer/buffer.d.ts +40 -0
  25. package/dist/frame-buffer/buffer.js +65 -0
  26. package/dist/frame-buffer/buffer.js.map +1 -0
  27. package/dist/index.d.ts +6 -0
  28. package/dist/index.js +4 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/output-message.d.ts +42 -0
  31. package/dist/output-message.js +2 -0
  32. package/dist/output-message.js.map +1 -0
  33. package/dist/output-stream-node.d.ts +93 -0
  34. package/dist/output-stream-node.js +166 -0
  35. package/dist/output-stream-node.js.map +1 -0
  36. package/dist/output-stream-processor.d.ts +13 -0
  37. package/dist/output-stream-processor.js +99 -0
  38. package/dist/output-stream-processor.js.map +1 -0
  39. package/dist/stream-node-factory.d.ts +87 -0
  40. package/dist/stream-node-factory.js +113 -0
  41. package/dist/stream-node-factory.js.map +1 -0
  42. package/dist/write-strategy/manual.d.ts +36 -0
  43. package/dist/write-strategy/manual.js +43 -0
  44. package/dist/write-strategy/manual.js.map +1 -0
  45. package/dist/write-strategy/strategy.d.ts +23 -0
  46. package/dist/write-strategy/strategy.js +2 -0
  47. package/dist/write-strategy/strategy.js.map +1 -0
  48. package/dist/write-strategy/timed.d.ts +36 -0
  49. package/dist/write-strategy/timed.js +92 -0
  50. package/dist/write-strategy/timed.js.map +1 -0
  51. package/dist/write-strategy/worker/message.d.ts +54 -0
  52. package/dist/write-strategy/worker/message.js +2 -0
  53. package/dist/write-strategy/worker/message.js.map +1 -0
  54. package/dist/write-strategy/worker/strategy.d.ts +34 -0
  55. package/dist/write-strategy/worker/strategy.js +125 -0
  56. package/dist/write-strategy/worker/strategy.js.map +1 -0
  57. package/dist/write-strategy/worker/worker.d.ts +35 -0
  58. package/dist/write-strategy/worker/worker.js +135 -0
  59. package/dist/write-strategy/worker/worker.js.map +1 -0
  60. package/package.json +54 -0
  61. package/src/constants.ts +18 -0
  62. package/src/events.ts +43 -0
  63. package/src/frame-buffer/buffer-factory.ts +115 -0
  64. package/src/frame-buffer/buffer-filler.ts +14 -0
  65. package/src/frame-buffer/buffer-reader.ts +56 -0
  66. package/src/frame-buffer/buffer-utils.ts +48 -0
  67. package/src/frame-buffer/buffer-writer.ts +56 -0
  68. package/src/frame-buffer/buffer.ts +68 -0
  69. package/src/index.ts +9 -0
  70. package/src/output-message.ts +37 -0
  71. package/src/output-stream-node.ts +197 -0
  72. package/src/output-stream-processor.ts +124 -0
  73. package/src/stream-node-factory.ts +161 -0
  74. package/src/write-strategy/manual.ts +50 -0
  75. package/src/write-strategy/strategy.ts +26 -0
  76. package/src/write-strategy/timed.ts +103 -0
  77. package/src/write-strategy/worker/message.ts +48 -0
  78. package/src/write-strategy/worker/strategy.ts +154 -0
  79. 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
+ }