微信录音功能,加音频格式转换ffmpeg(1)

上个项目有个需求是在浏览器页面使用录音功能,但是这个面临很多兼容性问题,所以后来决定只在微信浏览器中使用,其他页面采用提示。
写这个项目隐约就感觉有很多坑要踩,结果不出所料,期间被轰炸了一个多星期,当然炸弹不止微信的东西,更多是因为改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&timestamp=$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的缓存清掉,把问题转移到时间有效性上。

还有其他坑点代编辑,不过本篇文章已经较长,暂且如此

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值