APICloud AVM框架 开发视频会议APP

简介: 1.创建会议,确认会议时间、参会人员、会议主题、确定会议主持人(默认为发起人)可开启会议;同时会通过应用消息和短信通知参会人员。2.加入会议,可通过会议大厅找的会议列表直接加入,也可通过输入会议编号加入会议;加入会议的前提是会议已在进行中。3.快速会议,可直接确认会议人员然后发起实时视频会议,参会人员实时接收应用消息或短信,快速进入会议。3.历史会议,分为我主持的会议、我参与的会议。4.会议大厅,列表显示今天需要参加的会议。

 

APP开发采用的APICloud平台的AVM 多端应用开发框架

    1. 使用 avm.js 一个技术栈可同时开发 Android & iOS 原生 App、小程序和 iOS 轻 App,且多端渲染效果统一;
    2. 全新的 App 引擎 3.0 不依赖 webView,提供百分百的原生渲染,保障 App 性能和体验与原生 App 一致;
    3. 现有 api 直接映射兼容小程序接口,延续已有开发习惯;

    后台使用的PHP的thinkphp框架,通过composer集成各类插件。

    思维导图

    image.gif编辑

    功能介绍

    1.创建会议,确认会议时间、参会人员、会议主题、确定会议主持人(默认为发起人)可开启会议;同时会通过应用消息和短信通知参会人员。

    2.加入会议,可通过会议大厅找的会议列表直接加入,也可通过输入会议编号加入会议;加入会议的前提是会议已在进行中。

    3.快速会议,可直接确认会议人员然后发起实时视频会议,参会人员实时接收应用消息或短信,快速进入会议。

    3.历史会议,分为我主持的会议、我参与的会议。

    4.会议大厅,列表显示今天需要参加的会议。

    5.会议纪要,会议结束后,会议主持人可通过APP或后台系统,把会议纪要整理发布到相关会议中,参会人员可在会议详情中查看会议纪要。

    6.会议附件,主持人员可在会议详情中,把会议相关的附件上传至相关会议中,参与人员可在会议详情中下载附件。

    7.通讯录,展示系统内的联系人,在创建会议时,会议中邀请人的时候会用到。

    应用模块

    image.gif编辑

    项目目录

    image.gif编辑

    应用展示

    image.gif编辑

    开发介绍

    应用导航

    使用的是tabLayout布局作为应用的导航。

    image.gif编辑

    系统首页使用tabLayout,可以将相关参数配置在JSON文件中,再在config.xml中将content的 值设置成该JSON文件的路径。如果底部导航没有特殊需求这里强烈建议大家使用tabLayout为APP进行布局,官方已经将各类手机屏幕及不同的分辨率进行了适配,免去了很多关于适配方面的问题。

    {
        "name": "root",
        "hideNavigationBar": true,
        "navigationBar": {
          "background": "#ffffff",
          "color": "#333333",
          "shadow": "#ffffff",
          "hideBackButton": true
        },
        "tabBar": {
          "scrollEnabled": false,
          "background": "#fff",
          "shadow": "#dddddd",
          "color": "#aaaaaa",
          "selectedColor": "#333333",
          "index":0,
          "preload": 0,
          "frames": [{
            "name": "home",
            "url": "pages/main/home.stml",
            "title": "会议"
          }, {
            "name": "classify-index",
            "url": "pages/classify/classify-index.stml",
            "title": "消息"
          }, {
            "name": "shopping-index",
            "url": "pages/shopping/shopping-index.stml",
            "title": "文档"
          }, {
            "name": "my-index",
            "url": "pages/my/my-index.stml",
            "title": "我的"
          }],
          "list": [{
            "text": "会议",
            "iconPath": "image/tabbar/meeting.png",
            "selectedIconPath": "image/tabbar/meeting-o.png",
            "scale":3
          }, {
            "text": "消息",
            "iconPath": "image/tabbar/message.png",
            "selectedIconPath": "image/tabbar/message-o.png",
            "scale":3
          }, {
            "text": "文档",
            "iconPath": "image/tabbar/doc.png",
            "selectedIconPath": "image/tabbar/doc-o.png",
            "scale":3
          }, {
            "text": "我的",
            "iconPath": "image/tabbar/user.png",
            "selectedIconPath": "image/tabbar/user-o.png",
            "scale":3
          }]
        }
      }

    image.gif

    动态权限  

    安卓10之后,对应用的权限要求提高,不在像老版本一样配置上就会自动获取,必须进行提示。

    依据官方给出的教程进行了动态权限的设置。

    1.添加 mianfest.xml文件

    image.gif编辑

    <?xml version="1.0" encoding="UTF-8"?>
    <manifest>
        <application name="targetSdkVersion" value="30"/>
    </manifest>

    image.gif

    具体的使用说明,在官方论坛中有专门的帖子,APP动态权限及Android平台targetSdkVersion设置

    在系统主页进行动态权限获取,也可在特殊页面的中获取本页面所需的权限,这个可根据具体的业务需求进行处理。本系统涉及到了文件存储、摄像头、麦克风的获取,具体的获取方式见如下代码,因为本系统的初始化页面时home.stml,所以在本页面的apiready()中进行权限验证。

    apiready(){
                    let limits=[];
            //获取权限
            var resultList = api.hasPermission({
              list: ['storage', 'camera', 'microphone']
            });
            if (resultList[0].granted) {
              // 已授权,可以继续下一步操作
            } else {
              limits.push(resultList[0].name);
            }
            if (resultList[1].granted) {
              // 已授权,可以继续下一步操作
            } else {
              limits.push(resultList[1].name);
            }
            if (resultList[2].granted) {
              // 已授权,可以继续下一步操作
            } else {
              limits.push(resultList[2].name);
            } 
            if(limits.length>0){
              api.requestPermission({
                list: limits,
              }, (res) => {
              });
            }
                }

    image.gif

       

    WebSocket

    用于即时通话的时候,监听用户在线状态,可通知用户加入会议。

    具体的通讯原理步骤是:

    会议发起人发起会议-》通过websocket给参会人员发送消息指令-》参会人员接收发送的websocket消息,通过监听触发进入会议房间,同时给会议发起人发送进入会议房间的消息-》会议发起人收到有人进入了会议房间消息后,通过监听触发进入会议房间的操作。 这种流程是会议发起人不必先进入回房间进行等待,不用启用RTC模块,只有当有其他人员收到提醒进入会议房间后才会启用RTC模块进入房间。可以有效的避免资源浪费。

    还有一中简易模式,会议发起人发起会议,并启用RTC模块,进入会议房间进行等待(判断等待时间,比如超过3分钟没有其他人员加入房间,自动退出会议房间结束会议)-》通过websocket给参会人员发送消息指令-》参会人员接收发送的websocket消息,通过监听触发进入会议房间。这种模式如果其他参会人员不及时参加会议的时候会造成部分资源的浪费。

    进入会议后其他后续的操作,就可以通过tencnetTRTC模块中的方法进行处理。

    websocket的目的就是即时的通知参会人员有会议要参加,因为RTC模块本身没有集成这个功能。这部分操作是在进入会议房间之前的操作。

    本APP用的是websocket模块,本模块可配置全局变量,方便实用。当然也可以尝试其他的websocket模块。

    image.gif编辑image.gif编辑

    AVM框架里官方就集成了websocket。使用说明文档

    image.gif编辑

    apiready(){
        //链接websocket
        var webSocket = api.require('webSocket');
        //消息监听,可以监听连接,断开,接收消息等事件
        webSocket.addEventListener((ret, err) => {
          console.log(JSON.stringify(ret) + "  " + JSON.stringify(err));
          //断开重连
          if(ret.evenType=='Closed'){
            webSocket.open({
              url : 'ws://192.168.1.5:8888/socket'
            }, (ret, err) => {
              console.log(JSON.stringify(ret) + "  " + JSON.stringify(err));
            });
          }
          //收到消息
          if(ret.evenType=='ReturnData'){
            //解析data中的内容,获取会议房间ID进入会议
          }
        });
        //获取当前的websocket链接状态
        var webSocketStatus = webSocket.getConnectState();
        //未链接则进行链接,如果已链接则无效操作
        if(webSocketStatus.State =='CLOSED'){
          webSocket.open({
            url : 'ws://192.168.1.5:8888/socket'
          }, (ret, err) => {
            console.log(JSON.stringify(ret) + "  " + JSON.stringify(err));
          });
        }
      },

    image.gif

    视频通话 RTC

    使用的是tencnetTRTC模块,查看模块文档

    image.gif编辑image.gif编辑

    首先需要去申请腾讯云 SDKAppId,进入腾讯云实时音视频控制台 创建应用,即可看到 SDKAppId。

    为什要用tencnetTRTC呢,因为tencnetTRTC模块不会把SDKAppId与应用进行绑定,这样就可以使用一个SDKAppId来实现两个不同的APP之间的视频通话了,共用腾讯云的通话时长。

    而且tencnetTRTC的接口相比较其他RTC模块更丰富,可以更好的满足一些个性化的需求。

    消息事件

    通过sendEvent把事件广播出去,然后在其他页面通过addEventListener监听事件,通过事件名和附带的参数进行其他操作。API对象说明文档

    image.gif编辑image.gif编辑

    举例说明

    1.当创建会议成功之后,需要发送一个会议创建成功的事件;在会议列表或者其他展示会议的页面,需要监听此事件,然后在监听成功的回调中做刷新的操作。

    2.当会议开始或者结束之后,需要发送相应的事件,在会议列表或者其他展示会议的页面,需要监听此类事件,在监听成功的回调中做刷新列表或者更改会议状态的操作。

    消息推送

    ajpush模块封装了极光推送平台的SDK,使用此模块可实现接收推送通知和透传消息功能。

    image.gif编辑

    关于模块使用及注意事项,请仔细阅读模块说明文档

    image.gif编辑

    //初始化JpushSDK
        initJpush(){
          var jpush = api.require('ajpush');
          jpush.init((ret, err)=>{
            if(ret && ret.status){
              //绑定别名
              if(api.getPrefs({sync: true,key: 'userid'})){
                jpush.bindAliasAndTags({
                  alias:api.getPrefs({sync: true,key: 'userid'}),
                  tags:['APPUSER']
                }, (ret, err)=>{
                  if(ret.statusCode==0){
                    api.toast({ msg: '推送服务初始化成功'});
                  }
                  else{
                    api.toast({ msg: '绑定别名失败'});
                  }
                });
              }
              //监听消息
              jpush.setListener((ret) => {
                // var content = ret.content;
                api.toast({ msg: ret.content});
              });
            }
          else{
              api.toast({ msg: '推送服务初始化失败'});
            }
          });
          api.addEventListener({name:'pause'}, function(ret,err) {
            jpush.onResume();//监听应用进入后台,通知jpush暂停事件
          })
          api.addEventListener({name:'resume'}, function(ret,err) {
            jpush.onResume();//监听应用恢复到前台,通知jpush恢复事件
          })      
        },

    image.gif

    短信验证码

    用户注册的时候需要通过手机短信验证码进行校验,以保证手机号真实有效,能够正常接收应用推送的各类短信通知提醒。

    本应用中使用的是AVM模块库中的verification-code-input组件,可自定义验证码长度和再次获取时间间隔,自动校验验证码有效性。

    image.gif编辑

    示例代码

    <template>
      <view class="page">
        <safe-area></safe-area>
        <verification-code-input :limitSecond={seconds} :limitCode={codeLen} onsetCode="getCode"></verification-code-input>
      </view>
    </template>
    <script>
      import '../../components/verification-code-input.stml'
      export default {
        name: 'demo-verification-code-input',
        apiready(){
        },
        data() {
          return{
            code:'',
            seconds:60,
            codeLen:4
          }
        },
        methods: {    
          getCode(e){
            // console.log(JSON.stringify(e.detail));
            this.data.code = e.detail;
          }
        }
      }
    </script>

    image.gif

    关于验证码的有效时间,是通过后台进行设定的,通过session缓存每个手机号的验证码,并设置缓存有效时间,表单提交的时候通过session去获取验证码,如果session失效,则无法获取验证码,接口可直接返回验证码失效提示。

    清空缓存

    首先通过getCacheSize获取应用的缓存数量,并在标签中显示,然后给标签添加点击事件,在事件中通过clearCache清除应用缓存。

    image.gif编辑image.gif编辑

    计算当前应用的缓存大小,保留以为小数。

    apiready(){
      //获取APP缓存 异步返回结果:
      api.getCacheSize((ret) => {
        this.data.cache = parseInt(ret.size/1024/1024).toFixed(1);
      });
    },

    image.gif

    执行清除缓存,并提示信息。

    clearCache(){
      api.clearCache(() => {
        this.data.cache=0.0;
        api.toast({
          msg:'清除完成'
        })
      });   
    }

    image.gif

    AVM组件使用

    项目中使用了很多的AVM组件,其中包括视频通话组件、通讯录组件、滑动单元格组件、日期时间Picker组件、数字键盘组件等等。

    image.gif编辑image.gif编辑image.gif编辑image.gif编辑

    image.gif编辑image.gif编辑image.gif编辑

    其中视频通话组件(easy-video-call、easy-voice-communication、multi-person-video-call)用的是声网的SDK,这里借用了样式,把模块换成了TencentRTC。

    消息列表列表中使用了easy-swiper-cell滑动单元格组件,来实现滑动操作已读。

    时期和时间选择用到了time-picker、date-picker组件。

    通讯录使用的是address-book组件。

    在通过会议编号进入会议时,由于会议编号全是数字,这里使用了number-keyboard数组键盘组件。

    文档下载、图片浏览

    会议结束后会上传会议纪要,会议相关文件等各类文档,主要包括doc、excel、pdf和图片。

    对于doc、excel、pdf这类文件使用的是docReader模块。方式是先通过api.download方法下载文,然后在回调中通过docReader模块唤醒三方工具进行文件浏览。

    image.gif编辑

    image.gif编辑

    //下载、浏览附件
        loadfile(url){
          api.download({
              url: url,
              // savePath: 'fs://appDownload/',//不选自动创建路径
              report: true,
              cache: true,
              allowResume: true
          }, (ret, err)=> {
              if (ret.state == 1) {
                  //下载成功
                  api.hideProgress();
                  var path=ret.savePath;
                  // alert('下载成功,文件路径:'+ret.savePath);
                  var docReader = api.require('docReader');
                  docReader.open({
                      path: path,
                      autorotation: false
                  }, (ret, err) => {
                      if (!ret.status) {
                          if(err.code=='1'){
                            alert('打开文件错误,请自行查找文件打开,路径:'+path);
                          }
                          else if(err.code=='2'){
                            alert('文件格式错误,请自行查找文件打开,路径:'+path);
                          }
                      }
                  });
              }
              else if(ret.state == 0){
                api.showProgress({
                  title: '努力下载中...',
                  text: ret.percent+'%',
                  modal: false
                });
              }
              else if(ret.state == 2) {
                  api.hideProgress();
                  alert('下载失败,请重试。');
              }
          });
        }

    image.gif

    图片使用的是photoBrowser模块进行浏览

    image.gif编辑

    picturePreview(e){
      let images = e.currentTarget.dataset.list;
      //预览图片
      var photoBrowser = api.require('photoBrowser');
      photoBrowser.open({
        images: images,
        bgColor: '#000'
      }, function(ret, err) {
        if(ret.eventType=='click'){
          photoBrowser.close();
        }
      });
    }

    image.gif

    单设备登陆

    本APP做了单一设备登陆的限制,具体实现方式是,通过api.deviceId可以获取到收的设备ID,用户登陆成功之后进行设备绑定;APP初始化的时候进行设备验证,先通过接口获取数据库中记录的用户上次登录的设备ID,然后与本机设备ID进行比对,如果设备ID不一致则跳转登陆页面。

    //登记设备
              setDeviceID(){
            var data={
              secret:'',
              userid:api.getPrefs({sync: true,key: 'userid'}),
              deviceid:api.deviceId
            };
            api.showProgress();
            POST('updatedeviceid',data,{}).then(ret =>{
              // console.log(JSON.stringify(ret));
              if(ret.flag=='Success'){
                api.toast({
                  msg:'设备登记成功'
                })
              }       
              api.hideProgress();
            }).catch(err =>{
              api.toast({
                msg:JSON.stringify(err)
              })
            })
          }

    image.gif

    //验证设备
        checkDeviceID(){
          var data={
            secret:'',
            userid:api.getPrefs({sync: true,key: 'userid'})
          };
          api.showProgress();
          POST('querydeviceidbynew',data,{}).then(ret =>{
            // console.log(JSON.stringify(api.deviceId));
            if(ret.flag=='Success'){
              if(ret.data.deviceid != api.deviceId){
                api.toast({
                  msg:'您的设备已在其他设备上登录,请重新登录。'
                })
                $util.openWin({
                  name: 'login',
                  url: 'widget://pages/seeting/login.stml',
                  title: '',
                  hideNavigationBar:true
                });
              }
            }       
            api.hideProgress();
          }).catch(err =>{
            api.toast({
              msg:'设备登陆异常,请重新登陆。'
            })
            $util.openWin({
              name: 'login',
              url: 'widget://pages/seeting/login.stml',
              title: '',
              hideNavigationBar:true
            });
          })
        }

    image.gif

    接口调用

    封装了 req.js进行接口调用,采用了ES6语法中的Promise是异步编程的一种解决方案(比传统的回调函数更加合理、强大),用同步操作将异步流程表达出来。避免层层嵌套回调。promise 对象提供统一接口,使得控制异步操作更加容易。有兴趣的同学可以多研究一下Promise。

    const config = {
        schema: 'http',
        host: '192.168.1.5',
        path: 'index.php/Home/api/',
        secret:'1f3ef6ac********6deecd990f'
    }
    function req(options) {
        const baseUrl = `${config.schema}://${config.host}/${config.path}/`;
        options.url = baseUrl + options.url;
        return new Promise((resolve, reject) => {
            api.ajax(options,  (ret, err) => {
                console.log('[' + options.method + '] ' + options.url + ' [' + api.winName + '/' + api.frameName + ']\n' + JSON.stringify({
                    ...options, ret, err
                }))
                if (ret) {
                    resolve(ret);
                    api.hideProgress();
                } else {
                    reject(err); 
                    api.hideProgress();
                }
            });
        })
    }
    /**
     * GET请求快捷方法
     * @constructor
     * @param url {string} 地址
     * @param options {Object} 附加参数
     */
    function GET(url, options = {}) {
        return req({
            ...options, url, method: 'GET'
        });
    }
    /**
     * POST 请求快捷方法
     * @param url
     * @param data
     * @param options {Object} 附加参数
     * @returns {Promise<Object>}
     * @constructor
     */
    function POST(url, data, options = {}) {
        data.secret = config.secret;
        return req({
            ...options, url, method: 'POST', data: {
                values: data
            }
        });
    }
    export {
        req, GET, POST, config
    }

    image.gif

    在stml页面中,首先要引用封装好的req.js,目前只封装了POST、GET两种方式,如果接口中有其他的方式,可以在此基础上进行封装。

    下面以登录页为例,展示具体的使用。

    <template>
        <scroll-view class="page">
        <safe-area></safe-area>
        <view class="top">
          <text class="top-title">登录</text>
          <text class="top-sub-title">欢迎使用逍遥自在云视频会议,让您从此无忧工作!</text>
        </view>
        <view class="input-box">
          <image class="item-ico" src='../../image/user.png' mode="widthFix"></image>
          <input class="item-input" placeholder="请输入账号" v-model="username"/>
        </view>
        <view class="input-box">
          <image class="item-ico" src='../../image/psw.png' mode="widthFix"></image>
          <input class="item-input" type="password" placeholder="请输入密码" v-model="password"/>
        </view>
        <view class="btn-box">
          <button class="btn" onclick={this.login}>确定</button>
        </view>
        </scroll-view>
    </template>
    <script>
      import {POST} from '../../script/req.js'
      export default {
        name: 'login',
        apiready(){
          //监听返回  双击退出程序
          api.setPrefs({
            key: 'time_last',
            value: '0'
          });
          api.addEventListener({
            name : 'keyback'
            }, function(ret, err) {
            var time_last = api.getPrefs({sync: true,key: 'time_last'});
            var time_now = Date.parse(new Date());
            if (time_now - time_last > 2000) {
              api.setPrefs({key:'time_last',value:time_now});
              api.toast({
                msg : '再按一次退出APP',
                duration : 2000,
                location : 'bottom'
              });
            } else {
              api.closeWidget({
                silent : true
              });
            }
          });
        },
        data() {
          return{
            username:'',
            password:''
          }
        },
        methods: {
          login(){
            if (!this.data.username) {
              this.showToast("姓名不能为空");
              return;
            }
            if (!this.data.password) {
              this.showToast("密码不能为空");
              return;
            } 
            var data={
              secret:'',
              user:this.data.username,
              psw:this.data.password
            };
            api.showProgress();
            POST('loginuser',data,{}).then(ret =>{
              // console.log(JSON.stringify(ret));
              if(ret.flag=='Success'){
                api.setPrefs({key:'username',value:ret.data.username});
                api.setPrefs({key:'userid',value:ret.data.id});
                api.setPrefs({key:'deviceid',value:ret.data.deviceid});
                api.setPrefs({key:'phone',value:ret.data.phone});
                //登记设备
                this.setDeviceID();
                api.sendEvent({
                  name: 'loginsuccess',
                });
                api.closeWin();
              }
              else{
                api.toast({
                  msg:'登录失败!请稍后再试。'
                })
              }
              api.hideProgress();
            }).catch(err =>{
              api.toast({
                msg:JSON.stringify(err)
              })
            })
          },
          //登记设备
          setDeviceID(){
            var data={
              secret:'',
              userid:api.getPrefs({sync: true,key: 'userid'}),
              deviceid:api.deviceId
            };
            api.showProgress();
            POST('updatedeviceid',data,{}).then(ret =>{
              // console.log(JSON.stringify(ret));
              if(ret.flag=='Success'){
                api.setPrefs({key:'deviceid',value:api.deviceid});
                api.toast({
                  msg:'设备登记成功'
                })
              }       
              api.hideProgress();
            }).catch(err =>{
              api.toast({
                msg:JSON.stringify(err)
              })
            })
          }
        }
      }
    </script>
    <style>
        .page {
            height: 100%;
        background-color:#ffffff;
        }
      .top{
        margin-top: 50px;
        margin-left: 20px;
        margin-bottom: 100px;
      }
      .top-title{
        font-size: 25px;
        font-weight: bold;
      }
      .top-sub-title{
        font-size: 13px;
        font-weight: bold;
      }
      .input-box{
        margin: 20px;
        border-bottom: 1px solid #ccc;
        padding-bottom: 5px;
        flex-flow: row nowrap;
        align-items: center;
      }
      .item-input{
        width: auto;
        border: 0;
        font-size: 18px;
        margin-left: 10px;
      }
      .item-ico{
        width: 35px;
      }
      .btn-box{
        margin-top: 50px;
        margin-left: 10px;
        margin-right: 10px;
      }
      .btn{
        background-color: #256fff;
        color: #ffffff;
        font-size: 20px;
        border-radius: 20px;
        padding: 10px 0;
        font-weight: bold;
      }
    </style>

    image.gif

    后台代码

    代码示例

    <?php
    namespace Home\Controller;
    require 'vendor/autoload.php';    // 注意位置一定要在 引入ThinkPHP入口文件 之前
    use Think\Controller;
    use JPush\Client as JPushClient;
    use AlibabaCloud\Client\AlibabaCloud;
    use AlibabaCloud\Client\Exception\ClientException;
    use AlibabaCloud\Client\Exception\ServerException;
    class ApiController extends Controller {
        public function index(){
            $this->show('');
        }
        //用户登录
        public function loginuser(){
            checkscret('secret');//验证授权码
            checkdataPost('user');//账号
            checkdataPost('psw');//密码
            $map['username']=$_POST['user'];
            $map['password']=$_POST['psw'];
            $map['zt']='T';
            $releaseInfo=M()->table('user')->field('id,username,phone,deviceid,role')->where($map)->find();
            if($releaseInfo){
                returnApiSuccess('登录成功',$releaseInfo);
              }
              else{
                returnApiError( '登录失败,请稍后再试');
                exit();
              }
          }
           //记录登录设备ID
          public function updatedeviceid(){
            checkscret('secret');//验证授权码
            checkdataPost('userid');//用户ID
            checkdataPost('deviceid');//设备ID
            $userid=$_POST['userid'];
            $deviceid=$_POST['deviceid'];
            $map['id']=$userid;
            $data['deviceid']=$deviceid;
            $releaseInfo=M()->table('user')->where($map)->save($data);
            if($releaseInfo){
              returnApiSuccess('登记成功',$releaseInfo);
            }
            else{
              returnApiError( '登记失败,请稍后再试');
              exit();
            }
        }
        //获取最新的登录用户设备ID
        public function querydeviceidbynew(){
            checkscret('secret');//验证授权码
            checkdataPost('userid');//用户ID
            $userid=$_POST['userid'];
            $map['id']=$userid;
            $releaseInfo=M()->table('user')->field('deviceid')->where($map)->find();
            if($releaseInfo){
              returnApiSuccess('查询成功',$releaseInfo);
            }
            else{
              returnApiError( '查询失败,请稍后再试');
              exit();
            }
        }
        //APP修改密码
        public function updatepassword(){
            checkscret('secret');//验证授权码
            checkdataPost('userid');//用户ID 
            checkdataPost('password');//密码
            $userid=$_POST['userid'];
            $password=$_POST['password'];
            $map['id']=$userid;
            $data['password']=$password;
            $releaseInfo=M()->table('user')->where($map)->save($data);
            if($releaseInfo){
                returnApiSuccess('修改成功',$releaseInfo);
            }
            else{
                returnApiError( '修改失败,请稍后再试');
                exit();
            }
          }
        //新增会议
        public function addhuiyi(){
            checkscret('secret');//验证授权码
            checkdataPost('userid');//ID
            $userid=$_POST['userid'];
            $title=$_POST['title'];
            $content=$_POST['content'];
            $users=$_POST['users'];
            $hysj=$_POST['hysj'];
            $hylx=$_POST['hylx'];
            $data['title']=$title;
            $data['content']=$content;
            $data['fqr']=$userid;
            $data['cyr']=$users;
            $data['hysj']=$hysj;
            $data['flag']='01';//未开始
            $data['cjsj']=time();
            $data['type']=$hylx;
            $data['txsj']=date('Y-m-d H:i:s',strtotime("$hysj-10 minute"));
            $data['istip']='01';
            $arruser=explode(',',$users);
            $releaseInfo=M()->table('meeting')->data($data)->add();
            if($releaseInfo){     
                //发送消息
                $this->setmessage($users,'您有一个视频会议需要参加,时间:'.$hysj);
                //发送短信通知
                //$this->pushmsgbyusers($users,$hysj);
                //极光推送
                try{
                  $jpush = new JPushClient(C('JPUSH_APP_KEY'), C('JPUSH_MASTER_SECRET'));
                  $response = $jpush->push()
                      ->setPlatform('all')  //机型 IOS ANDROID
                      ->addAlias($arruser)
                      ->androidNotification($content)
                      ->iosNotification($content,'',0,true)
                      ->options(array(
                          'apns_production' => true,
                      ))
                      ->send();
                      returnApiSuccess('添加成功');
                  }
                  catch(\Exception $e){
                    returnApiSuccess('添加成功');
                    exit();
                  }         
            }
            else{
              returnApiError('添加失败,请稍后再试!');
              exit();
            }
        }
        //查询会议大厅
        public function querymeeting(){
          checkscret('secret');//验证授权码
          checkdataPost('userid');//用户ID
          checkdataPost('limit');//下一次加载多少条
          $userid=$_POST['userid'];
          $where['fqr']=$userid;
          $where['_string']='find_in_set('.$userid.',cyr)';
          $where['_logic']='or';
          $map['_complex']=$where;
          $map['flag']=array('neq','03');   
          $limit=$_POST['limit'];
          $skip=$_POST['skip'];
          if(empty($skip)){
            $skip=0;
          }
          $releaseInfo=M()->table('meeting')->field('id,title,flag,hysj,sjzd(type,\'会议类型\') hylx,cyr,fqr,type')->where($map)->limit($skip,$limit)->order('hysj desc')->select();   
          if($releaseInfo){
            returnApiSuccess('查询成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //设置会议状态
        public function setmeeting(){
          checkscret('secret');//验证授权码
          checkdataPost('id');//会议ID
          checkdataPost('flag');//会议状态
          $id=$_POST['id'];
          $flag=$_POST['flag'];
          $map['id']=$id;
          $data['flag']=$flag;
          if($flag=='02'){
            $data['start']=time();
          }
          else if($flag=='03'){
            $data['end']=time();
          }
          $releaseInfo=M()->table('meeting')->where($map)->save($data);
          if($releaseInfo){
            returnApiSuccess('更新成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //上传会议纪要
        public function addhyjy(){
          checkscret('secret');//验证授权码
          checkdataPost('id');//会议ID
          checkdataPost('hyjy');//会议纪要
          $id=$_POST['id'];
          $hyjy=$_POST['hyjy'];
          $map['id']=$id;
          $data['jiyao']=$hyjy;
          $releaseInfo=M()->table('meeting')->where($map)->save($data);
          if($releaseInfo){
            returnApiSuccess('上传成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //查询历史会议
        public function queryhistory(){
          checkscret('secret');//验证授权码
          checkdataPost('userid');//用户ID
          checkdataPost('limit');//下一次加载多少条
          $userid=$_POST['userid'];
          $where['fqr']=$userid;
          $where['_string']='find_in_set('.$userid.',cyr)';
          $where['_logic']='or';
          $map['_complex']=$where;
          $map['flag']=array('eq','03');   
          $limit=$_POST['limit'];
          $skip=$_POST['skip'];
          if(empty($skip)){
            $skip=0;
          }
          $releaseInfo=M()->table('meeting')->field('id,title,hysj')->where($map)->limit($skip,$limit)->order('hysj desc')->select();   
          if($releaseInfo){
            returnApiSuccess('查询成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //查询会议详情
        public function queryhistoryinfo(){
          checkscret('secret');//验证授权码
          checkdataPost('id');//会议ID
          $id=$_POST['id'];
          $map['id']=$id;
          $releaseInfo=M()->table('meeting')->field('id,title,hysj,content,getusers(cyr) users,sjzd(type,\'会议类型\') type,jiyao,getmeetinglong(id) sc')->where($map)->find();   
          if($releaseInfo){
            returnApiSuccess('查询成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //发送消息通知
        function setmessage($users,$content){
          $arruser=explode(',',$users);
          foreach ($arruser as $item) {
            $data['user']=$item;
            $data['content']=$content;
            $data['shijian']=time();
            $data['sfyd']='01';
            $info=M()->table('sp_message')->data($data)->add();
          }
        }
        //查询消息
        public function querymessage(){
          checkscret('secret');//验证授权码
          checkdataPost('userid');//用户ID
          checkdataPost('limit');//下一次加载多少条
          $userid=$_POST['userid'];
          $map['user']=$userid;
          $limit=$_POST['limit'];
          $skip=$_POST['skip'];
          if(empty($skip)){
            $skip=0;
          }
          $releaseInfo=M()->table('message')->field('id,content,sfyd,from_unixtime(shijian,\'%Y-%m-%d %H:%i:%s\') sj')->where($map)->limit($skip,$limit)->order('sj desc')->select();   
          if($releaseInfo){
            returnApiSuccess('查询成功',$releaseInfo);
          }
          else{
            returnApiError( '没有查询到任何数据');
            exit();
          }
        }
        //设置消息已读
        public function setxxyd(){
          checkscret('secret');//验证授权码
          checkdataPost('id');//ID
          $id=$_POST['id'];
          $map['id']=$id;
          $data['sfyd']='02';
          $releaseInfo=M()->table('message')->where($map)->save($data);
          if($releaseInfo){
            returnApiSuccess('设置成功',$data);
          }
          else{
            returnApiError( '设置失败,请稍后再试');
            exit();
          }      
        }
      //推送用户短信提醒
      function pushmsgbyusers($users,$shijian){
        $map['_string']='find_in_set(id,\''.$users.'\')';
        $data=M()->table('user')->field('group_concat(trim(phone)) phones')->where($map)->find();
        if($data){
            $phones=$data['phones'];
            //发送验证码       
            AlibabaCloud::accessKeyClient(C('accessKeyId'), C('accessSecret'))
                              ->regionId('cn-beijing')
                              ->asDefaultClient();
            try {
                $param = array("datetime"=>$shijian);
                $result = AlibabaCloud::rpc()
                          ->product('Dysmsapi')
                          // ->scheme('https') // https | http
                          ->version('2017-05-25')
                          ->action('SendSms')
                          ->method('POST')
                          ->host('dysmsapi.aliyuncs.com')
                          ->options([
                                'query' => [
                                'RegionId' => "cn-beijing",
                                'PhoneNumbers' =>$phones,
                                'SignName' => "****有限公司",
                                'TemplateCode' => "SMS_****",
                                'TemplateParam' => json_encode($param),
                              ],
                          ])
                          ->request();
            }catch (ClientException $e) {
            }
            return $result;
        }
      }
      //获取腾讯视频RTC usersig
      public function getQQrtcusersig(){
        checkscret('secret');//验证授权码
        checkdataPost('userid');//用户ID
        $sdkappid=C('sdkappid');
        $key=C('usersig_key');
        $userid=$_POST['userid'];
        require 'vendor/autoload.php';
        $api = new \Tencent\TLSSigAPIv2($sdkappid, $key);
        $sig = $api->genSig($userid);
        if($sig){
          returnApiSuccess('查询成功',$sig);
        }
        else{
          returnApiError( '查询失败,请稍后再试');
          exit();
        }
      }
    }

    image.gif

    插件引用

    用到了阿里短信插件、极光推送插件、腾讯RTC签名插件;通过composer安装。

    composer.json文件

    {
      "config": {  
            "secure-http": false  
        },
      "require": {
        "jpush/jpush": "^3.6",
        "tencent/tls-sig-api-v2": "1.0",
        "alibabacloud/client": "^1.5"
      }
    }

    image.gif

    相关文章
    |
    20天前
    |
    Web App开发 Java 视频直播
    FFmpeg开发笔记(四十九)助您在毕业设计中脱颖而出的几个流行APP
    对于软件、计算机等专业的毕业生,毕业设计需实现实用软件或APP。新颖的设计应结合最新技术,如5G时代的音视频技术。示例包括: 1. **短视频分享APP**: 集成FFmpeg实现视频剪辑功能,如添加字幕、转场特效等。 2. **电商购物APP**: 具备直播带货功能,使用RTMP/SRT协议支持流畅直播体验。 3. **同城生活APP**: 引入WebRTC技术实现可信的视频通话功能。这些应用不仅实用,还能展示开发者紧跟技术潮流的能力。
    51 4
    FFmpeg开发笔记(四十九)助您在毕业设计中脱颖而出的几个流行APP
    |
    14天前
    |
    移动开发 小程序 JavaScript
    uni-app开发微信小程序
    本文详细介绍如何使用 uni-app 开发微信小程序,涵盖需求分析、架构思路及实施方案。主要功能包括用户登录、商品列表展示、商品详情、购物车及订单管理。技术栈采用 uni-app、uView UI 和 RESTful API。文章通过具体示例代码展示了从初始化项目、配置全局样式到实现各页面组件及 API 接口的全过程,并提供了完整的文件结构和配置文件示例。此外,还介绍了微信授权登录及后端接口模拟方法,确保项目的稳定性和安全性。通过本教程,读者可快速掌握使用 uni-app 开发微信小程序的方法。
    40 3
    |
    1月前
    |
    Web App开发 Android开发
    FFmpeg开发笔记(四十六)利用SRT协议构建手机APP的直播Demo
    实时数据传输在互联网中至关重要,不仅支持即时通讯如QQ、微信的文字与图片传输,还包括音视频通信。一对一通信常采用WebRTC技术,如《Android Studio开发实战》中的App集成示例;而一对多的在线直播则需部署独立的流媒体服务器,使用如SRT等协议。SRT因其优越的直播质量正逐渐成为主流。本文档概述了SRT协议的使用,包括通过OBS Studio和SRT Streamer进行SRT直播推流的方法,并展示了推流与拉流的成功实例。更多细节参见《FFmpeg开发实战》一书。
    38 1
    FFmpeg开发笔记(四十六)利用SRT协议构建手机APP的直播Demo
    |
    1月前
    |
    Web App开发 5G Linux
    FFmpeg开发笔记(四十四)毕业设计可做的几个拉满颜值的音视频APP
    一年一度的毕业季来临,计算机专业的毕业设计尤为重要,不仅关乎学业评价还积累实战经验。选择紧跟5G技术趋势的音视频APP作为课题极具吸引力。这里推荐三类应用:一是融合WebRTC技术实现视频通话的即时通信APP;二是具备在线直播功能的短视频分享平台,涉及RTMP/SRT等直播技术;三是具有自定义动画特效及卡拉OK歌词字幕功能的视频剪辑工具。这些项目不仅技术含量高,也符合市场需求,是毕业设计的理想选择。
    60 6
    FFmpeg开发笔记(四十四)毕业设计可做的几个拉满颜值的音视频APP
    |
    1月前
    |
    编解码 Java Android开发
    FFmpeg开发笔记(四十五)使用SRT Streamer开启APP直播推流
    ​SRT Streamer是一个安卓手机端的开源SRT协议直播推流框架,可用于RTMP直播和SRT直播。SRT Streamer支持的视频编码包括H264、H265等等,支持的音频编码包括AAC、OPUS等等,可谓功能强大的APP直播框架。另一款APP直播框架RTMP Streamer支持RTMP直播和RTSP直播,不支持SRT协议的直播。而本文讲述的SRT Streamer支持RTMP直播和SRT直播,不支持RTSP协议的直播。有关RTMP Streamer的说明参见之前的文章《使用RTMP Streamer开启APP直播推流》,下面介绍如何使用SRT Streamer开启手机直播。
    53 4
    FFmpeg开发笔记(四十五)使用SRT Streamer开启APP直播推流
    |
    19天前
    |
    开发框架 JavaScript 前端开发
    uni-app x 跨平台开发框架
    uni-app x 是一个强大的跨平台开发框架 uni-app x 是一个庞大的工程,它包括uts语言、uvue渲染引擎、uni的组件和API、以及扩展机制。
    17 1
    |
    28天前
    |
    IDE Java 开发工具
    探索安卓开发之旅:打造你的第一款App
    【8月更文挑战第24天】在这篇文章中,我们将一起踏上激动人心的安卓开发之旅。不论你是编程新手还是希望扩展技能的老手,本文将为你提供一份详尽指南,帮助你理解安卓开发的基础知识并实现你的第一个应用程序。从搭建开发环境到编写“Hello World”,每一步都将用浅显易懂的语言进行解释。那么,让我们开始吧!
    |
    2月前
    |
    存储 开发框架 安全
    鸿蒙 HarmonyOS NEXT星河版APP应用开发-阶段一
    HarmonyOS NEXT星河版的应用开发标志着华为分布式操作系统的全新篇章,它聚焦于打造原生精致、易用、流畅、安全、智能和互联的极致体验。开发者可以利用其先进的API和工具集,如DevEco Studio,构建高性能、跨设备无缝协同的应用程序,从而充分利用HarmonyOS的分布式能力,为用户带来一致且丰富的多场景数字生活体验。随着“学习强国”、岚图汽车、中国电信等知名企业和应用的加入,鸿蒙生态正迅速扩展,引领着原生应用开发的新趋势。
    78 3
    鸿蒙 HarmonyOS NEXT星河版APP应用开发-阶段一
    |
    2月前
    |
    Web App开发 缓存 编解码
    FFmpeg开发笔记(三十八)APP如何访问SRS推流的RTMP直播地址
    《FFmpeg开发实战》书中介绍了轻量级流媒体服务器MediaMTX,适合测试RTSP/RTMP协议,但不适用于复杂直播场景。SRS是一款强大的开源流媒体服务器,支持多种协议,起初为RTMP,现扩展至HLS、SRT等。在FFmpeg 6.1之前,推送给SRS的HEVC流不受支持。要播放RTMP流,Android应用可使用ExoPlayer,需在`build.gradle`导入ExoPlayer及RTMP扩展,并根据URL类型创建MediaSource。若SRS播放黑屏,需在配置文件中开启`gop_cache`以缓存关键帧。
    108 2
    FFmpeg开发笔记(三十八)APP如何访问SRS推流的RTMP直播地址
    |
    1月前
    |
    XML Android开发 UED
    "掌握安卓开发新境界:深度解析AndroidManifest.xml中的Intent-filter配置,让你的App轻松响应scheme_url,开启无限交互可能!"
    【8月更文挑战第2天】在安卓开发中,scheme_url 通过在`AndroidManifest.xml`中配置`Intent-filter`,使应用能响应特定URL启动或执行操作。基本配置下,应用可通过定义特定URL模式的`Intent-filter`响应相应链接。
    81 12