hookehuyr

录音模块功能

...@@ -9,5 +9,11 @@ declare module 'vue' { ...@@ -9,5 +9,11 @@ declare module 'vue' {
9 export interface GlobalComponents { 9 export interface GlobalComponents {
10 RouterLink: typeof import('vue-router')['RouterLink'] 10 RouterLink: typeof import('vue-router')['RouterLink']
11 RouterView: typeof import('vue-router')['RouterView'] 11 RouterView: typeof import('vue-router')['RouterView']
12 + VanButton: typeof import('vant/es')['Button']
13 + VanCell: typeof import('vant/es')['Cell']
14 + VanCellGroup: typeof import('vant/es')['CellGroup']
15 + VanCol: typeof import('vant/es')['Col']
16 + VanFloatingPanel: typeof import('vant/es')['FloatingPanel']
17 + VanRow: typeof import('vant/es')['Row']
12 } 18 }
13 } 19 }
......
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
27 "recorder-core": "^1.3.23122400", 27 "recorder-core": "^1.3.23122400",
28 "typescript": "^4.7.3", 28 "typescript": "^4.7.3",
29 "uuid": "^8.3.2", 29 "uuid": "^8.3.2",
30 - "vant": "^4.8.1", 30 + "vant": "^4.8.3",
31 "vconsole": "^3.14.6", 31 "vconsole": "^3.14.6",
32 "vite-plugin-dynamic-import": "^0.9.6", 32 "vite-plugin-dynamic-import": "^0.9.6",
33 "vite-plugin-mp": "^1.6.1", 33 "vite-plugin-mp": "^1.6.1",
......
1 /* 1 /*
2 * @Date: 2022-05-18 22:56:08 2 * @Date: 2022-05-18 22:56:08
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2024-01-02 16:46:57 4 + * @LastEditTime: 2024-02-01 14:42:39
5 - * @FilePath: /tswj/src/api/fn.js 5 + * @FilePath: /my-record/src/api/fn.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
8 import axios from '@/utils/axios'; 8 import axios from '@/utils/axios';
...@@ -49,7 +49,7 @@ export const fn = (api) => { ...@@ -49,7 +49,7 @@ export const fn = (api) => {
49 export const uploadFn = (api) => { 49 export const uploadFn = (api) => {
50 return api 50 return api
51 .then(res => { 51 .then(res => {
52 - if (res.statusText === 'OK') { 52 + if (res.statusText === 'OK' || res.status === 200) {
53 return res.data || true; 53 return res.data || true;
54 } else { 54 } else {
55 // tslint:disable-next-line: no-console 55 // tslint:disable-next-line: no-console
......
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
2 * @Author: hookehuyr hookehuyr@gmail.com 2 * @Author: hookehuyr hookehuyr@gmail.com
3 * @Date: 2022-05-31 12:06:19 3 * @Date: 2022-05-31 12:06:19
4 * @LastEditors: hookehuyr hookehuyr@gmail.com 4 * @LastEditors: hookehuyr hookehuyr@gmail.com
5 - * @LastEditTime: 2023-12-31 17:32:10 5 + * @LastEditTime: 2024-02-01 11:59:18
6 - * @FilePath: /tswj/src/main.js 6 + * @FilePath: /my-record/src/main.js
7 * @Description: 7 * @Description:
8 */ 8 */
9 import { createApp } from 'vue'; 9 import { createApp } from 'vue';
10 -import { Button, Image as VanImage, Col, Row, Icon, Form, Field, CellGroup, ConfigProvider, Toast, Uploader, Empty, Tab, Tabs, Overlay, NumberKeyboard, Lazyload, List, PullRefresh, Popup, Picker, Sticky, Stepper, Tag, Swipe, SwipeItem, Dialog, ActionSheet, Loading, Checkbox, Search, Notify } from 'vant'; 10 +import { Button, Image as VanImage, Col, Row, Icon, Form, Field, CellGroup, ConfigProvider, Toast, Uploader, Empty, Tab, Tabs, Overlay, NumberKeyboard, Lazyload, List, PullRefresh, Popup, Picker, Sticky, Stepper, Tag, Swipe, SwipeItem, Dialog, ActionSheet, Loading, Checkbox, Search, Notify, FloatingPanel } from 'vant';
11 import router from './router'; 11 import router from './router';
12 import App from './App.vue'; 12 import App from './App.vue';
13 // import axios from './utils/axios'; 13 // import axios from './utils/axios';
...@@ -21,6 +21,6 @@ const app = createApp(App); ...@@ -21,6 +21,6 @@ const app = createApp(App);
21 21
22 app.config.globalProperties.$http = axios; // 关键语句 22 app.config.globalProperties.$http = axios; // 关键语句
23 23
24 -app.use(pinia).use(router).use(Button).use(VanImage).use(Col).use(Row).use(Icon).use(Form).use(Field).use(CellGroup).use(Toast).use(Uploader).use(Empty).use(Tab).use(Tabs).use(Overlay).use(NumberKeyboard).use(Lazyload).use(List).use(PullRefresh).use(Popup).use(Picker).use(Sticky).use(Stepper).use(Tag).use(Swipe).use(SwipeItem).use(Dialog).use(ActionSheet).use(Loading).use(Checkbox).use(Search).use(ConfigProvider).use(Notify); 24 +app.use(pinia).use(router).use(Button).use(VanImage).use(Col).use(Row).use(Icon).use(Form).use(Field).use(CellGroup).use(Toast).use(Uploader).use(Empty).use(Tab).use(Tabs).use(Overlay).use(NumberKeyboard).use(Lazyload).use(List).use(PullRefresh).use(Popup).use(Picker).use(Sticky).use(Stepper).use(Tag).use(Swipe).use(SwipeItem).use(Dialog).use(ActionSheet).use(Loading).use(Checkbox).use(Search).use(ConfigProvider).use(Notify).use(FloatingPanel);
25 25
26 app.mount('#app'); 26 app.mount('#app');
......
...@@ -10,4 +10,10 @@ export default [{ ...@@ -10,4 +10,10 @@ export default [{
10 meta: { 10 meta: {
11 title: 'record' 11 title: 'record'
12 } 12 }
13 +}, {
14 + path: '/h5-record',
15 + component: () => import('@/views/h5-record.vue'),
16 + meta: {
17 + title: 'h5-record'
18 + }
13 }]; 19 }];
......
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
2 * @Author: hookehuyr hookehuyr@gmail.com 2 * @Author: hookehuyr hookehuyr@gmail.com
3 * @Date: 2022-05-28 10:17:40 3 * @Date: 2022-05-28 10:17:40
4 * @LastEditors: hookehuyr hookehuyr@gmail.com 4 * @LastEditors: hookehuyr hookehuyr@gmail.com
5 - * @LastEditTime: 2024-01-02 18:02:31 5 + * @LastEditTime: 2024-02-01 14:34:51
6 - * @FilePath: /tswj/src/utils/axios.js 6 + * @FilePath: /my-record/src/utils/axios.js
7 * @Description: 7 * @Description:
8 */ 8 */
9 import axios from 'axios'; 9 import axios from 'axios';
......
1 +<!--
2 + * @Date: 2024-02-01 11:11:21
3 + * @LastEditors: hookehuyr hookehuyr@gmail.com
4 + * @LastEditTime: 2024-02-01 15:03:08
5 + * @FilePath: /my-record/src/views/h5-record.vue
6 + * @Description: 文件描述
7 +-->
8 +<template>
9 + <div class="h5-record">
10 + <div class="wave-wrapper">
11 + <div class="ctrlProcessWave wave-view"></div>
12 + <div class="wave-info">
13 + <div class="ctrlProcessX wave-info-top" :style="{ width: powerLevel + '%' }" ></div>
14 + <div class="ctrlProcessT wave-info-bottom" >
15 + {{ durationTxt + "/" + powerLevel }}
16 + </div>
17 + </div>
18 + </div>
19 +
20 + <div style="margin: 20px 10px;">
21 + <audio ref="LogAudioPlayer" style="width: 100%"></audio>
22 + </div>
23 +
24 + <van-floating-panel>
25 + <van-cell-group>
26 + <van-cell title="打开录音,请求权限" @click.native="recOpen" />
27 + <van-cell title="关闭录音,释放资源" @click.native="recClose" />
28 + <van-cell title="录制" @click.native="recStart" />
29 + <van-cell title="暂停" @click.native="recPause" />
30 + <van-cell title="继续录音" @click.native="recResume" />
31 + <van-cell title="停止录音" @click.native="recStop" />
32 + <van-cell title="播放录音" @click.native="recPlayLast" />
33 + <van-cell title="下载录音" @click.native="recDownLast" />
34 + <van-cell title="上传录音" @click.native="recUploadLast" />
35 + </van-cell-group>
36 + </van-floating-panel>
37 +
38 + </div>
39 +</template>
40 +
41 +<script setup>
42 +import { ref } from 'vue'
43 +import { useRoute, useRouter } from 'vue-router'
44 +
45 +import { Cookies, $, _, axios, storeToRefs, mainStore, Toast, useTitle } from '@/utils/generatePackage.js'
46 +//import { } from '@/utils/generateModules.js'
47 +//import { } from '@/utils/generateIcons.js'
48 +//import { } from '@/composables'
49 +import { v4 as uuidv4 } from 'uuid';
50 +import { qiniuTokenAPI, qiniuUploadAPI, saveFileAPI } from '@/api/common'
51 +//加载必须要的core,demo简化起见采用的直接加载类库,实际使用时应当采用异步按需加载
52 +import Recorder from "recorder-core";
53 +//需要使用到的音频格式编码引擎的js文件统统加载进来,这些引擎文件会比较大
54 +import "recorder-core/src/engine/mp3";
55 +import "recorder-core/src/engine/mp3-engine";
56 +//可选的扩展
57 +import "recorder-core/src/extensions/waveview";
58 +
59 +const $route = useRoute();
60 +const $router = useRouter();
61 +useTitle($route.meta.title);
62 +
63 +const type = ref("mp3"); // 音频类型
64 +const bitRate = ref(32); // 比特率
65 +const sampleRate = ref(32000); // 采样率
66 +const duration = ref(0);
67 +const durationTxt = ref("0");
68 +const powerLevel = ref(0);
69 +const logs = ref([]);
70 +const recLogLast = ref({});
71 +const LogAudioPlayer = ref(null);
72 +
73 +const rec = ref(null); // 录音控件实例
74 +const wave = ref(null); // 波形绘制对象
75 +
76 +const formatMs = (ms, all) => { // 格式化时间显示
77 + var ss = ms % 1000;
78 + ms = (ms - ss) / 1000;
79 + var s = ms % 60;
80 + ms = (ms - s) / 60;
81 + var m = ms % 60;
82 + ms = (ms - m) / 60;
83 + var h = ms;
84 + var t =
85 + (h ? h + ":" : "") +
86 + (all || h + m ? ("0" + m).substr(-2) + ":" : "") +
87 + (all || h + m + s ? ("0" + s).substr(-2) + "″" : "") +
88 + ("00" + ss).substr(-3);
89 + return t;
90 +}
91 +/**
92 + * 录音日志
93 + * @param {String} msg 日志内容
94 + * @param {String} color 日志颜色
95 + * @param {Object} res 日志资源
96 + */
97 +const reclog = (msg, color, res) => {
98 + var obj = {
99 + idx: logs.value.length,
100 + msg: msg,
101 + color: color,
102 + res: res,
103 + playMsg: "",
104 + down: 0,
105 + down64Val: "",
106 + };
107 + if (res && res.blob) {
108 + recLogLast.value = obj;
109 + }
110 + logs.value.splice(0, 0, obj);
111 +}
112 +
113 +/**
114 + * 打开录音
115 + */
116 +const recOpen = () => {
117 + rec.value = Recorder({
118 + type: type.value,
119 + bitRate: +bitRate.value,
120 + sampleRate: +sampleRate.value,
121 + onProcess: function (buffers, p, d, sampleRate) { // 播放进程中
122 + duration.value = d;
123 + durationTxt.value = formatMs(d, 1);
124 + powerLevel.value = p;
125 + // 改变波纹显示
126 + wave.value.input(buffers[buffers.length - 1], p, sampleRate);
127 + },
128 + });
129 +
130 + rec.value.open(
131 + () => { // 成功回调
132 + reclog(
133 + "已打开:" + type.value + " " + sampleRate.value + "hz " + bitRate.value + "kbps",
134 + 2
135 + );
136 +
137 + wave.value = Recorder.WaveView({ elem: ".ctrlProcessWave" });
138 + },
139 + (msg, isUserNotAllow) => { // 失败回调
140 + reclog((isUserNotAllow ? "UserNotAllow," : "") + "打开失败:" + msg, 1);
141 + }
142 + );
143 +}
144 +/**
145 + * 关闭录音
146 + */
147 +const recClose = () => {
148 + if (rec.value) {
149 + rec.value.close();
150 + reclog("已关闭");
151 + } else {
152 + reclog("未打开录音", 1);
153 + }
154 +}
155 +/**
156 + * 开始录音
157 + */
158 +const recStart = () => {
159 + if (!rec.value || !Recorder.IsOpen()) {
160 + reclog("未打开录音", 1);
161 + return;
162 + }
163 + rec.value.start();
164 +
165 + let set = rec.value.set;
166 + reclog(
167 + "录制中:" + set.type + " " + set.sampleRate + "hz " + set.bitRate + "kbps"
168 + );
169 +}
170 +/**
171 + * 暂停录音
172 + */
173 +const recPause = () => {
174 + if (rec.value && Recorder.IsOpen()) {
175 + rec.value.pause();
176 + } else {
177 + reclog("未打开录音", 1);
178 + }
179 +}
180 +/**
181 + * 继续录音
182 + */
183 +const recResume = () => {
184 + if (rec.value && Recorder.IsOpen()) {
185 + rec.value.resume();
186 + } else {
187 + reclog("未打开录音", 1);
188 + }
189 +}
190 +/**
191 + * 停止录音
192 + */
193 +const recStop = () => {
194 + if (!(rec.value && Recorder.IsOpen())) {
195 + reclog("未打开录音", 1);
196 + return;
197 + }
198 +
199 + rec.value.stop(
200 + function (blob, duration) {
201 + reclog("已录制:", "", {
202 + blob: blob,
203 + duration: duration,
204 + durationTxt: formatMs(duration),
205 + rec: rec,
206 + });
207 + },
208 + function (s) {
209 + reclog("录音失败:" + s, 1);
210 + }
211 + );
212 +}
213 +
214 +const recPlay = (idx) => {
215 + let o = logs.value[logs.value.length - idx - 1];
216 + o.play = (o.play || 0) + 1;
217 +
218 + let audio = LogAudioPlayer.value;
219 + audio.controls = true;
220 + if (!(audio.ended || audio.paused)) {
221 + audio.pause();
222 + }
223 + audio.onerror = function (e) {
224 + console.warn("播放失败:" + e);
225 + };
226 + audio.src = (window.URL || webkitURL).createObjectURL(o.res.blob);
227 + audio.play();
228 +}
229 +/**
230 + * 播放录音
231 + */
232 +const recPlayLast = () => {
233 + if (!recLogLast.value) {
234 + reclog("请先录音,然后停止后再播放", 1);
235 + return;
236 + }
237 + recPlay(recLogLast.value.idx);
238 +}
239 +
240 +const recDown = (idx) => {
241 + let o = logs.value[logs.value.length - idx - 1];
242 + o.down = (o.down || 0) + 1;
243 +
244 + o = o.res;
245 + let name =
246 + "rec-" +
247 + o.duration +
248 + "ms-" +
249 + (o.rec.set.bitRate || "-") +
250 + "kbps-" +
251 + (o.rec.set.sampleRate || "-") +
252 + "hz." +
253 + (o.rec.set.type || (/\w+$/.exec(o.blob.type) || [])[0] || "unknown");
254 + var downA = document.createElement("A");
255 + downA.href = (window.URL || webkitURL).createObjectURL(o.blob);
256 + downA.download = name;
257 + downA.click();
258 +}
259 +/**
260 + * 下载录音
261 + */
262 +const recDownLast = () => {
263 + if (!recLogLast.value) {
264 + reclog("请先录音,然后停止后再下载", 1);
265 + return;
266 + }
267 + recDown(recLogLast.value.idx);
268 +}
269 +/**
270 + * 转换格式base64
271 + */
272 +const recDown64 = (idx) => {
273 + let o = logs.value[logs.value.length - idx - 1];
274 + let reader = new FileReader();
275 + reader.onloadend = function () {
276 + o.down64Val = reader.result;
277 + };
278 + reader.readAsDataURL(o.res.blob);
279 +}
280 +/**
281 + * 上传录音
282 + */
283 +const recUploadLast = () => {
284 + if (!recLogLast.value) {
285 + reclog("请先录音,然后停止后再上传", 1);
286 + return;
287 + }
288 + let blob = recLogLast.value.res.blob;
289 +
290 + let reader = new FileReader();
291 + reader.onloadend = async function () {
292 + let base64url = encodeURIComponent((/.+;\s*base64\s*,\s*(.+)$/i.exec(reader.result) || [])[1]); //录音文件内容,后端进行base64解码成二进制
293 +
294 + let affix = uuidv4();
295 +
296 + // TAG: 获取token和保存的接口应该有问题
297 + // 获取七牛token
298 + const { token, key, code } = await qiniuTokenAPI({ filename: `${affix}_my_record.mp3`, file: base64url });
299 + if (code) {
300 + let formData = new FormData();
301 + formData.append("file", base64url); // 通过append向form对象添加数据
302 + formData.append("token", token);
303 + formData.append("key", `${affix}_my_record.mp3`);
304 + let config = {
305 + headers: { "Content-Type": "multipart/form-data" },
306 + };
307 + // 自拍图片上传七牛服务器
308 + let qiniuUploadUrl;
309 + if (window.location.protocol === 'https:') {
310 + qiniuUploadUrl = 'https://up.qbox.me';
311 + } else {
312 + qiniuUploadUrl = 'http://upload.qiniu.com';
313 + }
314 + const { filekey, hash } = await qiniuUploadAPI(
315 + qiniuUploadUrl,
316 + formData,
317 + config
318 + );
319 + if (filekey) {
320 + const { data } = await saveFileAPI({ filekey, hash });
321 + // console.warn(filekey)
322 + }
323 + }
324 + };
325 + reader.readAsDataURL(blob);
326 +}
327 +</script>
328 +
329 +<style lang="less" scoped>
330 +.h5-record {
331 + background-color: #f9f9f9;
332 + height: 100vh;
333 + .wave-wrapper {
334 + .wave-view {
335 + height: 100px;
336 + width: 100vw;
337 + border: 1px solid #ccc;
338 + box-sizing: border-box;
339 + display: inline-block;
340 + vertical-align: bottom;
341 + }
342 + .wave-info {
343 + height: 40px;
344 + width: 100vw;
345 + display: inline-block;
346 + background: #999;
347 + position: relative;
348 + vertical-align: bottom;
349 + .wave-info-top {
350 + position: absolute;
351 + height: 40px;
352 + background: #0b1;
353 + }
354 + .wave-info-bottom {
355 + position: relative;
356 + padding-left: 50px;
357 + line-height: 40px;
358 + }
359 + }
360 + }
361 +}
362 +</style>
1 <!-- 1 <!--
2 * @Date: 2024-02-01 09:45:51 2 * @Date: 2024-02-01 09:45:51
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2024-02-01 09:47:13 4 + * @LastEditTime: 2024-02-01 18:29:41
5 * @FilePath: /my-record/src/views/record.vue 5 * @FilePath: /my-record/src/views/record.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -140,7 +140,8 @@ a:hover { ...@@ -140,7 +140,8 @@ a:hover {
140 140
141 <div class="mainBox"> 141 <div class="mainBox">
142 <!-- 放一个 <audio ></audio> 播放器,标签名字大写,阻止uniapp里面乱编译 --> 142 <!-- 放一个 <audio ></audio> 播放器,标签名字大写,阻止uniapp里面乱编译 -->
143 - <AUDIO ref="LogAudioPlayer" style="width: 100%"></AUDIO> 143 + <!-- <AUDIO ref="LogAudioPlayer" style="width: 100%"></AUDIO> -->
144 + <audio ref="LogAudioPlayer" style="width: 100%"></audio>
144 145
145 <div class="mainLog"> 146 <div class="mainLog">
146 <div v-for="obj in logs" :key="obj.idx"> 147 <div v-for="obj in logs" :key="obj.idx">
...@@ -202,9 +203,9 @@ export default { ...@@ -202,9 +203,9 @@ export default {
202 logs: [], 203 logs: [],
203 }; 204 };
204 }, 205 },
205 - created: function () { 206 + // created: function () {
206 - this.Rec = Recorder; 207 + // this.Rec = Recorder;
207 - }, 208 + // },
208 methods: { 209 methods: {
209 recOpen: function () { 210 recOpen: function () {
210 var This = this; 211 var This = this;
...@@ -293,7 +294,6 @@ export default { ...@@ -293,7 +294,6 @@ export default {
293 } 294 }
294 ); 295 );
295 }, 296 },
296 -
297 recPlayLast: function () { 297 recPlayLast: function () {
298 if (!this.recLogLast) { 298 if (!this.recLogLast) {
299 this.reclog("请先录音,然后停止后再播放", 1); 299 this.reclog("请先录音,然后停止后再播放", 1);
......
...@@ -2916,10 +2916,10 @@ uuid@^8.3.2: ...@@ -2916,10 +2916,10 @@ uuid@^8.3.2:
2916 resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" 2916 resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"
2917 integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== 2917 integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
2918 2918
2919 -vant@^4.8.1: 2919 +vant@^4.8.3:
2920 - version "4.8.1" 2920 + version "4.8.3"
2921 - resolved "https://mirrors.cloud.tencent.com/npm/vant/-/vant-4.8.1.tgz" 2921 + resolved "https://mirrors.cloud.tencent.com/npm/vant/-/vant-4.8.3.tgz#f9a4d57c331edffc2174dfd26c1d0e6e1cdb6cae"
2922 - integrity sha512-SkFZM3Z3Bwi5do+iQNfRgDi7b+Ka29rUUNzck06W2KoFie3CLTqSifLa5TuZCEoXPSkqR+fRH/VE5G57mmL8sg== 2922 + integrity sha512-swJdNeeBKOx80O7z3NiQuVhYROnagjCT6zgOMK9Tsbki9AIf3pOZVk8605AuM7CFX4ZS/CndhKoIFlavob6c4A==
2923 dependencies: 2923 dependencies:
2924 "@vant/popperjs" "^1.3.0" 2924 "@vant/popperjs" "^1.3.0"
2925 "@vant/use" "^1.6.0" 2925 "@vant/use" "^1.6.0"
......