title: 微信 JS-SDK 开发实现录音和图片上传功能
date: 2018-08-13 19:00:00
toc: true #是否显示目录 table of contents
tags:html
现有一个 .NET 开发的 Wap 网站项目,须要用到录音和图片上传功能。目前该网站只部署在公众号中使用,而且手机录音功能的实现只能依赖于微信的接口(详见另外一篇文章《HTML5 实现手机原生功能》),另外采用即时拍照来上传图片的功能也只能调用微信接口才能实现,因此本文简要地记录下后端 .NET 、前端 H5 开发的网站如何调用微信接口实现录音和即时拍照上传图片功能。前端
一、(必须配置)打开微信公众平台-公众号设置-功能设置-JS接口安全域名 ,按提示配置。须要将网站发布到服务器上并绑定域名。加xx.com便可,yy.xx.com也能调用成功。web
JS接口安全域名ajax
设置JS接口安全域名后,公众号开发者可在该域名下调用微信开放的JS接口。
注意事项:
一、可填写三个域名或路径(例:wx.qq.com或wx.qq.com/mp),需使用字母、数字及“-”的组合,不支持IP地址、端口号及短链域名。
二、填写的域名须经过ICP备案的验证。
三、 将文件 MP_verify_iBVYET3obIwgppnr.txt(点击下载)上传至填写域名或路径指向的web服务器(或虚拟主机)的目录(若填写域名,将文件放置在域名根目录下,例如wx.qq.com/MP_verify_iBVYET3obIwgppnr.txt;若填写路径,将文件放置在路径目录下,例如wx.qq.com/mp/MP_verify_iBVYET3obIwgppnr.txt),并确保能够访问。
四、 一个天然月内最多可修改并保存三次,本月剩余保存次数:3redis
二、打开微信公众平台-公众号设置-功能设置-网页受权域名,按提示配置(这一步在当前开发需求中可能不须要)。算法
三、(必须配置)打开微信公众平台-基本配置-公众号开发信息-IP白名单,配置网页服务器的公网IP(经过开发者IP及密码调用获取access_token 接口时,须要设置访问来源IP为白名单)。json
微信开发者工具方便公众号网页和微信小程序在PC上进行调试,下载地址 ,说明文档 。小程序
参考微信公众平台技术文档,其中的《附录1-JS-SDK使用权限签名算法》(关于受权的文档找不到了)。c#
/// <summary> /// 获取AccessToken /// 参考 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183 /// </summary> /// <returns>access_toke</returns> public string GetAccessToken() { var cache = JCache<string>.Instance; var config = Server.ServerManage.Config.Weixin; // 获取并缓存 access_token string access_token = cache.GetOrAdd("access_token", "weixin", (i, j) => { var url = "https://api.weixin.qq.com/cgi-bin/token";// 注意这里不是用"https://api.weixin.qq.com/sns/oauth2/access_token" var result = HttpHelper.HttpGet(url, "appid=" + config.AppId + "&secret=" + config.AppSecret + "&grant_type=client_credential"); // 正常状况返回 {"access_token":"ACCESS_TOKEN","expires_in":7200} // 错误时返回 {"errcode":40013,"errmsg":"invalid appid"} var token = result.FormJObject(); if (token["errcode"] != null) { throw new JException("微信接口异常access_token:" + (string)token["errmsg"]); } return (string)token["access_token"]; }, new TimeSpan(0, 0, 7200)); return access_token; }
/// <summary> /// 获取jsapi_ticket /// jsapi_ticket是公众号用于调用微信JS接口的临时票据。 /// 正常状况下,jsapi_ticket的有效期为7200秒,经过access_token来获取。 /// 因为获取jsapi_ticket的api调用次数很是有限,频繁刷新jsapi_ticket会致使api调用受限,影响自身业务,开发者必须在本身的服务全局缓存jsapi_ticket 。 /// </summary> public string GetJsapiTicket() { var cache = JCache<string>.Instance; var config = Server.ServerManage.Config.Weixin; // 获取并缓存 jsapi_ticket string jsapi_ticket = cache.GetOrAdd("jsapi_ticket", "weixin", (k, r) => { var access_token = GetAccessToken(); var url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket"; var result = HttpHelper.HttpGet(url, "access_token=" + access_token + "&type=jsapi"); // 返回格式 {"errcode":0,"errmsg":"ok","ticket":"字符串","expires_in":7200} var ticket = result.FormJObject(); if ((string)ticket["errmsg"] != "ok") { throw new JException("微信接口异常ticket:" + (string)ticket["errmsg"]); } return (string)ticket["ticket"]; }, new TimeSpan(0, 0, 7200)); return jsapi_ticket; }
/// <summary> /// 微信相关的接口 /// </summary> public class WeixinApi { /// <summary> /// 获取微信签名 /// </summary> /// <param name="url">请求的页面地址</param> /// <param name="config">微信配置</param> /// <returns></returns> public static object GetSignature(string url) { var config = Park.Server.ServerManage.Config.Weixin; var jsapi_ticket = GetJsapiTicket(); // SHA1加密 // 对全部待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串 var obj = new { jsapi_ticket = jsapi_ticket, //必须与wx.config中的nonceStr和timestamp相同 noncestr = JString.GenerateNonceStr(), timestamp = JString.GenerateTimeStamp(), url = url, // 必须是调用JS接口页面的完整URL(location.href.split('#')[0]) }; var str = $"jsapi_ticket={obj.jsapi_ticket}&noncestr={obj.noncestr}×tamp={obj.timestamp}&url={obj.url}"; var signature = FormsAuthentication.HashPasswordForStoringInConfigFile(str, "SHA1"); return new { appid = config.AppId, noncestr = obj.noncestr, timestamp = obj.timestamp, signature = signature, }; } }
/// <summary> /// 微信JS-SDK接口 /// </summary> public class WeixinController : BaseController { /// <summary> /// 获取微信签名 /// </summary> /// <param name="url">请求的页面地址</param> /// <returns>签名信息</returns> [HttpGet] public JResult GetSignature(string url) { return JResult.Invoke(() => { return WeixinApi.GetSignature(url); }); } }
/* -----------------------调用微信JS-SDK接口的JS封装-------------------------------- */ if (Park && Park.Api) { Park.Weixin = { // 初始化配置 initConfig: function (fn) { // todo: 读取本地wx.config信息,若没有或过时则从新请求 var url = location.href.split('#')[0]; Park.get("/Weixin/GetSignature", { url: url }, function (d) { Park.log(d.Data); if (d.Status) { wx.config({ debug: false, // 开启调试模式,调用的全部api的返回值会在客户端alert出来,若要查看传入的参数,能够在pc端打开,参数信息会经过log打出,仅在pc端时才会打印。 appId: d.Data.appid,// 必填,公众号的惟一标识 nonceStr: d.Data.noncestr,// 必填,生成签名的随机串 timestamp: d.Data.timestamp,// 必填,生成签名的时间戳 signature: d.Data.signature,// 必填,签名,见附录1 jsApiList: [ // 必填,须要使用的JS接口列表,全部JS接口列表见附录2 'checkJsApi', 'translateVoice', 'startRecord', 'stopRecord', 'onVoiceRecordEnd', 'playVoice', 'onVoicePlayEnd', 'pauseVoice', 'stopVoice', 'uploadVoice', 'downloadVoice', 'chooseImage', 'previewImage', 'uploadImage', 'downloadImage', 'getNetworkType', 'openLocation', 'getLocation', ] }); wx.ready(function () { fn && fn(); }); } }); }, // 初始化录音功能 initUploadVoice: function (selector, fn) { var voiceUpload = false; // 是否可上传(避免60秒自动中止录音和松手中止录音重复上传) // 用localStorage进行记录,以前没有受权的话,先触发录音受权,避免影响后续交互 if (!localStorage.allowRecord || localStorage.allowRecord !== 'true') { wx.startRecord({ success: function () { localStorage.allowRecord = 'true'; // 仅仅为了受权,因此马上停掉 wx.stopRecord(); }, cancel: function () { alert('用户拒绝受权录音'); } }); } var btnRecord = $("" + selector); btnRecord.on('touchstart', function (event) { event.preventDefault(); btnRecord.addClass('hold'); startTime = new Date().getTime(); // 延时后录音,避免误操做 recordTimer = setTimeout(function () { wx.startRecord({ success: function () { voiceUpload = true; }, cancel: function () { alert('用户拒绝受权录音'); } }); }, 300); }).on('touchend', function (event) { event.preventDefault(); btnRecord.removeClass('hold'); // 间隔过短 if (new Date().getTime() - startTime < 300) { startTime = 0; // 不录音 clearTimeout(recordTimer); alert('录音时间过短'); } else { // 松手结束录音 if(voiceUpload){ voiceUpload = false; wx.stopRecord({ success: function (res) { // 上传到本地服务器 wxUploadVoice(res.localId, fn); }, fail: function (res) { alert(JSON.stringify(res)); } }); } } }); // 微信60秒自动触发中止录音 wx.onVoiceRecordEnd({ // 录音时间超过一分钟没有中止的时候会执行 complete 回调 complete: function (res) { voiceUpload = false; alert("录音时长不超过60秒"); // 上传到本地服务器 wxUploadVoice(res.localId, fn); } }); }, // 初始化图片功能 initUploadImage: function (selector, fn, num) { // 图片上传功能 // 参考 https://blog.csdn.net/fengqingtao2008/article/details/51469705 // 本地预览及删除功能参考 https://www.cnblogs.com/clwhxhn/p/6688571.html $("" + selector).click(function () { wx.chooseImage({ count: num || 1, // 默认9 sizeType: ['original', 'compressed'], // 能够指定是原图仍是压缩图,默认两者都有 sourceType: ['album', 'camera'], // 能够指定来源是相册仍是相机,默认两者都有 success: function (res) {//微信返回了一个资源对象 //localIds = res.localIds;//把图片的路径保存在images[localId]中--图片本地的id信息,用于上传图片到微信浏览器时使用 // 上传到本地服务器 wxUploadImage(res.localIds, 0, fn); } }); }); }, } } //上传录音到本地服务器,并作业务逻辑处理 function wxUploadVoice(localId, fn) { //调用微信的上传录音接口把本地录音先上传到微信的服务器 //不过,微信只保留3天,而咱们须要长期保存,咱们须要把资源从微信服务器下载到本身的服务器 wx.uploadVoice({ localId: localId, // 须要上传的音频的本地ID,由stopRecord接口得到 isShowProgressTips: 1, // 默认为1,显示进度提示 success: function (res) { //把录音在微信服务器上的id(res.serverId)发送到本身的服务器供下载。 $.ajax({ url: Park.getApiUrl('/Weixin/DownLoadVoice'), type: 'get', data: res, dataType: "json", success: function (d) { if (d.Status) { fn && fn(d.Data); } else { alert(d.Msg); } }, error: function (xhr, errorType, error) { console.log(error); } }); }, fail: function (res) { // 60秒的语音这里报错:{"errMsg":"uploadVoice:missing arguments"} alert(JSON.stringify(res)); } }); } //上传图片到微信,下载到本地,并作业务逻辑处理 function wxUploadImage(localIds, i, fn) { var length = localIds.length; //本次要上传全部图片的数量 wx.uploadImage({ localId: localIds[i], //图片在本地的id success: function (res) {//上传图片到微信成功的回调函数 会返回一个媒体对象 存储了图片在微信的id //把录音在微信服务器上的id(res.serverId)发送到本身的服务器供下载。 $.ajax({ url: Park.getApiUrl('/Weixin/DownLoadImage'), type: 'get', data: res, dataType: "json", success: function (d) { if (d.Status) { fn && fn(d.Data); } else { alert(d.Msg); } i++; if (i < length) { wxUploadImage(localIds, i, fn); } }, error: function (xhr, errorType, error) { console.log(error); } }); }, fail: function (res) { alert(JSON.stringify(res)); } }); };
// 页面上调用微信接口JS Park.Weixin.initConfig(function () { Park.Weixin.initUploadVoice("#voice-dp", function (d) { // 业务逻辑处理(d为录音文件在本地服务器的资源路径) }); Park.Weixin.initUploadImage("#img-dp", function (d) { // 业务逻辑处理(d为图片文件在本地服务器的资源路径) $("#img").append("<img src='" + Park.getImgUrl(d) + "' />"); }) });
// 即第4步中的'/Weixin/DownLoadVoice'和'/Weixin/DownLoadImage'方法的后端实现 /// <summary> /// 微信JS-SDK接口控制器 /// </summary> public class WeixinController : BaseController { /// <summary> /// 下载微信语音文件 /// </summary> /// <link>https://www.cnblogs.com/hbh123/archive/2017/08/15/7368251.html</link> /// <param name="serverId">语音的微信服务器端ID</param> /// <returns>录音保存路径</returns> [HttpGet] public JResult DownLoadVoice(string serverId) { return JResult.Invoke(() => { return WeixinApi.GetVoicePath(serverId); }); } /// <summary> /// 下载微信语音文件 /// </summary> /// <link>https://blog.csdn.net/fengqingtao2008/article/details/51469705</link> /// <param name="serverId">图片的微信服务器端ID</param> /// <returns>图片保存路径</returns> [HttpGet] public JResult DownLoadImage(string serverId) { return JResult.Invoke(() => { return WeixinApi.GetImagePath(serverId); }); } }
/// <summary> /// 微信相关的接口实现类 /// </summary> public class WeixinApi { /// <summary> /// 将微信语音保存到本地服务器 /// </summary> /// <param name="serverId">微信录音ID</param> /// <returns></returns> public static string GetVoicePath(string serverId) { var rootPath = Park.Server.ServerManage.Config.ResPath; string voice = ""; //调用downloadmedia方法得到downfile对象 Stream downFile = DownloadFile(serverId); if (downFile != null) { string fileName = Guid.NewGuid().ToString(); string path = "\\voice\\" + DateTime.Now.ToString("yyyyMMdd"); string phyPath = rootPath + path; if (!Directory.Exists(phyPath)) { Directory.CreateDirectory(phyPath); } // 异步处理(解决当文件稍大时页面上ajax请求没有返回的问题) Task task = new Task(() => { //生成amr文件 var armPath = phyPath + "\\" + fileName + ".amr"; using (FileStream fs = new FileStream(armPath, FileMode.Create)) { byte[] datas = new byte[downFile.Length]; downFile.Read(datas, 0, datas.Length); fs.Write(datas, 0, datas.Length); } //转换为mp3文件 string mp3Path = phyPath + "\\" + fileName + ".mp3"; JFile.ConvertToMp3(rootPath, armPath, mp3Path); }); task.Start(); voice = path + "\\" + fileName + ".mp3"; } return voice; } /// <summary> /// 将微信图片保存到本地服务器 /// </summary> /// <param name="serverId">微信图片ID</param> /// <returns></returns> public static string GetImagePath(string serverId) { var rootPath = Park.Server.ServerManage.Config.ResPath; string image = ""; //调用downloadmedia方法得到downfile对象 Stream downFile = DownloadFile(serverId); if (downFile != null) { string fileName = Guid.NewGuid().ToString(); string path = "\\image\\" + DateTime.Now.ToString("yyyyMMdd"); string phyPath = rootPath + path; if (!Directory.Exists(phyPath)) { Directory.CreateDirectory(phyPath); } //生成jpg文件 var jpgPath = phyPath + "\\" + fileName + ".jpg"; using (FileStream fs = new FileStream(jpgPath, FileMode.Create)) { byte[] datas = new byte[downFile.Length]; downFile.Read(datas, 0, datas.Length); fs.Write(datas, 0, datas.Length); } image = path + "\\" + fileName + ".mp3"; } return image; } /// <summary> /// 下载多媒体文件 /// </summary> /// <param name="media_id"></param> /// <returns></returns> public static Stream DownloadFile(string media_id) { var access_token = GetAccessToken(); string url = "http://file.api.weixin.qq.com/cgi-bin/media/get?"; var action = url + $"access_token={access_token}&media_id={media_id}"; HttpWebRequest myRequest = WebRequest.Create(action) as HttpWebRequest; myRequest.Method = "GET"; myRequest.Timeout = 20 * 1000; HttpWebResponse myResponse = myRequest.GetResponse() as HttpWebResponse; var stream = myResponse.GetResponseStream(); var ct = myResponse.ContentType; // 返回错误信息 if (ct.IndexOf("json") >= 0 || ct.IndexOf("text") >= 0) { using (StreamReader sr = new StreamReader(stream)) { // 返回格式 {"errcode":0,"errmsg":"ok"} var json = sr.ReadToEnd().FormJObject(); var errcode = (int)json["errcode"]; // 40001 被其余地方使用 || 42001 过时 if (errcode == 40001 || errcode == 42001) { // 从新获取token var cache = Park.Common.JCache.Instance; cache.Remove("access_token", "weixin"); return DownloadFile(media_id); } else { throw new JException(json.ToString()); } } } // 成功接收到数据 else { Stream MyStream = new MemoryStream(); byte[] buffer = new Byte[4096]; int bytesRead = 0; while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0) MyStream.Write(buffer, 0, bytesRead); MyStream.Position = 0; return MyStream; } } }
/// <summary> /// 文件处理类 /// </summary> public class JFile { /// <summary> /// 音频转换 /// </summary> /// <link>https://www.cnblogs.com/hbh123/p/7368251.html</link> /// <param name="ffmpegPath">ffmpeg文件目录</param> /// <param name="soruceFilename">源文件</param> /// <param name="targetFileName">目标文件</param> /// <returns></returns> public static string ConvertToMp3(string ffmpegPath, string soruceFilename, string targetFileName) { // 需事先将 ffmpeg.exe 放到 ffmpegPath 目录下 string cmd = ffmpegPath + @"\ffmpeg.exe -i " + soruceFilename + " -ar 44100 -ab 128k " + targetFileName; return ConvertWithCmd(cmd); } private static string ConvertWithCmd(string cmd) { try { System.Diagnostics.Process process = new System.Diagnostics.Process(); process.StartInfo.FileName = "cmd.exe"; process.StartInfo.UseShellExecute = false; process.StartInfo.CreateNoWindow = true; process.StartInfo.RedirectStandardInput = true; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; process.Start(); process.StandardInput.WriteLine(cmd); process.StandardInput.AutoFlush = true; Thread.Sleep(1000); process.StandardInput.WriteLine("exit"); process.WaitForExit(); string outStr = process.StandardOutput.ReadToEnd(); process.Close(); return outStr; } catch (Exception ex) { return "error" + ex.Message; } } }
微信jssdk录音功能开发记录
微信语音上传下载
ffmpeg.exe 下载地址
状况1:获取到 access_token 后,去获取 jsapi_ticket 时报错。
access_token 有两种。第一种是全局的和用户无关的access_token, 用 appid 和 appsecret 去获取(/cgi-bin/token)。第二种是和具体用户有关的,用 appid 和 appsecre 和 code 去获取 (/sns/oauth2/access_token)。这里须要的是第一种。
状况2:完成配置,发起录音下载录音到本地时报错。
缘由:用同一个appid 和 appsecret 去获取 token,在不一样的服务器去获取,致使前一个获取的token失效。解决方案:在下载录音到本地时,过滤报错信息,若为40001错误,则从新获取token。
获取签名,并配置到 wx.config 以后,同时不报这两个错误信息。
解决方案:生成的签名有误,注意各个签名参数的值和生成字符串时的顺序。
在微信开发者工具中调试,上传录音的方法中从微信下载录音报错。
解决方案: 在微信web开发者工具中调试会有这个问题,直接在微信调试则无此问题。
录制60秒的语音自动中止后,上传语音到微信服务器报错,待解决。