2
2
* External dependencies
3
3
*/
4
4
import { useRef , useState , useEffect , useCallback } from '@wordpress/element' ;
5
-
6
5
/*
7
6
* Types
8
7
*/
9
- type RecordingStateProp = 'inactive' | 'recording' | 'paused' ;
8
+ type RecordingStateProp = 'inactive' | 'recording' | 'paused' | 'processing' | 'error' ;
10
9
type UseMediaRecordingProps = {
11
- onDone ?: ( blob : Blob ) => void ;
10
+ onDone ?: ( blob : Blob , url : string ) => void ;
12
11
} ;
13
12
14
13
type UseMediaRecordingReturn = {
@@ -27,6 +26,21 @@ type UseMediaRecordingReturn = {
27
26
*/
28
27
url : string | null ;
29
28
29
+ /**
30
+ * The error message
31
+ */
32
+ error : string | null ;
33
+
34
+ /**
35
+ * The duration of the recorded audio
36
+ */
37
+ duration : number ;
38
+
39
+ /**
40
+ * The error handler
41
+ */
42
+ onError : ( err : string | Error ) => void ;
43
+
30
44
controls : {
31
45
/**
32
46
* `start` recording handler
@@ -47,6 +61,11 @@ type UseMediaRecordingReturn = {
47
61
* `stop` recording handler
48
62
*/
49
63
stop : ( ) => void ;
64
+
65
+ /**
66
+ * `reset` recording handler
67
+ */
68
+ reset : ( ) => void ;
50
69
} ;
51
70
} ;
52
71
@@ -66,15 +85,25 @@ export default function useMediaRecording( {
66
85
// Reference to the media recorder instance
67
86
const mediaRecordRef = useRef ( null ) ;
68
87
69
- // Recording state: `inactive`, `recording`, `paused`
88
+ // Recording state: `inactive`, `recording`, `paused`, `processing`, `error`
70
89
const [ state , setState ] = useState < RecordingStateProp > ( 'inactive' ) ;
71
90
91
+ // reference to the paused state to be used in the `onDataAvailable` event listener,
92
+ // as the `mediaRecordRef.current.state` is already `inactive` when the recorder is stopped,
93
+ // and the event listener does not react to state changes
94
+ const isPaused = useRef < boolean > ( false ) ;
95
+
96
+ const recordStartTimestamp = useRef < number > ( 0 ) ;
97
+ const [ duration , setDuration ] = useState < number > ( 0 ) ;
98
+
72
99
// The recorded blob
73
100
const [ blob , setBlob ] = useState < Blob | null > ( null ) ;
74
101
75
102
// Store the recorded chunks
76
103
const recordedChunks = useRef < Array < Blob > > ( [ ] ) . current ;
77
104
105
+ const [ error , setError ] = useState < string | null > ( null ) ;
106
+
78
107
/**
79
108
* Get the recorded blob.
80
109
*
@@ -88,29 +117,114 @@ export default function useMediaRecording( {
88
117
89
118
// `start` recording handler
90
119
const start = useCallback ( ( timeslice : number ) => {
120
+ clearData ( ) ;
121
+
91
122
if ( ! timeslice ) {
92
123
return mediaRecordRef ?. current ?. start ( ) ;
93
124
}
94
125
95
126
if ( timeslice < 100 ) {
96
127
timeslice = 100 ; // set minimum timeslice to 100ms
97
128
}
129
+
130
+ // Record the start time
131
+ recordStartTimestamp . current = Date . now ( ) ;
132
+
98
133
mediaRecordRef ?. current ?. start ( timeslice ) ;
99
134
} , [ ] ) ;
100
135
101
136
// `pause` recording handler
102
137
const pause = useCallback ( ( ) => {
138
+ isPaused . current = true ;
103
139
mediaRecordRef ?. current ?. pause ( ) ;
140
+
141
+ // Calculate the duration of the recorded audio from the start time
142
+ setDuration ( currentDuration => currentDuration + Date . now ( ) - recordStartTimestamp . current ) ;
104
143
} , [ ] ) ;
105
144
106
145
// `resume` recording handler
107
146
const resume = useCallback ( ( ) => {
147
+ isPaused . current = false ;
108
148
mediaRecordRef ?. current ?. resume ( ) ;
149
+
150
+ // Record the start time
151
+ recordStartTimestamp . current = Date . now ( ) ;
109
152
} , [ ] ) ;
110
153
111
154
// `stop` recording handler
112
155
const stop = useCallback ( ( ) => {
113
156
mediaRecordRef ?. current ?. stop ( ) ;
157
+
158
+ if ( state === 'recording' ) {
159
+ // Calculate the duration of the recorded audio from the start time
160
+ setDuration ( currentDuration => currentDuration + Date . now ( ) - recordStartTimestamp . current ) ;
161
+ }
162
+ } , [ ] ) ;
163
+
164
+ // clears the recording state
165
+ const clearData = useCallback ( ( ) => {
166
+ recordedChunks . length = 0 ;
167
+ setBlob ( null ) ;
168
+ setError ( null ) ;
169
+ setDuration ( 0 ) ;
170
+ isPaused . current = false ;
171
+ recordStartTimestamp . current = 0 ;
172
+ } , [ ] ) ;
173
+
174
+ // removes the event listeners
175
+ const clearListeners = useCallback ( ( ) => {
176
+ /*
177
+ * mediaRecordRef is not defined when
178
+ * the getUserMedia API is not supported,
179
+ * or when the user has not granted access
180
+ */
181
+ if ( ! mediaRecordRef ?. current ) {
182
+ return ;
183
+ }
184
+
185
+ mediaRecordRef . current . removeEventListener ( 'start' , onStartListener ) ;
186
+ mediaRecordRef . current . removeEventListener ( 'stop' , onStopListener ) ;
187
+ mediaRecordRef . current . removeEventListener ( 'pause' , onPauseListener ) ;
188
+ mediaRecordRef . current . removeEventListener ( 'resume' , onResumeListener ) ;
189
+ mediaRecordRef . current . removeEventListener ( 'dataavailable' , onDataAvailableListener ) ;
190
+ mediaRecordRef . current = null ;
191
+ } , [ ] ) ;
192
+
193
+ // resets the recording state, initializing the media recorder instance
194
+ const reset = useCallback ( ( ) => {
195
+ setState ( 'inactive' ) ;
196
+ clearData ( ) ;
197
+ clearListeners ( ) ;
198
+
199
+ // Check if the getUserMedia API is supported
200
+ if ( ! navigator . mediaDevices ?. getUserMedia ) {
201
+ return ;
202
+ }
203
+
204
+ const constraints = { audio : true } ;
205
+
206
+ navigator . mediaDevices
207
+ . getUserMedia ( constraints )
208
+ . then ( stream => {
209
+ mediaRecordRef . current = new MediaRecorder ( stream ) ;
210
+
211
+ mediaRecordRef . current . addEventListener ( 'start' , onStartListener ) ;
212
+ mediaRecordRef . current . addEventListener ( 'stop' , onStopListener ) ;
213
+ mediaRecordRef . current . addEventListener ( 'pause' , onPauseListener ) ;
214
+ mediaRecordRef . current . addEventListener ( 'resume' , onResumeListener ) ;
215
+ mediaRecordRef . current . addEventListener ( 'dataavailable' , onDataAvailableListener ) ;
216
+ } )
217
+ . catch ( err => {
218
+ // @todo : handle error
219
+ throw err ;
220
+ } ) ;
221
+ } , [ ] ) ;
222
+
223
+ // stops the recording and sets the error state
224
+ const onError = useCallback ( ( err : string | Error ) => {
225
+ stop ( ) ;
226
+ setError ( typeof err === 'string' ? err : err . message ) ;
227
+ setState ( 'error' ) ;
114
228
} , [ ] ) ;
115
229
116
230
/**
@@ -122,12 +236,15 @@ export default function useMediaRecording( {
122
236
123
237
/**
124
238
* `stop` event listener for the media recorder instance.
239
+ * Happens after the last `dataavailable` event.
125
240
*
126
241
* @returns {void }
127
242
*/
128
243
function onStopListener ( ) : void {
129
- setState ( 'inactive' ) ;
130
- onDone ?.( getBlob ( ) ) ;
244
+ setState ( 'processing' ) ;
245
+ const lastBlob = getBlob ( ) ;
246
+ const url = URL . createObjectURL ( lastBlob ) ;
247
+ onDone ?.( lastBlob , url ) ;
131
248
132
249
// Clear the recorded chunks
133
250
recordedChunks . length = 0 ;
@@ -164,61 +281,42 @@ export default function useMediaRecording( {
164
281
165
282
// Create and store the Blob for the recorded chunks
166
283
setBlob ( getBlob ( ) ) ;
167
- }
168
284
169
- // Create media recorder instance
170
- useEffect ( ( ) => {
171
- // Check if the getUserMedia API is supported
172
- if ( ! navigator . mediaDevices ?. getUserMedia ) {
173
- return ;
285
+ // If the recorder was paused, it is the last data available event, so we do not update the duration
286
+ if ( ! isPaused . current ) {
287
+ setDuration ( currentDuration => {
288
+ const now = Date . now ( ) ;
289
+ const difference = now - recordStartTimestamp . current ;
290
+ // Update the start time
291
+ recordStartTimestamp . current = now ;
292
+ return currentDuration + difference ;
293
+ } ) ;
174
294
}
295
+ }
175
296
176
- const constraints = { audio : true } ;
177
-
178
- navigator . mediaDevices
179
- . getUserMedia ( constraints )
180
- . then ( stream => {
181
- mediaRecordRef . current = new MediaRecorder ( stream ) ;
182
-
183
- mediaRecordRef . current . addEventListener ( 'start' , onStartListener ) ;
184
- mediaRecordRef . current . addEventListener ( 'stop' , onStopListener ) ;
185
- mediaRecordRef . current . addEventListener ( 'pause' , onPauseListener ) ;
186
- mediaRecordRef . current . addEventListener ( 'resume' , onResumeListener ) ;
187
- mediaRecordRef . current . addEventListener ( 'dataavailable' , onDataAvailableListener ) ;
188
- } )
189
- . catch ( err => {
190
- // @todo : handle error
191
- throw err ;
192
- } ) ;
297
+ // Remove listeners and clear the recorded chunks
298
+ useEffect ( ( ) => {
299
+ reset ( ) ;
193
300
194
301
return ( ) => {
195
- /*
196
- * mediaRecordRef is not defined when
197
- * the getUserMedia API is not supported,
198
- * or when the user has not granted access
199
- */
200
- if ( ! mediaRecordRef ?. current ) {
201
- return ;
202
- }
203
-
204
- mediaRecordRef . current . removeEventListener ( 'start' , onStartListener ) ;
205
- mediaRecordRef . current . removeEventListener ( 'stop' , onStopListener ) ;
206
- mediaRecordRef . current . removeEventListener ( 'pause' , onPauseListener ) ;
207
- mediaRecordRef . current . removeEventListener ( 'resume' , onResumeListener ) ;
208
- mediaRecordRef . current . removeEventListener ( 'dataavailable' , onDataAvailableListener ) ;
302
+ clearListeners ( ) ;
209
303
} ;
210
304
} , [ ] ) ;
211
305
212
306
return {
213
307
state,
214
308
blob,
215
309
url : blob ? URL . createObjectURL ( blob ) : null ,
310
+ error,
311
+ duration,
312
+ onError,
216
313
217
314
controls : {
218
315
start,
219
316
pause,
220
317
resume,
221
318
stop,
319
+ reset,
222
320
} ,
223
321
} ;
224
322
}
0 commit comments