matt HOFFNER
commited on
Commit
·
faa5faf
1
Parent(s):
7df6d3d
fixes to recurring recordings
Browse files- app/BlobFix.ts +549 -0
- app/hooks/useAudioManager.ts +0 -48
- 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 |
-
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
|
| 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 [
|
| 31 |
-
const
|
| 32 |
-
const
|
| 33 |
|
|
|
|
| 34 |
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
|
| 35 |
-
const
|
| 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 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 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 |
-
|
| 80 |
-
|
| 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 (
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
startListening(audioBuffer);
|
| 107 |
-
setIsRecording(!isRecording);
|
| 108 |
}
|
| 109 |
},
|
| 110 |
});
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
const
|
| 114 |
-
if
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|