上个项目有个需求是在浏览器页面使用录音功能,但是这个面临很多兼容性问题,所以后来决定只在微信浏览器中使用,其他页面采用提示。
写这个项目隐约就感觉有很多坑要踩,结果不出所料,期间被轰炸了一个多星期,当然炸弹不止微信的东西,更多是因为改bug,写功能和新需求同时进行等项目管理上的混乱。
先答疑解惑,把一些流程和明显的bug先罗列出来。
基本工作
准备工作 :
1、 前端代码。本来想提供文件,但是不支持上传,稍后会整理出前端代码的文章,网上也有很多。
2、工具:微信开发工具**devtool,**natapp内网穿透工具并配置好账号,两个都很简单;
3、测试号,确保可正常使用;(正式号的安全域名配置每个月只有三次机会,natapp的外网地址每次都不一样,必须只能测试号)
正常情况下,你调用我的文件并且准备好后台代码,提供的票据正确,就会弹出微信的探弹框,类似:config:ok。这一步如果有任何的报错,就是配置方法不正确。
后台微信接口地址
后台代码要用到的微信接口:
access_token:
https请求方式: GET
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
jsapi_ticket:
采用http GET方式请求获得jsapi_ticket(有效期7200秒,开发者必须在自己的服务全局缓存jsapi_ticket):https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=ACCESS_TOKEN&type=jsapi
成功会返回:{“errcode”:0, “errmsg”:”ok”, “ticket”:”xxxx”,”expires_in”:7200}
下载媒体接口
获取临时素材:https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID
文档地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1444738727
或者
后台php代码
/**
* 我用的是yii框架,里面有一些yii框架自带的方法,你们可以换成自己的,类的本质不变
**/
class InterestEnWechat{
public $timestamp;
public $noncestr;
public $appId;
public $appSecret;
public $token;
//微信开发通用基础域名
const WX_BASE_URL = 'https://api.weixin.qq.com';
const WX_ACCESS_TOKEN_EXPIRE = 7000;
//access_token
const WX_ACCESS_TOKEN_URL = 'https://api.weixin.qq.com/cgi-bin/token';
//js票据
const WX_JSAPI_TICKET_URL = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket';
//下载临时素材音频url(视频http)
const WX_DOWN_MEDIA_URL = 'http://api.weixin.qq.com/cgi-bin/media/get';
//reid key
const ACCESS_TOKEN_KEY = 'access_token';
const JS_API_TICKET_KEY = 'jsapi_ticket';
public function __construct(array $config = [])
{
$this->appId = $config['appId'];
$this->appSecret = $config['appSecret'];
parent::__construct($config);
}
//初始化方法,看你的框架
public function init()
{
if ($this->appId === null) {
throw new InvalidConfigException('The appId property must be set.');
} elseif ($this->appSecret === null) {
throw new InvalidConfigException('The appSecret property must be set.');
}
}
//获取acesstoken
public function getAccessToken() {
$access_token = \yii::$app->redis->getInstance()->get(self::ACCESS_TOKEN_KEY);
if(empty($access_token)){
$response = $this->updateAccessToken();
return $response;
}
return $access_token;
}
//更新access_token
public function updateAccessToken()
{
$request_url = self::WX_ACCESS_TOKEN_URL;
$client = new Client();
$request_url .= '?grant_type=client_credential&appid='. $this->appId . '&secret='.$this->appSecret;
$response = $client->request('get', $request_url,[]);
$res = $response->getBody()->getContents();
if (isset(json_decode($res, true)['errcode'])) {
//记录请求错误日志
$this->mongoLog($request_url, $res);
return $res;
}else{
$ret = \GuzzleHttp\json_decode($res, true);
\yii::$app->redis->getInstance()->setex(self::ACCESS_TOKEN_KEY, 7000, $ret['access_token']);
return $ret['access_token'];
}
}
//录音的js_api配置
public function jsAudioApiConfig($url='')
{
$noncestr = '随机字符串';
$timestap = '时间';
$signature = $this->getAudioJsConfigSign($noncestr, $timestap, $url);
if(isset($signature['errcode'])){
return $signature;
}
$config = [
// "debug"=>true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
"appId" => $this->appId,
"timestamp" => $timestap, // 必填,生成签名的时间戳
"nonceStr" => $noncestr, // 必填,生成签名的随机串
"signature" => $signature, // 必填,签名,见附录1
"jsApiList" => ['onMenuShareTimeline','onMenuShareAppMessage','startRecord','stopRecord','onVoiceRecordEnd','playVoice','stopVoice','onVoicePlayEnd','uploadVoice','translateVoice'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2
];
return $config;
}
//生成录音js配置的签名
public function getAudioJsConfigSign($noncestr, $timestap, $url='')
{
$jsapiTicket = $this->getJsApiTicket();
if(isset($jsapiTicket['errcode'])){
return $jsapiTicket;
}
if(empty($url)){
$url = explode('#', Yii::$app->getRequest()->getAbsoluteUrl())[0];
}
$string = "jsapi_ticket=$jsapiTicket&noncestr=$noncestr×tamp=$timestap&url=$url";
$signature = sha1($string);
return $signature;
}
//获取jsapiticket
public function getJsApiTicket()
{
$jsapi_ticket = \yii::$app->redis->getInstance()->get(self::JS_API_TICKET_KEY);
if(empty($jsapi_ticket)) {
$jsapi_ticket = $this->updateJsApiTicket();
}
return $jsapi_ticket;
}
//更新jsapi_ticket
public function updateJsApiTicket()
{
$response = $this->requestJsApiTicket();
return $response;
}
//请求jsapi_ticket主体方法
public function requestJsApiTicket()
{
$access_token = $this->getAccessToken();
if(is_array($access_token)){
return $access_token;
}
$request_url = self::WX_JSAPI_TICKET_URL;
$client = new Client();
$request_url .= '?type=jsapi&access_token=' . $access_token;
$response = $client->request('get', $request_url, []);
if ($response->getStatusCode() != 200) {
//记录请求错误日志
$this->mongoLog($request_url, $response);
}
$ret = \GuzzleHttp\json_decode($response->getBody()->getContents(), true);
if($ret['errcode'] == 0){
\yii::$app->redis->getInstance()->setex(self::JS_API_TICKET_KEY, 7000, $ret['ticket']);
return $ret['ticket'];
}else{
$this->mongoLog($request_url, $ret);
return $ret;
}
}
//下载媒体
public function getMedia($media_id)
{
$response = $this->requestMedia($media_id); //可能返回错误结果的json
if(is_string($response) && !strstr($response, 'AMR')){
$response = json_decode($response, true);
}
if(isset($response['errcode']) && ($response['errcode'] == 40001 || $response['errcode'] == 42001)){
$this->mongoLog('请求媒体文件|获取临时素材'.$media_id, $response);
\yii::$app->redis->getInstance()->del(self::ACCESS_TOKEN_KEY); //删除了access_token的缓存
}
return $response; //正确情况下直接返回字节流
}
//请求媒体下载主体方法
public function requestMedia($media_id, $flag=0)
{
$access_token = $this->getAccessToken();
if(is_array($access_token)){
return $access_token;
}
$request_url = self::WX_DOWN_MEDIA_URL;
$request_url .= '?access_token=' . $access_token . '&media_id='.$media_id;
$client = new Client();
$response = $client->request('get', $request_url, []);
if($response->getStatusCode() != 200){
$this->mongoLog($request_url, $response);
}
return $response->getBody()->getContents();
}
//记录日志(我自己用的yii框架的日志组件)
public function mongoLog($request_url='', $response)
{
if(is_string($response)){
$response = json_decode($response, true);
}
$argv = [];
$argv['url'] = $request_url;
foreach($response as $kk=>$vv){
$argv[$kk] = $vv;
}
\yii::$app->mongoLog->add($argv);
}
}
以上是php代码的实现情况
助你出坑
用手机测录音,用手机测录音, 用手机测录音,因为开发工具无法录音
重要的话说三遍,此处是深坑。我遇到的情况是如此,就是工具不能录音。
网络上很少有文章提及工具不能录音,但是有很多人都遇到了invalid mediaId的情况。在初期我的整个流程都是使用开发工具调试,也正常返回serverId,但接口一直提示invalid mediaId,我在这个地方卡了半天,因为手机不能看接口返回数据,有工具的话我一直拒绝手机测的,后来实在不行了就去和运营的童鞋讨论(机缘得知他们也在做这个),他们(男程序员)轻描淡写地来了句:“对啊,我都直接用自己的服务器(他们没用natapp),用手机测的。”当时当真一脸黑线,一脸蒙逼。
格式转换
由于我们的前端使用mp3格式的播放器来减少兼容性问题,所以拿到的音频要转换成mp3格式,使用ffmpeg转换
下载ffmpeg
centos7用下面的网址一般来讲毫无压力
https://blog.youkuaiyun.com/p_function/article/details/80324406
如报没有yasm,yum install -y tasm
即可
下载媒体接口返回的是字节流
这个不算坑,不过开始做的稍后会容易忽略,我的思路是写入文件,变成mp3,然后传到阿里云上。
/**
* 把amr转化成mp3格式
*/
public function amrTransfertoMp3($filename, $data)
{
$dir = '/tmp/beikao/record/';
if(!is_dir($dir)){
mkdir($dir, 0777, true);
}
$mp3_file_name = $dir . $filename . '.mp3';
$amr_file_name = $dir . $filename .'.amr';
file_put_contents($amr_file_name, $data);
$command = "ffmpeg -i $amr_file_name $mp3_file_name 2>&1";
shell_exec($command);
return $mp3_file_name;
}
public function savetoAliyun($filename, $data='')
{
$upload_name = $this->amrTransfertoMp3($filename, $data);
$destFileDir = \Yii::$app->params['upload_aliyun_audio_dir'];
$ret = File::upload($upload_name, $destFileDir, $filename.'.mp3');
$fileUploadUrl = $ret->data['url']; //文件上传后的远程地址
if ($ret->code == 0 && file_exists($upload_name)) {
unlink($upload_name); //删除本地文件
}else{
return false;
}
return $fileUploadUrl;
}
何时更新access_token
微信告诉我们失效后就更新access_token,所以为保证项目不报错,我在检测出40001或者42001的时候就尝试更新token并再次做当前目标请求,顺着思路是这个想法,但其实这样有问题。
如果你只需要的接口少或者用户人数少还好,如果很多接口都会调用access_token,或者用户基数大,同一时间会有n个正在进行的接口都在获取access_token,他们会同时更新access_token,或者由于网速原因,其中有一些部分已经做更新,另一部分也在进行中(请求已经发出),就会有很多请求在一个非常短的时间不停更新access_token,微信允许过期的access_token有5分钟的有效期,测试的时候并没发现(可能测试号是如此),微信测试网址请求access_token后,测试的网站里存的就过期了。
所以建议一次过期可以不予处理,因为很快就会重新生成,只需要一个请求,这个主要针对时间失效的token.
还有是expire没过期但是值失效的情况,使用没有过期频率的接口做每次的检查,如果后台有这样权限的接口,或者检查失效后,就把access_token的缓存清掉,把问题转移到时间有效性上。
还有其他坑点代编辑,不过本篇文章已经较长,暂且如此