APP- 录音录制上传下载播放的实现 -YB
来源自己有道笔记:
【有道云笔记】20241129 APP 录音功能调研:https://note.youdao.com/s/Ckfbvrah
一、先说结论:
实现效果:在 apicloud 平台下分别写 html 方式和 vue 方式(V52)实现。录音操作效果录屏如下:
- html 方式的录屏:
视频地址:https://yuanbao-oss.oss-cn-shenzhen.aliyuncs.com/img/public_imgs/PicGo/202412161615528.mp4
- vue 方式(V52 项目应用)的录屏::
视频地址:https://yuanbao-oss.oss-cn-shenzhen.aliyuncs.com/img/public_imgs/PicGo/202412161617457.mp4
二、录音功能调研实现过程:
2.1 原始需求:
设备模块的 APP 页面,需要语音录入保存到单据上作为附件,后续可以在其他页面播放这个录音。
2.2 功能拆解
按这个功能机制拆分为三个步骤:
-
录音:生成录音文件;
-
将录音文件上传到后端;
-
从后端下载获取后播放录音文件;
2.3 调研各步骤的实现方式
1、录音:
用友(APICLOUD)平台,API 方式或模块方式,各有相关逻辑。
-
API 方式:
-
-
使用 api.startRecord()、api.startRecord() 作开始 / 停止录制。
-
-
** 模块方式:** 商城里部分模块收费、少部分免费。
-
-
收费:audioRecorder 和 mp3Recorder 模块。
-
- audioRecorder 模块收费 64 块钱 / 每年。贵了点。为官方提供。
-
- mp3Recorder:非官方。
- 免费:FNRecordMp3 和 recordForMP3
接口 API 文档:https://developer.yonyou.com/docs/Client-API/Func-Ext/FNRecordMp3#open
接口 API 文档:https://developer.yonyou.com/docs/Client-API/Func-Ext/recordForMP3
2、将录音文件上传到后端; (以下为代码片段为前期参考,完整可用代码见文末 DEMO 代码)
var url = “http://” + serverIp + “:” + serverPort + “/” + serverProject + “/padController!uploadPhotos4APP.m”;
// 上传图片
// url:上传的url地址
// data:上传的文件
// callback:上传成功返回地址
function uploadFile(url, data) {
var params = new Object();
params.url = url;
params.timeout = 30;
params.dataType = 'json';
params.method = 'post';
var data1 = {
files: {
file: data
}
};
api.ajax({
url: url,
method: 'post',
data: {
files: {
//file: 'fs://a.gif'
file: data
}
}
}, function (res, err) {
if (res) {
// alert( JSON.stringify( res ) );
} else {
// alert( JSON.stringify( res ) );
}
});
}
3、从后端获取后播放录音文件 (以下为代码片段为前期参考,完整可用代码见文末 DEMO 代码)
分为两步:
最好是先判断这个文件是否存在,不存在再下载,若存在则直接走播放。
api.download({
url: downloadUrl,
report: true,
savePath: 'fs://'+pdfUrl,
}, function(ret, err){
if (ret && 1 == ret.state) {/* 下载完成 */
savePath = ret.savePath;
console.log(ret.savePath);
var fs = api.require('fs');
fs.rename({
oldPath: 'fs://'+pdfUrl,
newPath: 'fs://C-'+pdfUrl
}, function(ret, err) {
//下载完成后调用PDF批注模块
var modulePDF = api.require('modulePDF');
modulePDF.open({
x:0,
y:headerPos.h,
w:api.winWidth,
h:frameH,
appId : api.appId + '',//当前应用的appid
inpath: 'C-'+pdfUrl,//输入的pdf文件名,路径为sdcard/UZMap/ + appId + /123.pdf
outpath: pdfUrl//输出的pdf文件名,路径为sdcard/UZMap/ + appId + /tttt.pdf
},function(ret,err){
});
});
}
});
- 播放音频文件:
api.startPlay({
path: 'widget://res/1.mp3'
}, function(ret, err) {
if (ret) {
api.alert({
msg:'播放完成'
});
} else {
api.alert({
msg:JSON.stringify(err)
});
}
});
暂停播放:api.stopPlay() 不需要传参
停止播放:api.stopPlay() 不需要传参
三、完成 DEMO 开发测试:
效果差不多符合预期。页面如下:
再引入到 MOM V5.2 项目 APP 中:
四、附上可用的 DEMO 代码:
1、主要 js:
// 录音开始
function testAudioRecordStart() {
let fileName = this.generateUniqueFileName('AUDIO');
let param = {
type: 'wav',
path: 'fs://' + fileName + '.wav',
}
api.startRecord(param);
api.toast({
msg: '录音开始...',
duration: 3000,
location: 'top'
});
}
// 录音结束-本地
function testAudioRecordStop() {
api.stopRecord(function(ret, err) {
if (ret) {
var path = ret.path; // 音频文件的路径
var duration = ret.duration; // 音频文件的时长
console.log('audio 路径:' + path);
console.log('audio 时长:' + duration);
audioPath = path;
}
api.toast({
msg: '录音结束,路径:' + ret.path + ",时长:" + ret.duration,
duration: 3000,
location: 'top'
});
});
}
// 录音播放-本地
function testAudioPlay() {
startPlay(audioPath);
}
// 解析路径播放录音文件
function startPlay(audioPath) {
// 判断录音文件路径是否为空
if (!audioPath) {
alert("录音文件路径为空,请先完成录制。");
return;
}
// 截取出文件名
console.log("文件路径:" + audioPath);
let fileName = getFileNameFromPath(audioPath);
console.log("文件名称:" + fileName);
api.startPlay({
path: 'fs://' + fileName,
}, function(ret, err) {
if (ret) {
api.toast({
msg: '播放完成!',
duration: 3000,
location: 'top'
});
} else {
api.alert({
msg:JSON.stringify(err)
});
}
});
}
// 停止播放
function testAudioPlayStop()
{
api.stopPlay()
}
// 从文件路径中截取出文件名
function getFileNameFromPath(path) {
// 使用 '/' 作为分隔符将路径分割成数组
var parts = path.split('/');
// 数组的最后一项即为文件名
return parts[parts.length - 1];
}
// 生成唯一文件名:基础名_时间戳_随机数
function generateUniqueFileName(baseName) {
// 获取当前时间戳(毫秒级别)
const timestamp = new Date().getTime();
// 生成一个4位随机数
const randomPart = Math.floor(1000 + Math.random() * 9000);
// 拼接成最终文件名
return `${baseName}_${timestamp}_${randomPart}`;
}
// 录音结束-上传到服务器
function testAudioRecordStopUpload()
{
api.stopRecord(function(ret, err) {
if (ret) {
var path = ret.path; // 音频文件的路径
var duration = ret.duration; // 音频文件的时长
console.log('audio 路径:' + path);
console.log('audio 时长:' + duration);
audioPath = path;
// 截取出文件名
console.log("文件路径:" + audioPath);
let fileName = getFileNameFromPath(audioPath);
console.log("文件名称:" + fileName);
let url = serverUrl + "ybTestController!uploadAudio.m";
api.ajax({
url: url,
method: 'post',
data: {
files: {
file: 'fs://' + fileName,
}
}
}, function (res, err) {
console.log( JSON.stringify( res ) );
if (res && res.type) {
api.toast({
msg: '录音结束,并上传文件成功,objId:' + res.data,
duration: 3000,
location: 'top'
});
// 赋值文件对象GID
fileGid = res.data;
} else {
alert( JSON.stringify( err ) );
}
});
}
});
}
// 录音播放-从服务器上下载后播放
function testAudioPlayDownload() {
// 判断录音文件路径是否为空
if (!fileGid) {
alert("录音文件对象为空,请先完成录制上传。");
return;
}
let url = serverUrl + "ybTestController!downloadAudio.m?id=" + fileGid;
let fileName = new Date().getTime() + '.wav'; // 用时间戳作名称,下载不用加随机数
api.download({
url: url,
report: true,
savePath: 'fs://' + fileName,
}, function(ret, err){
/* 下载进行中时 */
if(ret && 0 == ret.state){
api.toast({
msg: "正在下载音频" + ret.percent + "%",
duration: 2000
});
}
/* 下载完成,之后进行播报 */
if (ret && 1 == ret.state) {
var savePath = ret.savePath;
console.log('savePath :' + savePath);
startPlay(savePath);
}
/* 下载失败 */
if(ret && 2 == ret.state){
api.toast({
msg: "下载失败",
duration: 5000
});
}
});
}
2、vue 完整页面,自行提取所需内容块。
完整 vue 代码文件: 见打赏区。
vue 上传代码:
stopRecord() {
// 判断任务单是否选择
if (!this.taskId) {
alert("请先选择任务单!");
return;
}
console.log("-------------:停止录音");
let self = this;
this.recordAudioText = "录音录制开始";
this.recordAudioColor = "primary";
document.getElementById("listen_gif").style.display = "none";
api.stopRecord(function(ret, err) {
if (ret) {
var path = ret.path; // 音频文件的路径
var duration = ret.duration; // 音频文件的时长
console.log("audio 路径:" + path);
console.log("audio 时长:" + duration);
self.audioPath = path;
// 如果录音时间过短,则提示录音时间过短,不作保存
if (duration < 1) {
api.toast({
msg: "录音时间过短,少于一秒,请重新录音!",
duration: 3000,
location: "top"
});
return;
}
// 截取出文件名
console.log("文件路径:" + self.audioPath);
let fileName = self.getFileNameFromPath(self.audioPath);
console.log("文件名称:" + fileName);
let url = "uempEquipCheckController!uploadAudio.m?taskId=" + self.taskId;
// 只能用原生api.ajax方式才能获取录音文件上传,用http.post方式上传不了
// var serverUrl = http.getPrefix(); // "http://10.1.8.246/prj-v5-web/";
var serverUrl = self.getServerUrl(); // 自己写获取服务地址前缀的方法
console.log("serverUrl:" + serverUrl);
api.ajax(
{
url: serverUrl + url,
method: "post",
data: {
files: {
file: "fs://audio/" + fileName
},
}
},
function(res, err) {
console.log(JSON.stringify(res));
if (res && res.type == "success") {
api.toast({
msg: "录音结束,并上传文件成功,objId:" + res.data,
duration: 3000,
location: "top"
});
// 赋值文件对象GID
self.fileGid = res.data;
} else {
alert(JSON.stringify(res));
}
}
);
}
});
},
3、后端代码:
测试时:
正式使用时:
注意:后端的内容没什么差别,主要还是前端的上传写法不太一样。