天天看点

如何开发视频会议App?

作者:APICloud

过去两年多时间里,视频会议成为职场工作乃至社会常态,在各类场景中得到广泛应用。例如企业会议、培训赋能、远程咨询、产品发布、远程面试等。本案例中的视频会议app来自开发者实战,采用YonBuilder移动开发平台(APICloud)的AVM.js多端框架进行开发。

使用AVM.js,一个技术栈可同时开发Android& iOS app、小程序和H5,且多端渲染效果统一;全新的app引擎3.0不依赖webView,提供百分百的原生渲染,保障app性能和体验与原生app一致;现有api直接映射兼容小程序接口,延续已有开发习惯;后台使用的PHP的thinkphp框架,通过composer集成各类插件。

思维导图

如何开发视频会议App?

功能介绍

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

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

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

4.历史会议:分为我主持的会议、我参与的会议。

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

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

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

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

应用模块

如何开发视频会议App?

项目目录

如何开发视频会议App?

应用展示

如何开发视频会议App?

开发过程

应用导航

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

如何开发视频会议App?

系统首页使用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
      }]
    }
  }           

动态权限

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

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

添加mianfest.xml文件;

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

具体的使用说明,在官方论坛中有专门的帖子,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) => {


          });
        }
            }           

WebSocket

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

具体的通讯原理步骤是:

会议发起人发起会议→通过websocket给参会人员发送消息指令→参会人员接收发送的websocket消息,通过监听触发进入会议房间,同时给会议发起人发送进入会议房间的消息→会议发起人收到有人进入了会议房间消息后,通过监听触发进入会议房间的操作。

这种流程是会议发起人不必先进入回房间进行等待,不用启用RTC模块,只有当有其他人员收到提醒进入会议房间后才会启用RTC模块进入房间,可以有效避免资源浪费。

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

这种模式如果其他参会人员不及时参加会议的时候会造成部分资源的浪费。

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

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

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

如何开发视频会议App?

AVM框架里官方就集成了websocket。

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));
      });
    }
  },           

视频通话RTC

使用的是tencentTRTC模块。

如何开发视频会议App?
如何开发视频会议App?

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

为什么用tencentTRTC?tencentTRTC模块不会把SDKappId与应用进行绑定,这样就可以使用一个SDKappId来实现两个不同的app之间的视频通话了,共用腾讯云的通话时长。

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

消息事件

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

如何开发视频会议App?
如何开发视频会议App?

举例说明:

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

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

消息推送

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

如何开发视频会议App?
//初始化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恢复事件
      })      
    },           

短信验证码

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

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

如何开发视频会议App?

示例代码

<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>           

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

清空缓存

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

如何开发视频会议App?
如何开发视频会议App?

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

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

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

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

AVM组件使用

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

如何开发视频会议App?

其中视频通话组件(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模块唤醒第三方工具进行文件浏览。

如何开发视频会议App?

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

//下载、浏览附件
    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('下载失败,请重试。');
          }
      });
    }           
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();
    }
  });
}           

单设备登陆

本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)
          })
        })
      }           
//验证设备
    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
        });
      })
    }           

接口调用

封装了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
}           

在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>           

后台代码

代码示例

<?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();
    }
  }
}           

插件引用

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

composer.json文件:

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

视频会议app开发到这里就完成了,更多教程可以查看历史文章!

继续阅读