matt HOFFNER commited on
Commit
faa5faf
β€’
1 Parent(s): 7df6d3d

fixes to recurring recordings

Browse files
Files changed (3) hide show
  1. app/BlobFix.ts +549 -0
  2. app/hooks/useAudioManager.ts +0 -48
  3. app/input.tsx +141 -97
app/BlobFix.ts ADDED
@@ -0,0 +1,549 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*
2
+ * There is a bug where `navigator.mediaDevices.getUserMedia` + `MediaRecorder`
3
+ * creates WEBM files without duration metadata. See:
4
+ * - https://bugs.chromium.org/p/chromium/issues/detail?id=642012
5
+ * - https://stackoverflow.com/a/39971175/13989043
6
+ *
7
+ * This file contains a function that fixes the duration metadata of a WEBM file.
8
+ * - Answer found: https://stackoverflow.com/a/75218309/13989043
9
+ * - Code adapted from: https://github.com/mat-sz/webm-fix-duration
10
+ * (forked from https://github.com/yusitnikov/fix-webm-duration)
11
+ */
12
+
13
+ /*
14
+ * This is the list of possible WEBM file sections by their IDs.
15
+ * Possible types: Container, Binary, Uint, Int, String, Float, Date
16
+ */
17
+ interface Section {
18
+ name: string;
19
+ type: string;
20
+ }
21
+
22
+ const sections: Record<number, Section> = {
23
+ 0xa45dfa3: { name: "EBML", type: "Container" },
24
+ 0x286: { name: "EBMLVersion", type: "Uint" },
25
+ 0x2f7: { name: "EBMLReadVersion", type: "Uint" },
26
+ 0x2f2: { name: "EBMLMaxIDLength", type: "Uint" },
27
+ 0x2f3: { name: "EBMLMaxSizeLength", type: "Uint" },
28
+ 0x282: { name: "DocType", type: "String" },
29
+ 0x287: { name: "DocTypeVersion", type: "Uint" },
30
+ 0x285: { name: "DocTypeReadVersion", type: "Uint" },
31
+ 0x6c: { name: "Void", type: "Binary" },
32
+ 0x3f: { name: "CRC-32", type: "Binary" },
33
+ 0xb538667: { name: "SignatureSlot", type: "Container" },
34
+ 0x3e8a: { name: "SignatureAlgo", type: "Uint" },
35
+ 0x3e9a: { name: "SignatureHash", type: "Uint" },
36
+ 0x3ea5: { name: "SignaturePublicKey", type: "Binary" },
37
+ 0x3eb5: { name: "Signature", type: "Binary" },
38
+ 0x3e5b: { name: "SignatureElements", type: "Container" },
39
+ 0x3e7b: { name: "SignatureElementList", type: "Container" },
40
+ 0x2532: { name: "SignedElement", type: "Binary" },
41
+ 0x8538067: { name: "Segment", type: "Container" },
42
+ 0x14d9b74: { name: "SeekHead", type: "Container" },
43
+ 0xdbb: { name: "Seek", type: "Container" },
44
+ 0x13ab: { name: "SeekID", type: "Binary" },
45
+ 0x13ac: { name: "SeekPosition", type: "Uint" },
46
+ 0x549a966: { name: "Info", type: "Container" },
47
+ 0x33a4: { name: "SegmentUID", type: "Binary" },
48
+ 0x3384: { name: "SegmentFilename", type: "String" },
49
+ 0x1cb923: { name: "PrevUID", type: "Binary" },
50
+ 0x1c83ab: { name: "PrevFilename", type: "String" },
51
+ 0x1eb923: { name: "NextUID", type: "Binary" },
52
+ 0x1e83bb: { name: "NextFilename", type: "String" },
53
+ 0x444: { name: "SegmentFamily", type: "Binary" },
54
+ 0x2924: { name: "ChapterTranslate", type: "Container" },
55
+ 0x29fc: { name: "ChapterTranslateEditionUID", type: "Uint" },
56
+ 0x29bf: { name: "ChapterTranslateCodec", type: "Uint" },
57
+ 0x29a5: { name: "ChapterTranslateID", type: "Binary" },
58
+ 0xad7b1: { name: "TimecodeScale", type: "Uint" },
59
+ 0x489: { name: "Duration", type: "Float" },
60
+ 0x461: { name: "DateUTC", type: "Date" },
61
+ 0x3ba9: { name: "Title", type: "String" },
62
+ 0xd80: { name: "MuxingApp", type: "String" },
63
+ 0x1741: { name: "WritingApp", type: "String" },
64
+ // 0xf43b675: { name: 'Cluster', type: 'Container' },
65
+ 0x67: { name: "Timecode", type: "Uint" },
66
+ 0x1854: { name: "SilentTracks", type: "Container" },
67
+ 0x18d7: { name: "SilentTrackNumber", type: "Uint" },
68
+ 0x27: { name: "Position", type: "Uint" },
69
+ 0x2b: { name: "PrevSize", type: "Uint" },
70
+ 0x23: { name: "SimpleBlock", type: "Binary" },
71
+ 0x20: { name: "BlockGroup", type: "Container" },
72
+ 0x21: { name: "Block", type: "Binary" },
73
+ 0x22: { name: "BlockVirtual", type: "Binary" },
74
+ 0x35a1: { name: "BlockAdditions", type: "Container" },
75
+ 0x26: { name: "BlockMore", type: "Container" },
76
+ 0x6e: { name: "BlockAddID", type: "Uint" },
77
+ 0x25: { name: "BlockAdditional", type: "Binary" },
78
+ 0x1b: { name: "BlockDuration", type: "Uint" },
79
+ 0x7a: { name: "ReferencePriority", type: "Uint" },
80
+ 0x7b: { name: "ReferenceBlock", type: "Int" },
81
+ 0x7d: { name: "ReferenceVirtual", type: "Int" },
82
+ 0x24: { name: "CodecState", type: "Binary" },
83
+ 0x35a2: { name: "DiscardPadding", type: "Int" },
84
+ 0xe: { name: "Slices", type: "Container" },
85
+ 0x68: { name: "TimeSlice", type: "Container" },
86
+ 0x4c: { name: "LaceNumber", type: "Uint" },
87
+ 0x4d: { name: "FrameNumber", type: "Uint" },
88
+ 0x4b: { name: "BlockAdditionID", type: "Uint" },
89
+ 0x4e: { name: "Delay", type: "Uint" },
90
+ 0x4f: { name: "SliceDuration", type: "Uint" },
91
+ 0x48: { name: "ReferenceFrame", type: "Container" },
92
+ 0x49: { name: "ReferenceOffset", type: "Uint" },
93
+ 0x4a: { name: "ReferenceTimeCode", type: "Uint" },
94
+ 0x2f: { name: "EncryptedBlock", type: "Binary" },
95
+ 0x654ae6b: { name: "Tracks", type: "Container" },
96
+ 0x2e: { name: "TrackEntry", type: "Container" },
97
+ 0x57: { name: "TrackNumber", type: "Uint" },
98
+ 0x33c5: { name: "TrackUID", type: "Uint" },
99
+ 0x3: { name: "TrackType", type: "Uint" },
100
+ 0x39: { name: "FlagEnabled", type: "Uint" },
101
+ 0x8: { name: "FlagDefault", type: "Uint" },
102
+ 0x15aa: { name: "FlagForced", type: "Uint" },
103
+ 0x1c: { name: "FlagLacing", type: "Uint" },
104
+ 0x2de7: { name: "MinCache", type: "Uint" },
105
+ 0x2df8: { name: "MaxCache", type: "Uint" },
106
+ 0x3e383: { name: "DefaultDuration", type: "Uint" },
107
+ 0x34e7a: { name: "DefaultDecodedFieldDuration", type: "Uint" },
108
+ 0x3314f: { name: "TrackTimecodeScale", type: "Float" },
109
+ 0x137f: { name: "TrackOffset", type: "Int" },
110
+ 0x15ee: { name: "MaxBlockAdditionID", type: "Uint" },
111
+ 0x136e: { name: "Name", type: "String" },
112
+ 0x2b59c: { name: "Language", type: "String" },
113
+ 0x6: { name: "CodecID", type: "String" },
114
+ 0x23a2: { name: "CodecPrivate", type: "Binary" },
115
+ 0x58688: { name: "CodecName", type: "String" },
116
+ 0x3446: { name: "AttachmentLink", type: "Uint" },
117
+ 0x1a9697: { name: "CodecSettings", type: "String" },
118
+ 0x1b4040: { name: "CodecInfoURL", type: "String" },
119
+ 0x6b240: { name: "CodecDownloadURL", type: "String" },
120
+ 0x2a: { name: "CodecDecodeAll", type: "Uint" },
121
+ 0x2fab: { name: "TrackOverlay", type: "Uint" },
122
+ 0x16aa: { name: "CodecDelay", type: "Uint" },
123
+ 0x16bb: { name: "SeekPreRoll", type: "Uint" },
124
+ 0x2624: { name: "TrackTranslate", type: "Container" },
125
+ 0x26fc: { name: "TrackTranslateEditionUID", type: "Uint" },
126
+ 0x26bf: { name: "TrackTranslateCodec", type: "Uint" },
127
+ 0x26a5: { name: "TrackTranslateTrackID", type: "Binary" },
128
+ 0x60: { name: "Video", type: "Container" },
129
+ 0x1a: { name: "FlagInterlaced", type: "Uint" },
130
+ 0x13b8: { name: "StereoMode", type: "Uint" },
131
+ 0x13c0: { name: "AlphaMode", type: "Uint" },
132
+ 0x13b9: { name: "OldStereoMode", type: "Uint" },
133
+ 0x30: { name: "PixelWidth", type: "Uint" },
134
+ 0x3a: { name: "PixelHeight", type: "Uint" },
135
+ 0x14aa: { name: "PixelCropBottom", type: "Uint" },
136
+ 0x14bb: { name: "PixelCropTop", type: "Uint" },
137
+ 0x14cc: { name: "PixelCropLeft", type: "Uint" },
138
+ 0x14dd: { name: "PixelCropRight", type: "Uint" },
139
+ 0x14b0: { name: "DisplayWidth", type: "Uint" },
140
+ 0x14ba: { name: "DisplayHeight", type: "Uint" },
141
+ 0x14b2: { name: "DisplayUnit", type: "Uint" },
142
+ 0x14b3: { name: "AspectRatioType", type: "Uint" },
143
+ 0xeb524: { name: "ColourSpace", type: "Binary" },
144
+ 0xfb523: { name: "GammaValue", type: "Float" },
145
+ 0x383e3: { name: "FrameRate", type: "Float" },
146
+ 0x61: { name: "Audio", type: "Container" },
147
+ 0x35: { name: "SamplingFrequency", type: "Float" },
148
+ 0x38b5: { name: "OutputSamplingFrequency", type: "Float" },
149
+ 0x1f: { name: "Channels", type: "Uint" },
150
+ 0x3d7b: { name: "ChannelPositions", type: "Binary" },
151
+ 0x2264: { name: "BitDepth", type: "Uint" },
152
+ 0x62: { name: "TrackOperation", type: "Container" },
153
+ 0x63: { name: "TrackCombinePlanes", type: "Container" },
154
+ 0x64: { name: "TrackPlane", type: "Container" },
155
+ 0x65: { name: "TrackPlaneUID", type: "Uint" },
156
+ 0x66: { name: "TrackPlaneType", type: "Uint" },
157
+ 0x69: { name: "TrackJoinBlocks", type: "Container" },
158
+ 0x6d: { name: "TrackJoinUID", type: "Uint" },
159
+ 0x40: { name: "TrickTrackUID", type: "Uint" },
160
+ 0x41: { name: "TrickTrackSegmentUID", type: "Binary" },
161
+ 0x46: { name: "TrickTrackFlag", type: "Uint" },
162
+ 0x47: { name: "TrickMasterTrackUID", type: "Uint" },
163
+ 0x44: { name: "TrickMasterTrackSegmentUID", type: "Binary" },
164
+ 0x2d80: { name: "ContentEncodings", type: "Container" },
165
+ 0x2240: { name: "ContentEncoding", type: "Container" },
166
+ 0x1031: { name: "ContentEncodingOrder", type: "Uint" },
167
+ 0x1032: { name: "ContentEncodingScope", type: "Uint" },
168
+ 0x1033: { name: "ContentEncodingType", type: "Uint" },
169
+ 0x1034: { name: "ContentCompression", type: "Container" },
170
+ 0x254: { name: "ContentCompAlgo", type: "Uint" },
171
+ 0x255: { name: "ContentCompSettings", type: "Binary" },
172
+ 0x1035: { name: "ContentEncryption", type: "Container" },
173
+ 0x7e1: { name: "ContentEncAlgo", type: "Uint" },
174
+ 0x7e2: { name: "ContentEncKeyID", type: "Binary" },
175
+ 0x7e3: { name: "ContentSignature", type: "Binary" },
176
+ 0x7e4: { name: "ContentSigKeyID", type: "Binary" },
177
+ 0x7e5: { name: "ContentSigAlgo", type: "Uint" },
178
+ 0x7e6: { name: "ContentSigHashAlgo", type: "Uint" },
179
+ 0xc53bb6b: { name: "Cues", type: "Container" },
180
+ 0x3b: { name: "CuePoint", type: "Container" },
181
+ 0x33: { name: "CueTime", type: "Uint" },
182
+ 0x37: { name: "CueTrackPositions", type: "Container" },
183
+ 0x77: { name: "CueTrack", type: "Uint" },
184
+ 0x71: { name: "CueClusterPosition", type: "Uint" },
185
+ 0x70: { name: "CueRelativePosition", type: "Uint" },
186
+ 0x32: { name: "CueDuration", type: "Uint" },
187
+ 0x1378: { name: "CueBlockNumber", type: "Uint" },
188
+ 0x6a: { name: "CueCodecState", type: "Uint" },
189
+ 0x5b: { name: "CueReference", type: "Container" },
190
+ 0x16: { name: "CueRefTime", type: "Uint" },
191
+ 0x17: { name: "CueRefCluster", type: "Uint" },
192
+ 0x135f: { name: "CueRefNumber", type: "Uint" },
193
+ 0x6b: { name: "CueRefCodecState", type: "Uint" },
194
+ 0x941a469: { name: "Attachments", type: "Container" },
195
+ 0x21a7: { name: "AttachedFile", type: "Container" },
196
+ 0x67e: { name: "FileDescription", type: "String" },
197
+ 0x66e: { name: "FileName", type: "String" },
198
+ 0x660: { name: "FileMimeType", type: "String" },
199
+ 0x65c: { name: "FileData", type: "Binary" },
200
+ 0x6ae: { name: "FileUID", type: "Uint" },
201
+ 0x675: { name: "FileReferral", type: "Binary" },
202
+ 0x661: { name: "FileUsedStartTime", type: "Uint" },
203
+ 0x662: { name: "FileUsedEndTime", type: "Uint" },
204
+ 0x43a770: { name: "Chapters", type: "Container" },
205
+ 0x5b9: { name: "EditionEntry", type: "Container" },
206
+ 0x5bc: { name: "EditionUID", type: "Uint" },
207
+ 0x5bd: { name: "EditionFlagHidden", type: "Uint" },
208
+ 0x5db: { name: "EditionFlagDefault", type: "Uint" },
209
+ 0x5dd: { name: "EditionFlagOrdered", type: "Uint" },
210
+ 0x36: { name: "ChapterAtom", type: "Container" },
211
+ 0x33c4: { name: "ChapterUID", type: "Uint" },
212
+ 0x1654: { name: "ChapterStringUID", type: "String" },
213
+ 0x11: { name: "ChapterTimeStart", type: "Uint" },
214
+ 0x12: { name: "ChapterTimeEnd", type: "Uint" },
215
+ 0x18: { name: "ChapterFlagHidden", type: "Uint" },
216
+ 0x598: { name: "ChapterFlagEnabled", type: "Uint" },
217
+ 0x2e67: { name: "ChapterSegmentUID", type: "Binary" },
218
+ 0x2ebc: { name: "ChapterSegmentEditionUID", type: "Uint" },
219
+ 0x23c3: { name: "ChapterPhysicalEquiv", type: "Uint" },
220
+ 0xf: { name: "ChapterTrack", type: "Container" },
221
+ 0x9: { name: "ChapterTrackNumber", type: "Uint" },
222
+ 0x0: { name: "ChapterDisplay", type: "Container" },
223
+ 0x5: { name: "ChapString", type: "String" },
224
+ 0x37c: { name: "ChapLanguage", type: "String" },
225
+ 0x37e: { name: "ChapCountry", type: "String" },
226
+ 0x2944: { name: "ChapProcess", type: "Container" },
227
+ 0x2955: { name: "ChapProcessCodecID", type: "Uint" },
228
+ 0x50d: { name: "ChapProcessPrivate", type: "Binary" },
229
+ 0x2911: { name: "ChapProcessCommand", type: "Container" },
230
+ 0x2922: { name: "ChapProcessTime", type: "Uint" },
231
+ 0x2933: { name: "ChapProcessData", type: "Binary" },
232
+ 0x254c367: { name: "Tags", type: "Container" },
233
+ 0x3373: { name: "Tag", type: "Container" },
234
+ 0x23c0: { name: "Targets", type: "Container" },
235
+ 0x28ca: { name: "TargetTypeValue", type: "Uint" },
236
+ 0x23ca: { name: "TargetType", type: "String" },
237
+ 0x23c5: { name: "TagTrackUID", type: "Uint" },
238
+ 0x23c9: { name: "TagEditionUID", type: "Uint" },
239
+ 0x23c4: { name: "TagChapterUID", type: "Uint" },
240
+ 0x23c6: { name: "TagAttachmentUID", type: "Uint" },
241
+ 0x27c8: { name: "SimpleTag", type: "Container" },
242
+ 0x5a3: { name: "TagName", type: "String" },
243
+ 0x47a: { name: "TagLanguage", type: "String" },
244
+ 0x484: { name: "TagDefault", type: "Uint" },
245
+ 0x487: { name: "TagString", type: "String" },
246
+ 0x485: { name: "TagBinary", type: "Binary" },
247
+ };
248
+
249
+ class WebmBase<T> {
250
+ source?: Uint8Array;
251
+ data?: T;
252
+
253
+ constructor(private name = "Unknown", private type = "Unknown") {}
254
+
255
+ updateBySource() {}
256
+
257
+ setSource(source: Uint8Array) {
258
+ this.source = source;
259
+ this.updateBySource();
260
+ }
261
+
262
+ updateByData() {}
263
+
264
+ setData(data: T) {
265
+ this.data = data;
266
+ this.updateByData();
267
+ }
268
+ }
269
+
270
+ class WebmUint extends WebmBase<string> {
271
+ constructor(name: string, type: string) {
272
+ super(name, type || "Uint");
273
+ }
274
+
275
+ updateBySource() {
276
+ // use hex representation of a number instead of number value
277
+ this.data = "";
278
+ for (let i = 0; i < this.source!.length; i++) {
279
+ const hex = this.source![i].toString(16);
280
+ this.data += padHex(hex);
281
+ }
282
+ }
283
+
284
+ updateByData() {
285
+ const length = this.data!.length / 2;
286
+ this.source = new Uint8Array(length);
287
+ for (let i = 0; i < length; i++) {
288
+ const hex = this.data!.substr(i * 2, 2);
289
+ this.source[i] = parseInt(hex, 16);
290
+ }
291
+ }
292
+
293
+ getValue() {
294
+ return parseInt(this.data!, 16);
295
+ }
296
+
297
+ setValue(value: number) {
298
+ this.setData(padHex(value.toString(16)));
299
+ }
300
+ }
301
+
302
+ function padHex(hex: string) {
303
+ return hex.length % 2 === 1 ? "0" + hex : hex;
304
+ }
305
+
306
+ class WebmFloat extends WebmBase<number> {
307
+ constructor(name: string, type: string) {
308
+ super(name, type || "Float");
309
+ }
310
+
311
+ getFloatArrayType() {
312
+ return this.source && this.source.length === 4
313
+ ? Float32Array
314
+ : Float64Array;
315
+ }
316
+ updateBySource() {
317
+ const byteArray = this.source!.reverse();
318
+ const floatArrayType = this.getFloatArrayType();
319
+ const floatArray = new floatArrayType(byteArray.buffer);
320
+ this.data! = floatArray[0];
321
+ }
322
+ updateByData() {
323
+ const floatArrayType = this.getFloatArrayType();
324
+ const floatArray = new floatArrayType([this.data!]);
325
+ const byteArray = new Uint8Array(floatArray.buffer);
326
+ this.source = byteArray.reverse();
327
+ }
328
+ getValue() {
329
+ return this.data;
330
+ }
331
+ setValue(value: number) {
332
+ this.setData(value);
333
+ }
334
+ }
335
+
336
+ interface ContainerData {
337
+ id: number;
338
+ idHex?: string;
339
+ data: WebmBase<any>;
340
+ }
341
+
342
+ class WebmContainer extends WebmBase<ContainerData[]> {
343
+ offset: number = 0;
344
+ data: ContainerData[] = [];
345
+
346
+ constructor(name: string, type: string) {
347
+ super(name, type || "Container");
348
+ }
349
+
350
+ readByte() {
351
+ return this.source![this.offset++];
352
+ }
353
+ readUint() {
354
+ const firstByte = this.readByte();
355
+ const bytes = 8 - firstByte.toString(2).length;
356
+ let value = firstByte - (1 << (7 - bytes));
357
+ for (let i = 0; i < bytes; i++) {
358
+ // don't use bit operators to support x86
359
+ value *= 256;
360
+ value += this.readByte();
361
+ }
362
+ return value;
363
+ }
364
+ updateBySource() {
365
+ let end: number | undefined = undefined;
366
+ this.data = [];
367
+ for (
368
+ this.offset = 0;
369
+ this.offset < this.source!.length;
370
+ this.offset = end
371
+ ) {
372
+ const id = this.readUint();
373
+ const len = this.readUint();
374
+ end = Math.min(this.offset + len, this.source!.length);
375
+ const data = this.source!.slice(this.offset, end);
376
+
377
+ const info = sections[id] || { name: "Unknown", type: "Unknown" };
378
+ let ctr: any = WebmBase;
379
+ switch (info.type) {
380
+ case "Container":
381
+ ctr = WebmContainer;
382
+ break;
383
+ case "Uint":
384
+ ctr = WebmUint;
385
+ break;
386
+ case "Float":
387
+ ctr = WebmFloat;
388
+ break;
389
+ }
390
+ const section = new ctr(info.name, info.type);
391
+ section.setSource(data);
392
+ this.data.push({
393
+ id: id,
394
+ idHex: id.toString(16),
395
+ data: section,
396
+ });
397
+ }
398
+ }
399
+ writeUint(x: number, draft = false) {
400
+ for (
401
+ var bytes = 1, flag = 0x80;
402
+ x >= flag && bytes < 8;
403
+ bytes++, flag *= 0x80
404
+ ) {}
405
+
406
+ if (!draft) {
407
+ let value = flag + x;
408
+ for (let i = bytes - 1; i >= 0; i--) {
409
+ // don't use bit operators to support x86
410
+ const c = value % 256;
411
+ this.source![this.offset! + i] = c;
412
+ value = (value - c) / 256;
413
+ }
414
+ }
415
+
416
+ this.offset += bytes;
417
+ }
418
+
419
+ writeSections(draft = false) {
420
+ this.offset = 0;
421
+ for (let i = 0; i < this.data.length; i++) {
422
+ const section = this.data[i],
423
+ content = section.data.source,
424
+ contentLength = content!.length;
425
+ this.writeUint(section.id, draft);
426
+ this.writeUint(contentLength, draft);
427
+ if (!draft) {
428
+ this.source!.set(content!, this.offset);
429
+ }
430
+ this.offset += contentLength;
431
+ }
432
+ return this.offset;
433
+ }
434
+
435
+ updateByData() {
436
+ // run without accessing this.source to determine total length - need to know it to create Uint8Array
437
+ const length = this.writeSections(true);
438
+ this.source = new Uint8Array(length);
439
+ // now really write data
440
+ this.writeSections();
441
+ }
442
+
443
+ getSectionById(id: number) {
444
+ for (let i = 0; i < this.data.length; i++) {
445
+ const section = this.data[i];
446
+ if (section.id === id) {
447
+ return section.data;
448
+ }
449
+ }
450
+
451
+ return undefined;
452
+ }
453
+ }
454
+
455
+ class WebmFile extends WebmContainer {
456
+ constructor(source: Uint8Array) {
457
+ super("File", "File");
458
+ this.setSource(source);
459
+ }
460
+
461
+ fixDuration(duration: number) {
462
+ const segmentSection = this.getSectionById(0x8538067) as WebmContainer;
463
+ if (!segmentSection) {
464
+ return false;
465
+ }
466
+
467
+ const infoSection = segmentSection.getSectionById(
468
+ 0x549a966,
469
+ ) as WebmContainer;
470
+ if (!infoSection) {
471
+ return false;
472
+ }
473
+
474
+ const timeScaleSection = infoSection.getSectionById(
475
+ 0xad7b1,
476
+ ) as WebmFloat;
477
+ if (!timeScaleSection) {
478
+ return false;
479
+ }
480
+
481
+ let durationSection = infoSection.getSectionById(0x489) as WebmFloat;
482
+ if (durationSection) {
483
+ if (durationSection.getValue()! <= 0) {
484
+ durationSection.setValue(duration);
485
+ } else {
486
+ return false;
487
+ }
488
+ } else {
489
+ // append Duration section
490
+ durationSection = new WebmFloat("Duration", "Float");
491
+ durationSection.setValue(duration);
492
+ infoSection.data.push({
493
+ id: 0x489,
494
+ data: durationSection,
495
+ });
496
+ }
497
+
498
+ // set default time scale to 1 millisecond (1000000 nanoseconds)
499
+ timeScaleSection.setValue(1000000);
500
+ infoSection.updateByData();
501
+ segmentSection.updateByData();
502
+ this.updateByData();
503
+
504
+ return true;
505
+ }
506
+
507
+ toBlob(type = "video/webm") {
508
+ return new Blob([this.source!.buffer], { type });
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Fixes duration on MediaRecorder output.
514
+ * @param blob Input Blob with incorrect duration.
515
+ * @param duration Correct duration (in milliseconds).
516
+ * @param type Output blob mimetype (default: video/webm).
517
+ * @returns
518
+ */
519
+ export const webmFixDuration = (
520
+ blob: Blob,
521
+ duration: number,
522
+ type = "video/webm",
523
+ ): Promise<Blob> => {
524
+ return new Promise((resolve, reject) => {
525
+ try {
526
+ const reader = new FileReader();
527
+
528
+ reader.addEventListener("loadend", () => {
529
+ try {
530
+ const result = reader.result as ArrayBuffer;
531
+ const file = new WebmFile(new Uint8Array(result));
532
+ if (file.fixDuration(duration)) {
533
+ resolve(file.toBlob(type));
534
+ } else {
535
+ resolve(blob);
536
+ }
537
+ } catch (ex) {
538
+ reject(ex);
539
+ }
540
+ });
541
+
542
+ reader.addEventListener("error", () => reject());
543
+
544
+ reader.readAsArrayBuffer(blob);
545
+ } catch (ex) {
546
+ reject(ex);
547
+ }
548
+ });
549
+ };
app/hooks/useAudioManager.ts DELETED
@@ -1,48 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useCallback } from 'react';
4
- import constants from '../constants';
5
-
6
- const useAudioManager = () => {
7
- const [progress, setProgress] = useState<number | undefined>(undefined);
8
- const [audioData, setAudioData] = useState<{
9
- buffer: AudioBuffer;
10
- url: string;
11
- source: any;
12
- mimeType: string;
13
- } | undefined>(undefined);
14
-
15
- // Reset the audio data
16
- const resetAudio = useCallback(() => {
17
- setAudioData(undefined);
18
- }, []);
19
-
20
- // Set audio from a Blob (e.g., from recording)
21
- const setAudioFromRecording = useCallback(async (data: Blob) => {
22
- resetAudio();
23
- setProgress(0);
24
- const blobUrl = URL.createObjectURL(data);
25
- const audioCTX = new AudioContext({sampleRate: constants.SAMPLING_RATE, });
26
- const arrayBuffer = await data.arrayBuffer();
27
- const decoded = await audioCTX.decodeAudioData(arrayBuffer);
28
- setProgress(undefined);
29
- setAudioData({
30
- buffer: decoded,
31
- url: blobUrl,
32
- source: "RECORDING",
33
- mimeType: data.type,
34
- });
35
- }, [resetAudio]);
36
-
37
- // Other functionalities (e.g., setAudioFromDownload, downloadAudioFromUrl)
38
- // can be added similarly based on your requirements
39
-
40
- return {
41
- audioData,
42
- progress,
43
- setAudioFromRecording,
44
- // Export other functions as needed
45
- };
46
- };
47
-
48
- export default useAudioManager;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/input.tsx CHANGED
@@ -1,24 +1,13 @@
1
  import React, { useState, useEffect, useRef } from 'react';
2
- import MicIcon from '@mui/icons-material/Mic';
3
- import StopIcon from '@mui/icons-material/Stop';
4
  import styles from './page.module.css';
5
-
6
  import useSpeechRecognition from './hooks/useSpeechRecognition';
7
- import useAudioManager from './hooks/useAudioManager';
8
  import { useMicVAD } from "@ricky0123/vad-react";
9
-
10
  import * as ort from "onnxruntime-web";
11
- ort.env.wasm.wasmPaths = "/_next/static/chunks/";
 
 
12
 
13
- const getMimeType = (): string | null => {
14
- const types = ["audio/webm", "audio/mp4", "audio/ogg", "audio/wav", "audio/aac"];
15
- for (let type of types) {
16
- if (MediaRecorder.isTypeSupported(type)) {
17
- return type;
18
- }
19
- }
20
- return null;
21
- };
22
 
23
  interface VoiceInputFormProps {
24
  handleSubmit: any;
@@ -26,22 +15,40 @@ interface VoiceInputFormProps {
26
  setInput: React.Dispatch<React.SetStateAction<string>>;
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  const VoiceInputForm: React.FC<VoiceInputFormProps> = ({ handleSubmit, input, setInput }) => {
30
- const [isRecording, setIsRecording] = useState(false);
31
- const { startListening, stopListening, recognizedText } = useSpeechRecognition();
32
- const { setAudioFromRecording } = useAudioManager();
33
 
 
34
  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
35
- const audioChunksRef = useRef<BlobPart[]>([]);
36
-
37
- const cleanupRecording = () => {
38
- if (mediaRecorderRef.current) {
39
- mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
40
- mediaRecorderRef.current = null;
41
- }
42
- audioChunksRef.current = [];
43
- };
44
 
 
45
 
46
  useEffect(() => {
47
  if (recognizedText) {
@@ -49,84 +56,122 @@ const VoiceInputForm: React.FC<VoiceInputFormProps> = ({ handleSubmit, input, se
49
  }
50
  }, [recognizedText, setInput]);
51
 
52
- const startRecording = async () => {
53
- cleanupRecording(); // Clean up any existing recording resources
54
-
55
- try {
56
- // Simplified constraints for broader compatibility
57
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
58
- let recorderOptions = {};
59
-
60
- // Check if the mimeType is supported; if so, use it
61
- const mimeType = getMimeType();
62
- if (mimeType && MediaRecorder.isTypeSupported(mimeType)) {
63
- recorderOptions = { mimeType };
64
- }
65
-
66
- mediaRecorderRef.current = new MediaRecorder(stream, recorderOptions);
67
-
68
- mediaRecorderRef.current.ondataavailable = (event: BlobEvent) => {
69
- audioChunksRef.current.push(event.data);
70
- };
71
-
72
- mediaRecorderRef.current.start();
73
- } catch (err) {
74
- console.error("Error accessing media devices:", err);
75
- }
76
- };
77
-
78
 
79
- const stopRecording = async (): Promise<Blob> => {
80
- return new Promise((resolve, reject) => {
81
- const recorder = mediaRecorderRef.current;
82
- if (recorder && recorder.state === "recording") {
83
- recorder.onstop = () => {
84
- const audioBlob = new Blob(audioChunksRef.current, { 'type': recorder.mimeType });
85
- audioChunksRef.current = [];
86
- resolve(audioBlob);
87
- };
88
- recorder.stop();
89
- } else {
90
- reject(new Error("MediaRecorder is not recording"));
91
  }
92
- });
93
- };
 
 
94
 
95
  const vad = useMicVAD({
96
  modelURL: "/_next/static/chunks/silero_vad.onnx",
97
  workletURL: "/_next/static/chunks/vad.worklet.bundle.min.js",
98
  startOnLoad: false,
99
- onSpeechEnd: async (audio) => {
100
- console.log('hello??')
101
- if (isRecording) {
102
- stopListening();
103
- const recordedBlob = await stopRecording();
104
- setAudioFromRecording(recordedBlob);
105
- const audioBuffer = await convertBlobToAudioBuffer(recordedBlob);
106
- startListening(audioBuffer);
107
- setIsRecording(!isRecording);
108
  }
109
  },
110
  });
111
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- const handleRecording = async () => {
114
- if (isRecording) {
115
- stopListening();
116
- const recordedBlob = await stopRecording();
117
- if (recordedBlob) {
118
- setAudioFromRecording(recordedBlob);
119
- const audioBuffer = await convertBlobToAudioBuffer(recordedBlob);
120
- startListening(audioBuffer);
 
 
 
 
121
  }
122
- cleanupRecording(); // Clean up resources after stopping recording
123
- } else {
124
- vad.toggle();
125
- await startRecording();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  }
127
- setIsRecording(!isRecording);
128
  };
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  return (
132
  <div>
@@ -139,17 +184,16 @@ const VoiceInputForm: React.FC<VoiceInputFormProps> = ({ handleSubmit, input, se
139
  placeholder="Speak or type..."
140
  />
141
  </form>
142
- <button onClick={handleRecording} className={styles.button}>
143
- {isRecording ? <StopIcon /> : <MicIcon />}
144
- </button>
 
 
 
 
145
  </div>
146
  );
147
  };
148
 
149
- const convertBlobToAudioBuffer = async (blob: Blob): Promise<AudioBuffer> => {
150
- const audioContext = new AudioContext();
151
- const arrayBuffer = await blob.arrayBuffer();
152
- return await audioContext.decodeAudioData(arrayBuffer);
153
- };
154
 
155
  export default VoiceInputForm;
 
1
  import React, { useState, useEffect, useRef } from 'react';
 
 
2
  import styles from './page.module.css';
 
3
  import useSpeechRecognition from './hooks/useSpeechRecognition';
 
4
  import { useMicVAD } from "@ricky0123/vad-react";
 
5
  import * as ort from "onnxruntime-web";
6
+ import MicIcon from '@mui/icons-material/Mic';
7
+ import StopIcon from '@mui/icons-material/Stop';
8
+ import { webmFixDuration } from './BlobFix';
9
 
10
+ ort.env.wasm.wasmPaths = "/_next/static/chunks/";
 
 
 
 
 
 
 
 
11
 
12
  interface VoiceInputFormProps {
13
  handleSubmit: any;
 
15
  setInput: React.Dispatch<React.SetStateAction<string>>;
16
  }
17
 
18
+ function getMimeType() {
19
+ const types = [
20
+ "audio/webm",
21
+ "audio/mp4",
22
+ "audio/ogg",
23
+ "audio/wav",
24
+ "audio/aac",
25
+ ];
26
+ for (let i = 0; i < types.length; i++) {
27
+ if (MediaRecorder.isTypeSupported(types[i])) {
28
+ return types[i];
29
+ }
30
+ }
31
+ return undefined;
32
+ }
33
+
34
+ const convertBlobToAudioBuffer = async (blob: Blob): Promise<AudioBuffer> => {
35
+ const audioContext = new AudioContext();
36
+ const arrayBuffer = await blob.arrayBuffer();
37
+ return await audioContext.decodeAudioData(arrayBuffer);
38
+ };
39
+
40
+
41
  const VoiceInputForm: React.FC<VoiceInputFormProps> = ({ handleSubmit, input, setInput }) => {
42
+ const [recording, setRecording] = useState(false);
43
+ const [duration, setDuration] = useState(0);
44
+ const [recordedBlob, setRecordedBlob] = useState<Blob | null>(null);
45
 
46
+ const streamRef = useRef<MediaStream | null>(null);
47
  const mediaRecorderRef = useRef<MediaRecorder | null>(null);
48
+ const chunksRef = useRef<Blob[]>([]);
49
+ const audioRef = useRef<HTMLAudioElement | null>(null);
 
 
 
 
 
 
 
50
 
51
+ const { startListening, recognizedText } = useSpeechRecognition();
52
 
53
  useEffect(() => {
54
  if (recognizedText) {
 
56
  }
57
  }, [recognizedText, setInput]);
58
 
59
+ useEffect(() => {
60
+ const processRecording = async () => {
61
+ if (recordedBlob) {
62
+ // Process the blob for transcription
63
+ const audioBuffer = await convertBlobToAudioBuffer(recordedBlob);
64
+ startListening(audioBuffer); // Start the transcription process
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ // Reset the blob state if you want to prepare for a new recording
67
+ setRecordedBlob(null);
 
 
 
 
 
 
 
 
 
 
68
  }
69
+ };
70
+
71
+ processRecording();
72
+ }, [recordedBlob, startListening]);
73
 
74
  const vad = useMicVAD({
75
  modelURL: "/_next/static/chunks/silero_vad.onnx",
76
  workletURL: "/_next/static/chunks/vad.worklet.bundle.min.js",
77
  startOnLoad: false,
78
+ onSpeechEnd: async () => {
79
+ if (recording) {
80
+ await stopRecording(); // Stop the recording
81
+
82
+ console.log('input', input);
83
+
84
+ setRecording(!recording); // Update the recording state
 
 
85
  }
86
  },
87
  });
88
 
89
+ const stopRecording = () => {
90
+ if (
91
+ mediaRecorderRef.current &&
92
+ mediaRecorderRef.current.state === "recording"
93
+ ) {
94
+ mediaRecorderRef.current.stop(); // set state to inactive
95
+ setDuration(0);
96
+ setRecording(false);
97
+ vad.pause();
98
+ }
99
+ };
100
 
101
+ const startRecording = async () => {
102
+ // Reset recording (if any)
103
+ setRecordedBlob(null);
104
+ vad.start();
105
+
106
+ let startTime = Date.now();
107
+
108
+ try {
109
+ if (!streamRef.current) {
110
+ streamRef.current = await navigator.mediaDevices.getUserMedia({
111
+ audio: true,
112
+ });
113
  }
114
+
115
+ const mimeType = getMimeType();
116
+ const mediaRecorder = new MediaRecorder(streamRef.current, {
117
+ mimeType,
118
+ });
119
+
120
+ mediaRecorderRef.current = mediaRecorder;
121
+
122
+ mediaRecorder.addEventListener("dataavailable", async (event) => {
123
+ if (event.data.size > 0) {
124
+ chunksRef.current.push(event.data);
125
+ }
126
+ if (mediaRecorder.state === "inactive") {
127
+ const duration = Date.now() - startTime;
128
+
129
+ // Received a stop event
130
+ let blob = new Blob(chunksRef.current, { type: mimeType });
131
+
132
+ if (mimeType === "audio/webm") {
133
+ blob = await webmFixDuration(blob, duration, blob.type);
134
+ }
135
+
136
+ setRecordedBlob(blob);
137
+
138
+ chunksRef.current = [];
139
+ }
140
+ });
141
+ mediaRecorder.start();
142
+ setRecording(true);
143
+ } catch (error) {
144
+ console.error("Error accessing microphone:", error);
145
  }
 
146
  };
147
 
148
+ useEffect(() => {
149
+ let stream: MediaStream | null = null;
150
+
151
+ if (recording) {
152
+ const timer = setInterval(() => {
153
+ setDuration((prevDuration) => prevDuration + 1);
154
+ }, 1000);
155
+
156
+ return () => {
157
+ clearInterval(timer);
158
+ };
159
+ }
160
+
161
+ return () => {
162
+ if (stream) {
163
+ stream.getTracks().forEach((track) => track.stop());
164
+ }
165
+ };
166
+ }, [recording]);
167
+
168
+ const handleToggleRecording = () => {
169
+ if (recording) {
170
+ stopRecording();
171
+ } else {
172
+ startRecording();
173
+ }
174
+ };
175
 
176
  return (
177
  <div>
 
184
  placeholder="Speak or type..."
185
  />
186
  </form>
187
+ <button
188
+ type='button'
189
+ className={styles.button}
190
+ onClick={handleToggleRecording}
191
+ >
192
+ {recording ? <StopIcon /> : <MicIcon />}
193
+ </button>
194
  </div>
195
  );
196
  };
197
 
 
 
 
 
 
198
 
199
  export default VoiceInputForm;