From 2635a46c525775135bb68d429a5f367144102e5e Mon Sep 17 00:00:00 2001
From: liyong <liyong@3mang.com>
Date: Fri, 24 Nov 2017 18:25:47 +0800
Subject: [PATCH] 新增音频禁用控制功能

---
 src/EngineEntrance.js         |  32 +++++++++++++++++++++++++++++---
 src/EngineUtils.js            | 400 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 src/MessageTypes.js           |   2 ++
 src/apes/ApeConsts.js         |   2 ++
 src/apes/ConferApe.js         |  46 ++++++++++++++++++++++++++++++++++++++++++----
 src/apes/WebRtcApe.js         | 426 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------------------------------------------------------
 src/assets/css/mcuStyle.css   |  18 ++++++++++++++++++
 src/assets/img/audioClose.png | Bin 0 -> 562 bytes
 src/assets/img/audioOpen.png  | Bin 0 -> 733 bytes
 9 files changed, 570 insertions(+), 356 deletions(-)
 create mode 100644 src/assets/css/mcuStyle.css
 create mode 100644 src/assets/img/audioClose.png
 create mode 100644 src/assets/img/audioOpen.png

diff --git a/src/EngineEntrance.js b/src/EngineEntrance.js
index 6ef3bf7..1c814cb 100644
--- a/src/EngineEntrance.js
+++ b/src/EngineEntrance.js
@@ -63,7 +63,7 @@ export default class MessageEntrance extends Emiter {
     super();
     this.lastClassActiveTime=0;//最后一次课堂激活的时间戳
     //sdk 信息
-    GlobalConfig.sdkVersion = "v2.32.1.20171123";
+    GlobalConfig.sdkVersion = "v2.33.6.20171123";
     loger.warn("sdkVersion:" + GlobalConfig.sdkVersion);
     console.log("sdkVersion:" + GlobalConfig.sdkVersion);
     //设置
@@ -102,6 +102,7 @@ export default class MessageEntrance extends Emiter {
     _webRtc.on(MessageTypes.USER_DEVICE_STATUS_CHAANGE, this.userDeviecStatusChange.bind(this)); //监听摄像头和麦克风的开启状态
     _webRtc.on(MessageTypes.MEDIA_PUBLISH_STATUS_CHANGE, this.mediaPublishStatusChange.bind(this)); //webRtc推流状态发生改变
     _webRtc.on(WebRtcApe.RE_JOIN_CHANNEL, this._webRtcRejoinChannel.bind(this)); //重先加入音视频频道
+    _webRtc.on(MessageTypes.MEDIA_ENABLED_CHANGE, this._mediaEnabledChange.bind(this)); //音视频禁用状态改变
 
     // Sass平台层
     _sass = Sass;
@@ -152,6 +153,7 @@ export default class MessageEntrance extends Emiter {
     //_confer_ape.on(MessageTypes.SWITCH_HLS_IP, this._switchHlsIpHandler.bind(this)); //MS HLS动态选点
     _confer_ape.on(MessageTypes.STOP_ALL_MEDIA_PUBLISH, this._stopAllMediaPublishHandler.bind(this)); //课堂状态发生改变,需要停止当前的所有推流
     _confer_ape.on(MessageTypes.CLASS_UPDATE_ROSTER, this._onRosterUpdateHandler.bind(this));
+    _confer_ape.on(MessageTypes.RECEIVE_MEDIA_ENABLED_CHANGE, this._receiveMeiaEnabledChange.bind(this));
 
     _chat_ape = new ChatApe();
     _chat_ape.on('*', (type, data) => this._emit(type, data));
@@ -651,6 +653,8 @@ export default class MessageEntrance extends Emiter {
       randUserId = "A" + randUserId;
     } else if (GlobalConfig.userRole == ApeConsts.presenter) {
       randUserId = "P" + randUserId;
+    } else  if (GlobalConfig.userRole == ApeConsts.invisible) {
+      randUserId = "I" + randUserId;
     } else {
       randUserId = "S" + randUserId;
     }
@@ -700,7 +704,8 @@ export default class MessageEntrance extends Emiter {
     let autoLoginMd5 = MD5("" + GlobalConfig.classId + GlobalConfig.paramUserId + GlobalConfig.userRole);
     //let autoLoginMd5 = MD5("" + GlobalConfig.classId + GlobalConfig.userId + GlobalConfig.userRole);
     //loger.log("joinClass-GlobalConfig.autoLogin", GlobalConfig.autoLogin, "autoLoginMd5-", autoLoginMd5);
-    if (GlobalConfig.autoLogin && autoLoginMd5 == GlobalConfig.autoLogin || GlobalConfig.isInvisible) {
+    if ((GlobalConfig.autoLogin && autoLoginMd5 == GlobalConfig.autoLogin) ||
+      GlobalConfig.isInvisible||GlobalConfig.isAssistant||GlobalConfig.isPresenter) {
       // MD5(classId+userId+userRole)==m
       //自动登录,跳过验证流程
       loger.log("自动登录->" + GlobalConfig.userRole);
@@ -1404,7 +1409,17 @@ export default class MessageEntrance extends Emiter {
       _webRtc.tryAddMobileStream(_data.nodeId);
     }
   }
-
+  /*
+  * 收到媒体禁用状态切换的消息
+  * */
+  _receiveMeiaEnabledChange(_data){
+    if(!_data||GlobalConfig.isRecordPlayBack||GlobalConfig.isH5){
+      return;
+    }
+    if(_webRtc){
+      _webRtc.webRtcMeiaEnabledChange(_data);
+    }
+  }
   //手动切换MS -> {ip;"xxx.xx.xx","port":"xxxx"}
   _switchMediaServer(_param) {
     if (GlobalConfig.isRecordPlayBack) {
@@ -2153,6 +2168,17 @@ export default class MessageEntrance extends Emiter {
   _webRtcRejoinChannel(_data){
     this._reJoinChannel(_data)
   }
+  /*
+   * 监听webRtc
+   * */
+  _mediaEnabledChange(_data){
+    if (!_mcu.connected||GlobalConfig.isRecordPlayBack) {
+      return;
+    }
+    if(_confer_ape){
+      _confer_ape.sendMediaEnabledChange(_data);
+    }
+  }
 
   //监听摄像头麦克风状态
   userDeviecStatusChange(_data) {
diff --git a/src/EngineUtils.js b/src/EngineUtils.js
index bd8a516..b23f04f 100644
--- a/src/EngineUtils.js
+++ b/src/EngineUtils.js
@@ -4,212 +4,222 @@
 import Base64 from 'base64-js';
 import UTF8 from 'utf-8';
 
-class EngineUtils{
-    static isEmptyObject(O){
-        for (var x in O){
-            return false;
-        }
-        return true;
+class EngineUtils {
+  static isEmptyObject(O) {
+    for (var x in O) {
+      return false;
     }
-    static arrayToJsonString(_param){
-        try{
-            return JSON.stringify(_param);
-        }catch (err){
-            console.warn("数组转JSON失败->ERROR:"+err.message);
-        }
-        return "";
+    return true;
+  }
+
+  static arrayToJsonString(_param) {
+    try {
+      return JSON.stringify(_param);
+    } catch (err) {
+      console.warn("数组转JSON失败->ERROR:" + err.message);
     }
-    static arrayFromJsonString(_param){
-        try{
-            return JSON.parse(_param);
-        }catch (err){
-            console.warn("JSON转数组失败->ERROR:"+err.message);
-        }
-        return [];
+    return "";
+  }
+
+  static arrayFromJsonString(_param) {
+    try {
+      return JSON.parse(_param);
+    } catch (err) {
+      console.warn("JSON转数组失败->ERROR:" + err.message);
     }
+    return [];
+  }
 
-    /*
-    * 生成随机数   _part 段数 默认3段;   splitStr分隔符
-    * */
-    static creatRandomNum(_part=3,splitStr=""){
-        let randNumStr="";
-        for(let i=0;i<_part;i++){
-            randNumStr+=splitStr+parseInt(Math.random()*1000);
-        }
-        return randNumStr;
-    }
-
-    //生成时间戳后9位 保证唯一
-    static creatSoleNumberFromTimestamp(){
-        let time   = new Date().getTime();
-        let timestamp= time % 1000000000;//time后9位
-        return timestamp;
-    }
-
-    //生成时间戳毫秒
-    static creatTimestamp(){
-        let time   = parseInt(new Date().getTime()/1000);//精确到秒
-        return time;
-    }
-    //生成时间戳 string
-    static creatTimestampStr(){
-        let curTime = new Date();
-        let timeStr = "" + curTime.getFullYear() + "-";
-        timeStr += (curTime.getMonth()+1) + "-";
-        timeStr += curTime.getDate() + "-";
-        timeStr+=curTime.getHours() + "-";
-        timeStr+=curTime.getMinutes() + "-";
-        timeStr+=curTime.getSeconds();
-        return timeStr;
-    }
-    //根据时间戳字符串转换为时间戳  2017-10-27-15-38-15
-    static getTimestampFromStr(_timeStr){
-        if(!_timeStr) return this.creatTimestamp();
-        let timeArr=_timeStr.split("_");
-        if(timeArr&&timeArr.length==6){
-            return parseInt( new Date(timeArr[0],parseInt(timeArr[1])-1,timeArr[2],timeArr[3],timeArr[4],timeArr[5]).getTime()/1000);
-        }
-        return this.creatTimestamp();
+  /*
+   * 生成随机数   _part 段数 默认3段;   splitStr分隔符
+   * */
+  static creatRandomNum(_part = 3, splitStr = "") {
+    let randNumStr = "";
+    for (let i = 0; i < _part; i++) {
+      randNumStr += splitStr + parseInt(Math.random() * 1000);
     }
-    //生成时间戳  格式:"20170209"
-    static creatTimestampYMD(){
-        let curTime = new Date();
-        let year = "" + curTime.getFullYear();
-        let month = "" +(curTime.getMonth()+1);
-        let day = "" + curTime.getDate();
+    return randNumStr;
+  }
 
-        if(month.length<2){
-            month="0"+month;
-        }
-        if(day.length<2){
-            day="0"+day;
-        }
-        return year+month+day;
-    }
-    static objectToBase64(_object){
-        try{
-            let _objectStr=JSON.stringify(_object);
-            //console.log("objectToBase64------1----------")
-            let byte=UTF8.setBytesFromString(_objectStr);
-            //console.log("objectToBase64------2----------")
-            let _objectBase64=Base64.fromByteArray(byte);
-            return _objectBase64
-        }catch (err){
-            console.log("objectToBase64 err:"+err.message);
-            return "";
-        }
-        return ""
-    }
-    static objectFromBase64(_objectBase64){
-        try{
-            let byte=Base64.toByteArray(_objectBase64);
-            let _objectStr=UTF8.getStringFromBytes(byte);
-            let _object=JSON.parse(_objectStr);
-            return _object
-        }catch (err){
-            console.log("objectFromBase64 err:"+err.message);
-            return null;
-        }
-        return null;
+  //生成时间戳后9位 保证唯一
+  static creatSoleNumberFromTimestamp() {
+    let time = new Date().getTime();
+    let timestamp = time % 1000000000;//time后9位
+    return timestamp;
+  }
+
+  //生成时间戳毫秒
+  static creatTimestamp() {
+    let time = parseInt(new Date().getTime() / 1000);//精确到秒
+    return time;
+  }
+
+  //生成时间戳 string
+  static creatTimestampStr() {
+    let curTime = new Date();
+    let timeStr = "" + curTime.getFullYear() + "-";
+    timeStr += (curTime.getMonth() + 1) + "-";
+    timeStr += curTime.getDate() + "-";
+    timeStr += curTime.getHours() + "-";
+    timeStr += curTime.getMinutes() + "-";
+    timeStr += curTime.getSeconds();
+    return timeStr;
+  }
+
+  //根据时间戳字符串转换为时间戳  2017-10-27-15-38-15
+  static getTimestampFromStr(_timeStr) {
+    if (!_timeStr) return this.creatTimestamp();
+    let timeArr = _timeStr.split("_");
+    if (timeArr && timeArr.length == 6) {
+      return parseInt(new Date(timeArr[0], parseInt(timeArr[1]) - 1, timeArr[2], timeArr[3], timeArr[4], timeArr[5]).getTime() / 1000);
     }
-    //优化压缩坐标点数组,去除一些连续重复坐标的点,返回一个数组
-    static compressPoint(_arr){
-        if(!_arr){
-            return [];
-        }
-        if(_arr.length<5){
-            //点,直线,矩形 坐标点小于5不需要处理
-            return _arr;
-        }
-        let tempPointArr=_arr;
-        let newPointArr=[];
-        newPointArr.push(tempPointArr[0]);
-        let lastW=tempPointArr[0].w;
-        let continueNum=0;//坐标相同的连续次数
-        //先筛除水平方向的连续重复坐标点
-        let len=tempPointArr.length-1;
-        for(let i=1;i<len;i++){
-            let item=tempPointArr[i];
-            if(item&&item.w!=lastW){
-                lastW=item.w;
-                if(continueNum>0){
-                    newPointArr.push(tempPointArr[i-1]);
-                }
-                newPointArr.push(item);
-                continueNum=0;
-            }else {
-                continueNum++;
-            }
-        }
-        //如果最终的坐标点数量小于2,需要把最后一个坐标点添加
-        if(tempPointArr[len]){
-            newPointArr.push(tempPointArr[len]);
-        }
-        //如果坐标点已经小于等于2不需要继续检测
-        if(newPointArr.length<=2){
-            return newPointArr;
-        }
+    return this.creatTimestamp();
+  }
 
-        //筛除水垂直向的连续重复坐标点
-        let finalPointArr=[];
-        finalPointArr.push(newPointArr[0]);
-        let lastH=newPointArr[0].h;
-        continueNum=0;
-        len=newPointArr.length-1;
-        for(let k=1;k<len;k++){
-            let item=newPointArr[k];
-            if(item&&item.h!=lastH){
-                lastH=item.h;
-                if(continueNum>0){
-                    finalPointArr.push(newPointArr[k-1]);
-                }
-                finalPointArr.push(item);
-                continueNum=0;
-            }else {
-                continueNum++;
-            }
-        }
-        if(newPointArr[len]){
-            finalPointArr.push(newPointArr[len]);
-        }
-        return finalPointArr;
+  //生成时间戳  格式:"20170209"
+  static creatTimestampYMD() {
+    let curTime = new Date();
+    let year = "" + curTime.getFullYear();
+    let month = "" + (curTime.getMonth() + 1);
+    let day = "" + curTime.getDate();
+
+    if (month.length < 2) {
+      month = "0" + month;
     }
-    //压缩数据,把坐标点数组转换为字符串,返回字符串
-    static optimizePoint(_pointGroup){
-        if(!_pointGroup){
-            return "";
-        }
-        let tempStr="";
-        try {
-            tempStr=JSON.stringify(_pointGroup);
-        }catch (err){
-            return "";
-        }
-        let regexp1=/},{"w":/g;
-        let regexp2=/,"h":/g;
-        tempStr=tempStr.replace(regexp1,"#");
-        tempStr=tempStr.replace(regexp2,"&");
-        //console.log("标注压缩后的字符长度->",tempStr.length);
-        return tempStr;
-    }
-    //把字符串坐标点集合转换为数组,返回数组的JSON
-    static unPackOptimizePoint(_str){
-        if(!_str){
-            return "";
-        }
-        let tempStr=_str;
-        let regexp1=/#/g;
-        let regexp2=/&/g;
-        tempStr=tempStr.replace(regexp1,'},{"w":');
-        tempStr=tempStr.replace(regexp2,',"h":');
+    if (day.length < 2) {
+      day = "0" + day;
+    }
+    return year + month + day;
+  }
 
-        let dataArr=[];
-        try {
-            dataArr=JSON.parse(tempStr);
-        }catch (err){
+  static objectToBase64(_object) {
+    try {
+      let _objectStr = JSON.stringify(_object);
+      //console.log("objectToBase64------1----------")
+      let byte = UTF8.setBytesFromString(_objectStr);
+      //console.log("objectToBase64------2----------")
+      let _objectBase64 = Base64.fromByteArray(byte);
+      return _objectBase64
+    } catch (err) {
+      console.log("objectToBase64 err:" + err.message);
+      return "";
+    }
+    return ""
+  }
+
+  static objectFromBase64(_objectBase64) {
+    try {
+      let byte = Base64.toByteArray(_objectBase64);
+      let _objectStr = UTF8.getStringFromBytes(byte);
+      let _object = JSON.parse(_objectStr);
+      return _object
+    } catch (err) {
+      console.log("objectFromBase64 err:" + err.message);
+      return null;
+    }
+    return null;
+  }
+
+  //优化压缩坐标点数组,去除一些连续重复坐标的点,返回一个数组
+  static compressPoint(_arr) {
+    if (!_arr) {
+      return [];
+    }
+    if (_arr.length < 5) {
+      //点,直线,矩形 坐标点小于5不需要处理
+      return _arr;
+    }
+    let tempPointArr = _arr;
+    let newPointArr = [];
+    newPointArr.push(tempPointArr[0]);
+    let lastW = tempPointArr[0].w;
+    let continueNum = 0;//坐标相同的连续次数
+    //先筛除水平方向的连续重复坐标点
+    let len = tempPointArr.length - 1;
+    for (let i = 1; i < len; i++) {
+      let item = tempPointArr[i];
+      if (item && item.w != lastW) {
+        lastW = item.w;
+        if (continueNum > 0) {
+          newPointArr.push(tempPointArr[i - 1]);
+        }
+        newPointArr.push(item);
+        continueNum = 0;
+      } else {
+        continueNum++;
+      }
+    }
+    //如果最终的坐标点数量小于2,需要把最后一个坐标点添加
+    if (tempPointArr[len]) {
+      newPointArr.push(tempPointArr[len]);
+    }
+    //如果坐标点已经小于等于2不需要继续检测
+    if (newPointArr.length <= 2) {
+      return newPointArr;
+    }
+
+    //筛除水垂直向的连续重复坐标点
+    let finalPointArr = [];
+    finalPointArr.push(newPointArr[0]);
+    let lastH = newPointArr[0].h;
+    continueNum = 0;
+    len = newPointArr.length - 1;
+    for (let k = 1; k < len; k++) {
+      let item = newPointArr[k];
+      if (item && item.h != lastH) {
+        lastH = item.h;
+        if (continueNum > 0) {
+          finalPointArr.push(newPointArr[k - 1]);
+        }
+        finalPointArr.push(item);
+        continueNum = 0;
+      } else {
+        continueNum++;
+      }
+    }
+    if (newPointArr[len]) {
+      finalPointArr.push(newPointArr[len]);
+    }
+    return finalPointArr;
+  }
+
+  //压缩数据,把坐标点数组转换为字符串,返回字符串
+  static optimizePoint(_pointGroup) {
+    if (!_pointGroup) {
+      return "";
+    }
+    let tempStr = "";
+    try {
+      tempStr = JSON.stringify(_pointGroup);
+    } catch (err) {
+      return "";
+    }
+    let regexp1 = /},{"w":/g;
+    let regexp2 = /,"h":/g;
+    tempStr = tempStr.replace(regexp1, "#");
+    tempStr = tempStr.replace(regexp2, "&");
+    //console.log("标注压缩后的字符长度->",tempStr.length);
+    return tempStr;
+  }
+
+  //把字符串坐标点集合转换为数组,返回数组的JSON
+  static unPackOptimizePoint(_str) {
+    if (!_str) {
+      return "";
+    }
+    let tempStr = _str;
+    let regexp1 = /#/g;
+    let regexp2 = /&/g;
+    tempStr = tempStr.replace(regexp1, '},{"w":');
+    tempStr = tempStr.replace(regexp2, ',"h":');
+
+    let dataArr = [];
+    try {
+      dataArr = JSON.parse(tempStr);
+    } catch (err) {
 
-        }
-        return dataArr;
     }
+    return dataArr;
+  }
 }
 export  default EngineUtils;
\ No newline at end of file
diff --git a/src/MessageTypes.js b/src/MessageTypes.js
index 9984fb7..ac992c4 100644
--- a/src/MessageTypes.js
+++ b/src/MessageTypes.js
@@ -111,6 +111,8 @@ MessageTypes.WEB_RTC_JOIN_FAILED = "web_rtc_join_failed";
 MessageTypes.WEB_RTC_PUBLISH_FAILED = "web_rtc_publish_failed";
 MessageTypes.GET_DEVICES_SUCCESS = "get_devices_success";
 MessageTypes.MEDIA_PUBLISH_STATUS_CHANGE = "media_publish_status_change";//音视频推流的状态发生改变
+MessageTypes.MEDIA_ENABLED_CHANGE = "media_enabled_change";//媒体禁用或开启状态改变
+MessageTypes.RECEIVE_MEDIA_ENABLED_CHANGE = "receive_media_enabled_change";//收到媒体禁用状态控制的消息
 
 //MCU MS
 MessageTypes.SWITCH_MCU_IP = "switch_mcu_ip"; //切换mcu 重新选点
diff --git a/src/apes/ApeConsts.js b/src/apes/ApeConsts.js
index da9a8ed..8ed675f 100644
--- a/src/apes/ApeConsts.js
+++ b/src/apes/ApeConsts.js
@@ -27,6 +27,8 @@ ApeConsts.USER_ACTION_SILENCE_STATUS_CHANGE = 3; //更改用户的禁言状态
 ApeConsts.CLASS_ACTION_KICK_OUT_ROSTER=4; //指定的人踢出课堂
 ApeConsts.STOP_ALL_PUBLISH=5; //所有人停止推流
 ApeConsts.CLASS_ACTION_DRAW_STATUS_CHANGE = 6; //更改用户的画笔状态
+ApeConsts.MEDIA_ENABLED_CHANGE = 7; //更改用户的音视频禁用状态
+
 
 //课堂类型 1:1v1(2路流) 2:直播(1路流) 3:小班课(可以多路流)
 ApeConsts.CLASS_TYPE_1v1 = 1; // 互动课堂,通过MS转发音视频,不能进行H5观看 1v1(2路流)
diff --git a/src/apes/ConferApe.js b/src/apes/ConferApe.js
index aa80403..8bd0277 100644
--- a/src/apes/ConferApe.js
+++ b/src/apes/ConferApe.js
@@ -855,6 +855,17 @@ class ConferApe extends Ape {
           loger.warn('chatMsg->JSON数据解析失败');
         }
         break;
+      case ApeConsts.MEDIA_ENABLED_CHANGE:
+        let mediaMsgObj = null;
+        try {
+          mediaMsgObj = JSON.parse(chatMsg.message);
+          if (mediaMsgObj) {
+            this.receiveChangeUserMediaEnabledStatus(mediaMsgObj);
+          }
+        } catch (err) {
+          loger.warn('chatMsg->JSON数据解析失败',chatMsg.message);
+        }
+        break;
       default:
         break;
     }
@@ -1028,6 +1039,11 @@ class ConferApe extends Ape {
       loger.log("监课进入或更新数据");
       return;
     }
+    if (nodeData.role == ApeConsts.NR_ASSISTANT && GlobalConfig.userRole != ApeConsts.invisible && GlobalConfig.userRole != ApeConsts.assistant) {
+      loger.log("助教进入或更新数据");
+      return;
+    }
+
     if (!rosterExists) {
       //this.rosterLen = Object.keys(this.rosters).length;
       //GlobalConfig.rosterNum = this.rosterLen;//记录当前的总人数
@@ -1091,11 +1107,36 @@ class ConferApe extends Ape {
     }
   }
 
-  //设备状态更新
+  /*
+  * 设备状态更新
+  * */
   updaterUserDeviecStatusChange(_data) {
     loger.log("音视频设备状态更新->", _data);
     this.updateUserInfo();
   }
+  /*
+   * 媒体开启或禁用
+   * */
+  sendMediaEnabledChange(_data){
+    if(!_data){
+      return;
+    }
+
+    try{
+      loger.log("发送->媒体开启或禁用",_data);
+      this.sendConferMsg({"to": 0, "message":JSON.stringify(_data), "actionType": ApeConsts.MEDIA_ENABLED_CHANGE});
+    }catch (err){
+      loger.warn("发送->媒体开启或禁用->失败",_data,err.message);
+    }
+
+  }
+
+  /*
+   * 收到媒体开启或禁用控制
+   * */
+  receiveChangeUserMediaEnabledStatus(_data){
+    this._emit(MessageTypes.RECEIVE_MEDIA_ENABLED_CHANGE,_data);
+  }
 
   //删除用户
   rosterDelHandler(nodeId) {
@@ -1114,12 +1155,9 @@ class ConferApe extends Ape {
         if(GlobalConfig.classType!= ApeConsts.CLASS_TYPE_ZHIBO){
           loger.log(nodeId, "->离开课堂->身份->", user.userRole);
         }
-
       }
       delete this.rosters[nodeId];
       GlobalConfig.rosters = this.rosters;
-      //this.rosterLen = Object.keys(this.rosters).length;
-      //GlobalConfig.rosterNum = this.rosterLen;//记录当前的总人数
 
       if(!GlobalConfig.isH5) {
         this.emitRosterChange();
diff --git a/src/apes/WebRtcApe.js b/src/apes/WebRtcApe.js
index 0634b5d..31f822d 100644
--- a/src/apes/WebRtcApe.js
+++ b/src/apes/WebRtcApe.js
@@ -1,6 +1,8 @@
 //*
 // WebRtc
 // */
+require('../assets/css/mcuStyle.css');
+
 import Emiter from 'Emiter';
 import Loger from "Loger";
 import $ from "jquery";
@@ -10,12 +12,13 @@ import EngineUtils from 'EngineUtils';
 import MessageTypes from 'MessageTypes';
 var AgoraRTC = require('../AgoraRTCSDK-1.14.0');
 let loger = Loger.getLoger('WebRtcApe');
-const SIZE_480=480;
-const SIZE_360=360;
-const SIZE_320=320;
-const SIZE_240=240;
-const SIZE_160=160;
-const SIZE_120=120;
+const SIZE_480 = 480;
+const SIZE_360 = 360;
+const SIZE_320 = 320;
+const SIZE_240 = 240;
+const SIZE_160 = 160;
+const SIZE_120 = 120;
+
 class WebRtcApe extends Emiter {
   constructor() {
     super();
@@ -24,10 +27,10 @@ class WebRtcApe extends Emiter {
     this.appCertificate = "";
     this.appRecordingKey = "";
 
-    this.setConfigTimestamp=0;//设置旁路地址的时间戳
+    this.setConfigTimestamp = 0;//设置旁路地址的时间戳
     this.configPublisherUrl = "";//旁路地址;
-    this.m3u8Url="";//旁路拉流地址
-    this.rtmpUrl="";//旁路拉流地址
+    this.m3u8Url = "";//旁路拉流地址
+    this.rtmpUrl = "";//旁路拉流地址
 
     this.channelKey = null;
     this.channelId = "";
@@ -47,10 +50,10 @@ class WebRtcApe extends Emiter {
 
     this.remoteVideoList = {};//记录远程视频流
 
-    this.pWidth=SIZE_480;
-    this.pHeight=SIZE_360;
-    this.pFrameRate=30;
-    this.pBitrate=500;
+    this.pWidth = SIZE_480;
+    this.pHeight = SIZE_360;
+    this.pFrameRate = 30;
+    this.pBitrate = 500;
 
     //120P	0	160x120	15	65
     //240P	20	320x240	15	200
@@ -58,10 +61,10 @@ class WebRtcApe extends Emiter {
     this.videoResolution = "240P";
     this.isOpenVideo = true;
 
-    this.firstPublishSuccess=false;//记录加入频道成功之后是否推流成功过,离开频道之后需要设置为false
+    this.firstPublishSuccess = false;//记录加入频道成功之后是否推流成功过,离开频道之后需要设置为false
 
     this.isPublish = false;//当前是否正在推流
-    this.videoScale=1;//视图的缩放比例,默认为1;
+    this.videoScale = 1;//视图的缩放比例,默认为1;
 
 
     this.normalRemoteViewId = "";
@@ -85,10 +88,10 @@ class WebRtcApe extends Emiter {
     this.invisibleVideoHeight = SIZE_360;
     this.xdyRemote = "xdy_remote";
 
-    this.localWebRtcVideoClass='localWebRtcVideoClass';//本地视图统一的class名称
-    this.invisibleWebRtcVideoClass='invisibleWebRtcVideoClass';
-    this.normalWebRtcVideoClass='normalWebRtcVideoClass';
-    this.hostWebRtcVideoClass='hostWebRtcVideoClass';
+    this.localWebRtcVideoClass = 'localWebRtcVideoClass';//本地视图统一的class名称
+    this.invisibleWebRtcVideoClass = 'invisibleWebRtcVideoClass';
+    this.normalWebRtcVideoClass = 'normalWebRtcVideoClass';
+    this.hostWebRtcVideoClass = 'hostWebRtcVideoClass';
 
     //webRtc sdk
     this.client = AgoraRTC.createClient({mode: this.mode});
@@ -146,7 +149,7 @@ class WebRtcApe extends Emiter {
     this.client.on('stream-published', (evt)=> {
       loger.log("webRtc->推流成功->", new Date().getTime());
       this.isPublish = true;
-      this.firstPublishSuccess=true;
+      this.firstPublishSuccess = true;
       GlobalConfig.openCamera = EngineUtils.creatTimestamp();
       GlobalConfig.openMicrophones = GlobalConfig.openCamera;
       this._emit(MessageTypes.USER_DEVICE_STATUS_CHAANGE, {
@@ -208,41 +211,47 @@ class WebRtcApe extends Emiter {
       }
 
       //添加之前先删除之前存在的重复视图
-      let len=$("#"+this.xdyRemote + uid+" #player_" + uid).length;
-      if(len<1){
-        loger.log("添加之前先删除之前存在的重复视图",uid);
-        $("#"+this.xdyRemote + uid).remove();
+      let len = $("#" + this.xdyRemote + uid + " #player_" + uid).length;
+      if (len < 1) {
+        loger.log("添加之前先删除之前存在的重复视图", uid);
+        $("#" + this.xdyRemote + uid).remove();
+      }
+      let audioMutedDiv = "";
+      if (GlobalConfig.isTeachOrAssistant || GlobalConfig.isInvisible) {
+        audioMutedDiv = `<div class="audioMuted audioOpen" id=${"audioMuted_"+uid}></div>`;
       }
 
       if (userRole == ApeConsts.invisible) {
-        let nameDiv = `<div style=${this.invisibleVideoWidth}px;height:20px; position: absolute;bottom: 0; z-index: 1;overflow:hidden;font-size: 14px;text-align: right; color: #cccccc;display:${this.nameDisplay}">${userName}</div>`;
+        let nameDiv = `<div style=${this.invisibleVideoWidth}px;height:22px; position: absolute;bottom: 2px; z-index: 1;overflow:hidden;font-size: 14px;text-align: right; vertical-align: middle;background-color: #2926251a;color: #e7e7e7display:${this.nameDisplay}">${userName + audioMutedDiv}</div>`;
         //把远程视频添加到监课列表
         loger.log("获取远程视频流成功->监课:" + userName + "->" + uid, new Date().getTime());
-        let viewDiv = `<div id="${this.xdyRemote + uid}" class="${this.invisibleWebRtcVideoClass}" style="width:${this.invisibleVideoWidth*this.videoScale}px;height:${this.invisibleVideoHeight*this.videoScale}px;float: left;margin-right: 1px;pointer-events: none;">${nameDiv}</div>`;
+        let viewDiv = `<div id="${this.xdyRemote + uid}" class="${this.invisibleWebRtcVideoClass}" style="width:${this.invisibleVideoWidth * this.videoScale}px;height:${this.invisibleVideoHeight * this.videoScale}px;float: left;margin-right: 1px;">${nameDiv}</div>`;
         $(this.invisibleViewId).append(viewDiv);
-      } else if (userRole == ApeConsts.host||userRole == ApeConsts.assistant||userRole == ApeConsts.presenter) {
-        let nameDiv = `<div style="width:${this.hostRemoteVideoWidth}px;height:20px; position: absolute;bottom: 0; z-index: 1;overflow:hidden;font-size: 14px;text-align: right; color: #cccccc;display:${this.nameDisplay}">${userName}</div>`;
+      } else if (userRole == ApeConsts.host || userRole == ApeConsts.assistant || userRole == ApeConsts.presenter) {
+        let nameDiv = `<div style="width:${this.hostRemoteVideoWidth}px;height:22px; position: absolute;bottom: 2px; z-index: 1;overflow:hidden;font-size: 14px;text-align: right;vertical-align: middle; background-color: #2926251a;color: #e7e7e7;display:${this.nameDisplay}">${userName + audioMutedDiv}</div>`;
         //把远程视图添加到老师列表
-        loger.log("获取远程视频流成功->userRole:"+userRole+":" + userName + "->" + uid, new Date().getTime());
-        let viewDiv = `<div id="${this.xdyRemote + uid}" class="${this.hostWebRtcVideoClass}" style="width:${this.hostRemoteVideoWidth*this.videoScale}px;height:${this.hostRemoteVideoHeight*this.videoScale}px;float: left;margin-right: 1px;pointer-events: none;">${nameDiv}</div>`;
+        loger.log("获取远程视频流成功->userRole:" + userRole + ":" + userName + "->" + uid, new Date().getTime());
+        let viewDiv = `<div id="${this.xdyRemote + uid}" class="${this.hostWebRtcVideoClass}" style="width:${this.hostRemoteVideoWidth * this.videoScale}px;height:${this.hostRemoteVideoHeight * this.videoScale}px;float: left;margin-right: 1px;">${nameDiv}</div>`;
         $(this.hostRemoteViewId).prepend(viewDiv);
       } else {
-        let nameDiv = `<div style="width:${this.normalRemoteVideoWidth}px;height:20px; position: absolute;bottom: 0; z-index: 1;overflow:hidden;font-size: 14px;text-align: right; color: #cccccc;display:${this.nameDisplay}">${userName}</div>`;
+        let nameDiv = `<div style="width:${this.normalRemoteVideoWidth}px;height:22px; position: absolute;bottom: 2px; z-index: 1;overflow:hidden;font-size: 14px;text-align: right;vertical-align: middle;background-color: #2926251a;color: #e7e7e7;display:${this.nameDisplay}">${userName + audioMutedDiv}</div>`;
         //把视图添加到学生列表
         loger.log("获取远程视频流成功->学生:" + userName + "->" + uid, new Date().getTime());
-        let viewDiv = `<div id="${this.xdyRemote + uid}" class="${this.normalWebRtcVideoClass}" style="width:${this.normalRemoteVideoWidth*this.videoScale}px;height:${this.normalRemoteVideoHeight*this.videoScale}px;float: left;margin-right: 1px;pointer-events: none;">${nameDiv}</div>`;
+        let viewDiv = `<div id="${this.xdyRemote + uid}" class="${this.normalWebRtcVideoClass}" style="width:${this.normalRemoteVideoWidth * this.videoScale}px;height:${this.normalRemoteVideoHeight * this.videoScale}px;float: left;margin-right: 1px;">${nameDiv}</div>`;
         $(this.normalRemoteViewId).append(viewDiv);
       }
+      $("#audioMuted_" + uid).off("click", this._clickAudioMuted.bind(this));
+      $("#audioMuted_" + uid).on("click", this._clickAudioMuted.bind(this));
       //播放视频,隐藏控制条
       try {
-        $("bar_" + stream.getId()).hide();
+        $("#bar_" + stream.getId()).hide();
         stream.play(this.xdyRemote + stream.getId());
       } catch (err) {
         //添加之前先删除之前存在的重复视图
-        let len=$("#"+this.xdyRemote + uid+" #player_" + uid).length;
-        if(len<1){
-          lloger.log("流播放失败->删除视图",uid);
-          $("#"+this.xdyRemote + uid).remove();
+        let len = $("#" + this.xdyRemote + uid + " #player_" + uid).length;
+        if (len < 1) {
+          loger.log("流播放失败->删除视图", uid);
+          $("#" + this.xdyRemote + uid).remove();
         }
       }
       if (user && (user.deviceType == GlobalConfig.deviceIOS || user.deviceType == GlobalConfig.deviceAndroid)) {
@@ -252,43 +261,44 @@ class WebRtcApe extends Emiter {
 
     this.clearInvalidVideoView();
   }
+
   //清除无效的视图
-  clearInvalidVideoView(){
-    let normalList=document.getElementsByClassName(this.normalWebRtcVideoClass);
-    let hostList=document.getElementsByClassName(this.hostWebRtcVideoClass);
-    let localList=document.getElementsByClassName(this.localWebRtcVideoClass);
-
-    let divItem=null;
-    if(normalList){
-      for (let i=normalList.length-1;i>=0;i--){
-        divItem=normalList[i];
-        if(divItem&&divItem.children.length<2){
+  clearInvalidVideoView() {
+    let normalList = document.getElementsByClassName(this.normalWebRtcVideoClass);
+    let hostList = document.getElementsByClassName(this.hostWebRtcVideoClass);
+    let localList = document.getElementsByClassName(this.localWebRtcVideoClass);
+
+    let divItem = null;
+    if (normalList) {
+      for (let i = normalList.length - 1; i >= 0; i--) {
+        divItem = normalList[i];
+        if (divItem && divItem.children.length < 2) {
           console.log("删除无效的学生视图");
           divItem.remove();
         }
       }
     }
-    divItem=null;
-    if(hostList){
-      for (let i=hostList.length-1;i>=0;i--){
-        divItem=hostList[i];
-        if(divItem&&divItem.children.length<2){
+    divItem = null;
+    if (hostList) {
+      for (let i = hostList.length - 1; i >= 0; i--) {
+        divItem = hostList[i];
+        if (divItem && divItem.children.length < 2) {
           console.log("删除无效的老师视图");
           divItem.remove();
         }
       }
     }
-    divItem=null;
-    if(localList){
-      for (let i=localList.length-1;i>=0;i--){
-        divItem=localList[i];
-        if(divItem&&divItem.children.length<2){
+    divItem = null;
+    if (localList) {
+      for (let i = localList.length - 1; i >= 0; i--) {
+        divItem = localList[i];
+        if (divItem && divItem.children.length < 2) {
           console.log("删除无效的本地视图");
           divItem.remove();
         }
       }
     }
-    divItem=null;
+    divItem = null;
   }
 
 
@@ -331,8 +341,8 @@ class WebRtcApe extends Emiter {
     //console.log("remoteView->", remoteView)
     if (remoteView) {
       let player = document.getElementById("player_" + nodeId);
-      let len=$("#"+this.xdyRemote + nodeId+" #player_" + nodeId).length;
-      if (len>0) {
+      let len = $("#" + this.xdyRemote + nodeId + " #player_" + nodeId).length;
+      if (len > 0) {
         loger.log(nodeId + " 流已经添加显示,不需要再处理");
         return;
       } else {
@@ -358,29 +368,29 @@ class WebRtcApe extends Emiter {
 
     //记录加入频道成功之后是否立即推流,默认false
     //一般只有在刷新重进频道的时候会用到
-    this.immediatePublish=_params.immediatePublish||false;
+    this.immediatePublish = _params.immediatePublish || false;
 
     //根据不同身份设置不同的分辨率
-    if(GlobalConfig.isTeachOrAssistant){
-      if(GlobalConfig.maxMediaChannels==1){
+    if (GlobalConfig.isTeachOrAssistant) {
+      if (GlobalConfig.maxMediaChannels == 1) {
         //直播时使用标清最高档
-        this.videoResolution="360P_8";
-      }else {
-        this.videoResolution="240P";
+        this.videoResolution = "360P_8";
+      } else {
+        this.videoResolution = "240P";
       }
-    }else if(GlobalConfig.isInvisible){
-      this.videoResolution="120P";
-    }else {
+    } else if (GlobalConfig.isInvisible) {
+      this.videoResolution = "120P";
+    } else {
       //学生在两路视频的时候使用240P 其他多人课堂的时候使用低清的
-      if(GlobalConfig.maxMediaChannels==2||GlobalConfig.maxMediaChannels>3){
-        this.videoResolution="240P"
-      }else {
+      if (GlobalConfig.maxMediaChannels == 2 || GlobalConfig.maxMediaChannels > 3) {
+        this.videoResolution = "240P"
+      } else {
         //180P_4	13	240x180	15	120
-        this.videoResolution="180P_4"
+        this.videoResolution = "180P_4"
       }
     }
 
-    loger.log("开始加入视频通话频道->channelId:" + this.channelId, "uid:" + this.uid,"videoResolution:"+this.videoResolution);
+    loger.log("开始加入视频通话频道->channelId:" + this.channelId, "uid:" + this.uid, "videoResolution:" + this.videoResolution);
     this.client.join(this.channelKey, "" + this.channelId, this.uid, (uid)=> {
       this.uid = uid;
       loger.log("加入视频通话频道->成功->channelId:" + this.channelId, "uid:" + this.uid);
@@ -388,12 +398,12 @@ class WebRtcApe extends Emiter {
       this.openLoaclStream();
 
       //加入频道成功之后需要判断是否立即开启推流
-      if(this.immediatePublish==true){
-        loger.log("加入音视频频道成功->立刻开始推流->"+this.immediatePublish);
+      if (this.immediatePublish == true) {
+        loger.log("加入音视频频道成功->立刻开始推流->" + this.immediatePublish);
         clearTimeout(this.rePublishDelay);
-        this.rePublishDelay=setTimeout(()=>{
+        this.rePublishDelay = setTimeout(()=> {
           this.publish();
-        },200);
+        }, 200);
       }
     }, (err)=> {
       loger.error("加入视频通话频道->失败->", err);
@@ -439,11 +449,11 @@ class WebRtcApe extends Emiter {
   }
 
   leaveChannel() {
-    loger.log("调用离开视频通话频道->isPublish"+this.isPublish);
+    loger.log("调用离开视频通话频道->isPublish" + this.isPublish);
     if (!this.client) {
       return;
     }
-    this.firstPublishSuccess=false;
+    this.firstPublishSuccess = false;
     this.unpublish();
     this.client.leave(() => {
       loger.log("离开视频通话频道->成功");
@@ -469,31 +479,39 @@ class WebRtcApe extends Emiter {
   /*
    * 设置旁录地址
    * */
-  setConfigPublisherUrl(_publishUrl){
-    this.firstPublishSuccess=false;
-    this.setConfigTimestamp=new Date().getTime()/1000;
-    this.configPublisherUrl=_publishUrl;
-    if(this.client&& this.configPublisherUrl){
-      let configObj={width: parseInt(this.pWidth), height: parseInt(this.pHeight), framerate: parseInt(this.pFrameRate), bitrate: parseInt(this.pBitrate), publishUrl:""+this.configPublisherUrl};
+  setConfigPublisherUrl(_publishUrl) {
+    this.firstPublishSuccess = false;
+    this.setConfigTimestamp = new Date().getTime() / 1000;
+    this.configPublisherUrl = _publishUrl;
+    if (this.client && this.configPublisherUrl) {
+      let configObj = {
+        width: parseInt(this.pWidth),
+        height: parseInt(this.pHeight),
+        framerate: parseInt(this.pFrameRate),
+        bitrate: parseInt(this.pBitrate),
+        publishUrl: "" + this.configPublisherUrl
+      };
       //let configObj={width: 480, height:360, framerate:30, bitrate:500, publishUrl:_publishUrl};
       //let configObj={width: 480, height:360, framerate:30, bitrate:500, publishUrl:'rtmp://txlivepush.xuedianyun.com/live/markettest_395312484_T8440_983041_1507888360?bizid=11220&txSecret=15cc50d93f86f9e1a2a76a10db2b09a8&txTime=59e135a8&record=hls|flv&record_interval=5400'};
-      loger.warn("设置旁路地址->",configObj);
+      loger.warn("设置旁路地址->", configObj);
       this.client.configPublisher(configObj);
-    }else {
-      loger.warn("设置旁路地址->失败->为初始化或旁路地址无效",_publishUrl);
+    } else {
+      loger.warn("设置旁路地址->失败->为初始化或旁路地址无效", _publishUrl);
     }
   }
+
   /*
    * 设置旁录拉流地址
    * */
-  setRtmpM3u8Path(_param){
+  setRtmpM3u8Path(_param) {
     //_webRtc.setRtmpM3u8Path({m3u8Url:m3u8Stream,rtmpUrl:rtmpStream});
-    loger.log("设置旁录拉流地址",_param);
-    if(_param){
-      this.m3u8Url=_param.m3u8Url||"";
-      this.rtmpUrl=_param.rtmpUrl||"";
+    loger.log("设置旁录拉流地址", _param);
+    if (_param) {
+      this.m3u8Url = _param.m3u8Url || "";
+      this.rtmpUrl = _param.rtmpUrl || "";
     }
   }
+
   publish(_params) {
     if (!this.client || !this.localStream) {
       loger.warn("推流失败->未加入频道!");
@@ -504,13 +522,13 @@ class WebRtcApe extends Emiter {
       return;
     }
     //老师-助教-主讲人-->设置旁路大于30秒没有推流,推流服务会停止,需要重设旁录和重加频道;
-    if(this.setConfigTimestamp>0&&GlobalConfig.isTeachOrAssistant){
+    if (this.setConfigTimestamp > 0 && GlobalConfig.isTeachOrAssistant) {
       //如果间隔大于28秒并且没有推过流需要重新加入频道,推成功一次之后就不需要
-      let interval=parseInt(new Date().getTime()/1000-this.setConfigTimestamp);
-      loger.log("设置旁路的时间和推流时间的间隔->"+interval+"秒 firstPublishSuccess:"+ this.firstPublishSuccess);
-      if(interval>=28&&!this.firstPublishSuccess){
+      let interval = parseInt(new Date().getTime() / 1000 - this.setConfigTimestamp);
+      loger.log("设置旁路的时间和推流时间的间隔->" + interval + "秒 firstPublishSuccess:" + this.firstPublishSuccess);
+      if (interval >= 28 && !this.firstPublishSuccess) {
         loger.warn("设置旁路大于30秒没有推流,推流服务会停止,需要重设旁录和重加频道");
-        this._emit(WebRtcApe.RE_JOIN_CHANNEL,{publish:true});
+        this._emit(WebRtcApe.RE_JOIN_CHANNEL, {publish: true});
         return;
       }
     }
@@ -520,9 +538,9 @@ class WebRtcApe extends Emiter {
       let viewName = 'localVideoBox_' + this.uid;
       let videoBox = document.createElement("div");
       videoBox.id = viewName;
-      videoBox.className=this.localWebRtcVideoClass;
-      videoBox.style.width = (this.localVideoWidth*this.videoScale) + 'px';
-      videoBox.style.height = (this.localVideoHeight*this.videoScale) + 'px';
+      videoBox.className = this.localWebRtcVideoClass;
+      videoBox.style.width = (this.localVideoWidth * this.videoScale) + 'px';
+      videoBox.style.height = (this.localVideoHeight * this.videoScale) + 'px';
       videoBox.style.float = 'left';
       videoBox.style.marginRight = "1px";
       videoBox.style.pointerEvents = 'none';
@@ -536,8 +554,14 @@ class WebRtcApe extends Emiter {
       if (user) {
         userName = user.name || "";
       }
-      let nameDiv = `<div id="${"videoOwnerName_" + this.uid}"  class="localVideoOwnerName" style="width:${this.localVideoWidth}px;height:20px; position: absolute;bottom: 0; z-index: 1;overflow:hidden;font-size: 14px;text-align: right; color: #cccccc;display:${this.nameDisplay}">${userName}</div>`;
+      let audioMutedDiv = "";
+      if (GlobalConfig.isTeachOrAssistant) {
+        audioMutedDiv = `<div class="audioMuted audioOpen" id=${"audioMuted_"+this.uid}></div>`;
+      }
+      let nameDiv = `<div id="${"videoOwnerName_" + this.uid}"  class="localVideoOwnerName" style="width:${this.localVideoWidth}px;height:22px; position: absolute;bottom: 2px; z-index: 1;overflow:hidden;font-size: 14px;text-align: right; vertical-align: middle;background-color: #2926251a;color: #e7e7e7;display:${this.nameDisplay}">${userName + audioMutedDiv}</div>`;
       $(this.localViewId).prepend(nameDiv);
+      $(".audioMuted").off("click", this._clickAudioMuted.bind(this));
+      $(".audioMuted").on("click", this._clickAudioMuted.bind(this));
 
       loger.log("webRtc->推流->", viewName, new Date().getTime());
       this.localStream.play(viewName);
@@ -553,31 +577,31 @@ class WebRtcApe extends Emiter {
       this.isPublish = false;
       this.clearLocalView();
       this._emit(MessageTypes.WEB_RTC_PUBLISH_FAILED, err);
-      if(err) {
+      if (err) {
         switch (err.msg) {
           case "DEVICES_NOT_FOUND":
-            this.curCameraId="";
-            this.curMicrophoneId="";
+            this.curCameraId = "";
+            this.curMicrophoneId = "";
             this.unpublish();
             break;
           case "NOT_SUPPORTED":
-            this.curCameraId="";
-            this.curMicrophoneId="";
+            this.curCameraId = "";
+            this.curMicrophoneId = "";
             this.unpublish();
             break;
           case "PERMISSION_DENIED":
-            this.curCameraId="";
-            this.curMicrophoneId="";
+            this.curCameraId = "";
+            this.curMicrophoneId = "";
             this.unpublish();
             break;
           case "CONSTRAINT_NOT_SATISFIED":
-            this.curCameraId="";
-            this.curMicrophoneId="";
+            this.curCameraId = "";
+            this.curMicrophoneId = "";
             this.unpublish();
             break;
           case "STREAM_ALREADY_INITIALIZED":
-            this.curCameraId="";
-            this.curMicrophoneId="";
+            this.curCameraId = "";
+            this.curMicrophoneId = "";
             this.unpublish();
             break;
           default:
@@ -617,38 +641,38 @@ class WebRtcApe extends Emiter {
     $('#localVideoBox_' + this.uid).remove();
     $("#videoOwnerName_" + this.uid).remove();
     $(".localVideoOwnerName").remove();
-    $("."+this.localWebRtcVideoClass).remove();
+    $("." + this.localWebRtcVideoClass).remove();
   }
 
   /*
    * 更新所有视频的尺寸大小
    * */
-  updateAllVideoSize(){
-    $("."+this.localWebRtcVideoClass).css("width",this.localVideoWidth*this.videoScale);
-    $("."+this.localWebRtcVideoClass).css("height",this.localVideoHeight*this.videoScale);
+  updateAllVideoSize() {
+    $("." + this.localWebRtcVideoClass).css("width", this.localVideoWidth * this.videoScale);
+    $("." + this.localWebRtcVideoClass).css("height", this.localVideoHeight * this.videoScale);
 
-    $("."+this.hostWebRtcVideoClass).css("width",this.hostRemoteVideoWidth*this.videoScale);
-    $("."+this.hostWebRtcVideoClass).css("height",this.hostRemoteVideoHeight*this.videoScale);
+    $("." + this.hostWebRtcVideoClass).css("width", this.hostRemoteVideoWidth * this.videoScale);
+    $("." + this.hostWebRtcVideoClass).css("height", this.hostRemoteVideoHeight * this.videoScale);
 
-    $("."+this.normalWebRtcVideoClass).css("width",this.normalRemoteVideoWidth*this.videoScale);
-    $("."+this.normalWebRtcVideoClass).css("height",this.normalRemoteVideoHeight*this.videoScale);
+    $("." + this.normalWebRtcVideoClass).css("width", this.normalRemoteVideoWidth * this.videoScale);
+    $("." + this.normalWebRtcVideoClass).css("height", this.normalRemoteVideoHeight * this.videoScale);
 
   }
 
   /*
-  * 设置rtc视频的属性
-  * */
-  changeRtcVideoConfig(_params){
+   * 设置rtc视频的属性
+   * */
+  changeRtcVideoConfig(_params) {
     //{videoScale:1}
-    if(!_params){
+    if (!_params) {
       return;
     }
-    let scale=parseInt(_params.videoScale)||1;//最小值只能为1,这个是按倍数缩放视频
-    if(this.videoScale==scale){
+    let scale = parseInt(_params.videoScale) || 1;//最小值只能为1,这个是按倍数缩放视频
+    if (this.videoScale == scale) {
       return;
     }
-    this.videoScale=scale;
-    loger.log("更新视频视图大小->videoScale:"+this.videoScale);
+    this.videoScale = scale;
+    loger.log("更新视频视图大小->videoScale:" + this.videoScale);
     this.updateAllVideoSize();
   }
 
@@ -663,8 +687,8 @@ class WebRtcApe extends Emiter {
     this.localVideoHeight = parseInt(_params.height) || SIZE_360;
     this.nameDisplay = _params.nameDisplay || "block";
 
-    this.localVideoWidth=this.localVideoWidth;
-    this.localVideoHeight=this.localVideoHeight;
+    this.localVideoWidth = this.localVideoWidth;
+    this.localVideoHeight = this.localVideoHeight;
   }
 
   /*
@@ -676,8 +700,8 @@ class WebRtcApe extends Emiter {
     this.hostRemoteStyle = _params.styleStr || "";
     this.hostRemoteVideoWidth = parseInt(_params.width) || SIZE_480;
     this.hostRemoteVideoHeight = parseInt(_params.height) || SIZE_360;
-    this.hostRemoteVideoWidth=this.hostRemoteVideoWidth;
-    this.hostRemoteVideoHeight=this.hostRemoteVideoHeight;
+    this.hostRemoteVideoWidth = this.hostRemoteVideoWidth;
+    this.hostRemoteVideoHeight = this.hostRemoteVideoHeight;
   }
 
   /*
@@ -690,8 +714,8 @@ class WebRtcApe extends Emiter {
     this.normalRemoteVideoWidth = parseInt(_params.width) || SIZE_480;
     this.normalRemoteVideoHeight = parseInt(_params.height) || SIZE_360;
 
-    this.normalRemoteVideoWidth=this.normalRemoteVideoWidth;
-    this.normalRemoteVideoHeight=this.normalRemoteVideoHeight;
+    this.normalRemoteVideoWidth = this.normalRemoteVideoWidth;
+    this.normalRemoteVideoHeight = this.normalRemoteVideoHeight;
   }
 
   /*
@@ -708,12 +732,12 @@ class WebRtcApe extends Emiter {
   /*
    * 设置旁录推流的参数
    * */
-  setConfigPublisher(_params){
-    loger.log("设置旁录推流的参数",_params);
-    this.pWidth=_params.width||SIZE_480;
-    this.pHeight=_params.height||SIZE_360;
-    this.pFrameRate=_params.frameRate||30;
-    this.pBitrate=_params.bitrate||500;
+  setConfigPublisher(_params) {
+    loger.log("设置旁录推流的参数", _params);
+    this.pWidth = _params.width || SIZE_480;
+    this.pHeight = _params.height || SIZE_360;
+    this.pFrameRate = _params.frameRate || 30;
+    this.pBitrate = _params.bitrate || 500;
   }
 
   /*
@@ -776,7 +800,7 @@ class WebRtcApe extends Emiter {
       for (let i = 0; i < devices.length; i++) {
         let device = devices[i];
         //{"deviceId":"default","kind":"audiooutput","label":"默认","groupId":"cf49a03ca26700235629fc13d3e6630bd34407c66438d157056a34dd3ae03ef5"}
-        if(device){
+        if (device) {
           if (device.kind == 'audioinput') {
             this.microphones.push(device);
             GlobalConfig.microphones.push(device.label);
@@ -796,13 +820,107 @@ class WebRtcApe extends Emiter {
     });
   }
 
+  _clickAudioMuted(evt) {
+    let className = evt.currentTarget.className;
+    console.log("点击禁音",evt.currentTarget.id);
+    let idArr = (evt.currentTarget.id).split("_");
+    let uid = 10000000;//默认设置一个不存在的uid
+    if (idArr && idArr.length > 1) {
+      uid = parseInt(idArr[1]);
+    }
+    if (className.indexOf("audioOpen") > 0) {
+      loger.log("点击禁音->"+uid);
+      // evt.currentTarget.className="audioMuted audioClose";
+      if (uid != GlobalConfig.nodeId) {
+        this.sendChangeUserMediaEnabled({nodeId: uid, video: true, audio: false});
+        $("#audioMuted_" + uid).removeClass();
+        $("#audioMuted_" + uid).addClass("audioMuted audioClose");
+      } else {
+        this.disableAudio(uid);
+      }
+    } else {
+      loger.log("点击开启声音");
+      //evt.currentTarget.className="audioMuted audioOpen";
+      if (uid != GlobalConfig.nodeId) {
+        this.sendChangeUserMediaEnabled({nodeId: uid, video: true, audio: true});
+        $("#audioMuted_" + uid).removeClass();
+        $("#audioMuted_" + uid).addClass("audioMuted audioOpen");
+      } else {
+        this.enableAudio(uid);
+      }
+    }
+  }
+
+  /*
+   * 发送控制音视频禁用消息
+   * */
+  sendChangeUserMediaEnabled(_param) {
+    this._emit(MessageTypes.MEDIA_ENABLED_CHANGE, _param);
+  }
+
+  /*
+   * 收到控制音视频禁用消息
+   * */
+  webRtcMeiaEnabledChange(_data) {
+    loger.log("收到控制音视频禁用消息",_data);
+   // {nodeId: uid, video: true, audio: false}
+    if(!_data){
+      return;
+    }
+    if(_data.nodeId!=GlobalConfig.nodeId){
+      if(_data.audio==false){
+        $("#audioMuted_" + _data.nodeId).removeClass();
+        $("#audioMuted_" + _data.nodeId).addClass("audioMuted audioClose");
+      }else {
+        $("#audioMuted_" + _data.nodeId).removeClass();
+        $("#audioMuted_" + _data.nodeId).addClass("audioMuted audioOpen");
+      }
+
+    }else{
+      if(_data.audio==false){
+        this.disableAudio(_data.nodeId);
+      }else {
+        this.enableAudio(_data.nodeId);
+      }
+    }
+
+  }
+
+  /*
+   * 开启禁音
+   * */
+  disableAudio(uid) {
+    loger.log("开启禁音:" + uid);
+    if (parseInt(uid) == GlobalConfig.nodeId) {
+      if (this.localStream) {
+        $("#audioMuted_" + uid).removeClass();
+        $("#audioMuted_" + uid).addClass("audioMuted audioClose");
+        this.localStream.disableAudio();
+      }
+    }
+  }
+
+  /*
+   * 开启音频
+   * */
+  enableAudio(_uid) {
+    loger.log("开启音频:" + _uid);
+    if (parseInt(_uid) == GlobalConfig.nodeId) {
+      if (this.localStream) {
+        this.localStream.enableAudio();
+        $("#audioMuted_" + _uid).removeClass();
+        $("#audioMuted_" + _uid).addClass("audioMuted audioOpen");
+      }
+    }
+  }
+
   //组织数据,发送给服务器,控制录制和开启录制-推流和停止推流  status:0 停止推流  1:开始推流(同时开启录制),2:停止录制(同时停止推流)
   packMediaInfoData(_status) {
     let curTimestamp = new Date().getTime();
     let data = `appId=${GlobalConfig.appId}&channel=${GlobalConfig.channelId}&channelKey=${GlobalConfig.appCertificate}&uid=${GlobalConfig.userUid}&status=${_status}&userId=${GlobalConfig.userId}&userName=${GlobalConfig.userName}&userRole=${GlobalConfig.userRole}&timestamp=${curTimestamp}&recordTimestamp=${GlobalConfig.recordTimestamp}`;
 
     //markettest_623790840_T9540_1508207080
-    let streamId=GlobalConfig.siteId+"_"+GlobalConfig.classId+"_"+GlobalConfig.userId+"_"+curTimestamp;
+    let streamId = GlobalConfig.siteId + "_" + GlobalConfig.classId + "_" + GlobalConfig.userId + "_" + curTimestamp;
     //mcu记录一份数据
     this._emit(MessageTypes.MEDIA_PUBLISH_STATUS_CHANGE, {
       appId: GlobalConfig.appId,
@@ -815,10 +933,10 @@ class WebRtcApe extends Emiter {
       userRole: GlobalConfig.userRole,
       timestamp: curTimestamp,
       recordTimestamp: GlobalConfig.recordTimestamp,
-      streamId:streamId,
-      publishUrl:this.configPublisherUrl,
-      m3u8Url:this.m3u8Url,
-      rtmpUrl:this.rtmpUrl
+      streamId: streamId,
+      publishUrl: this.configPublisherUrl,
+      m3u8Url: this.m3u8Url,
+      rtmpUrl: this.rtmpUrl
     });
     return data;
   }
@@ -833,7 +951,7 @@ class WebRtcApe extends Emiter {
     }
     let url = GlobalConfig.locationProtocol + GlobalConfig.recordInterfaces;
     let data = this.packMediaInfoData(_status);
-    loger.log("调用服务器端更新视频录制状态->status",_status);
+    loger.log("调用服务器端更新视频录制状态->status", _status);
     fetch(encodeURI(url), {
       method: 'POST',
       headers: {
@@ -863,14 +981,14 @@ class WebRtcApe extends Emiter {
   }
 
   /*
-  * 切换音视频的录制状态
-  * */
+   * 切换音视频的录制状态
+   * */
   changeMediaRecordStatus(_params) {
-    if (!GlobalConfig.recordInterfaces||!_params) {
-      loger.warn("切换音视频的录制状态->失败->接口地址无效",_params);
+    if (!GlobalConfig.recordInterfaces || !_params) {
+      loger.warn("切换音视频的录制状态->失败->接口地址无效", _params);
       return;
     }
-    loger.warn("切换音视频的录制状态->"+_params);
+    loger.warn("切换音视频的录制状态->" + _params);
     let url = GlobalConfig.locationProtocol + GlobalConfig.recordInterfaces;
     let curTimestamp = new Date().getTime();
     let data = `appId=${GlobalConfig.appId}&channel=${GlobalConfig.channelId}&channelKey=${GlobalConfig.appCertificate}&uid=${GlobalConfig.userUid}&status=${_params.status}&userId=${GlobalConfig.userId}&userName=${GlobalConfig.userName}&userRole=${GlobalConfig.userRole}&timestamp=${curTimestamp}&recordTimestamp=${GlobalConfig.recordTimestamp}`;
diff --git a/src/assets/css/mcuStyle.css b/src/assets/css/mcuStyle.css
new file mode 100644
index 0000000..f150bdc
--- /dev/null
+++ b/src/assets/css/mcuStyle.css
@@ -0,0 +1,18 @@
+@charset "utf-8";
+
+/*禁音按钮*/
+.audioMuted{
+    margin-left: 6px;
+    float: right;
+    width: 20px;
+    height: 20px;
+    cursor: pointer;
+    background-size: auto;
+    background-repeat: no-repeat;
+}
+.audioClose {
+    background-image: url(../../assets/img/audioClose.png);
+}
+.audioOpen {
+    background-image: url(../../assets/img/audioOpen.png);
+}
\ No newline at end of file
diff --git a/src/assets/img/audioClose.png b/src/assets/img/audioClose.png
new file mode 100644
index 0000000..35d9a01
Binary files /dev/null and b/src/assets/img/audioClose.png differ
diff --git a/src/assets/img/audioOpen.png b/src/assets/img/audioOpen.png
new file mode 100644
index 0000000..241330f
Binary files /dev/null and b/src/assets/img/audioOpen.png differ
--
libgit2 0.24.0