<template>
<uni-shadow-root class="trtc-room-trtc-room"><view :class="'trtc-room-container '+(isFullscreenDevice?'fullscreen-device-fix':'')">
  <block v-if="template === '1v1'">
    <1v1 v-bind="{pusher, streamList, debug, enableIM}" wx-template-name="1v1"></1v1>
  </block>
  <block v-if="template === 'grid'">
    <grid v-bind="{pusher, streamList, visibleStreamList, debug, enableIM, panelName, gridPagePlaceholderStreamList, gridPageCount, gridCurrentPage, gridPlayerPerPage, isShowMoreMenu, MICVolume, BGMVolume, BGMProgress, beautyStyle, beautyStyleArray, filterIndex, filterArray, audioReverbTypeArray}" wx-template-name="grid"></grid>
  </block>
  <block v-if="template === 'custom'">
    <custom v-bind="{pusher, streamList, debug}" wx-template-name="custom"></custom>
  </block>

  <view class="im-panel" v-if="enableIM && showIMPanel">
    <view class="message-panel-body">
      <scroll-view scroll-y="true" class="message-scroll-container" :scroll-into-view="'message'+(messageList.length-1)" :scroll-with-animation="true">
        <view class="message-list">
          <view v-for="(item,index) in (messageList)" :key="item.index" class="message-item" :id="'message'+(index)">
            <span :class="'user-name '+(item.name == config.userID?'mine':'')">{{item.name}}</span>
            <span class="message-content">{{item.message}}</span>
          </view>
          <view id="message-bottom"></view>
        </view>
      </scroll-view>
        
    </view>
    <view class="message-panel-bottom">
      <view class="message-input-container">
        <input class="message-input" type="text" :value="messageContent" @input="_inputIMMessage" @confirm="_sendIMMessage" confirm-type="send" placeholder="请输入消息" maxlength="200" placeholder-style="color:#ffffff;opacity: 0.55;"></input>
      </view>
      <view class="message-send-btn">
        <button class="btn" @click="_sendIMMessage" hover-class="btn-hover">发送</button>
      </view>
    </view>
    <view @click="_toggleIMPanel" class="close-btn">X</view>
  </view>

  <view :class="'debug-info-btn '+(debugMode && !debugPanel?'':'none')">
    <button class="debug-btn" @click="_debugTogglePanel" hover-class="button-hover">Debug</button>
  </view>
  <view :class="'debug-info '+(debugMode && debugPanel?'':'none')">
    <view @click="_debugTogglePanel" class="close-btn">X</view>
    <view>appVersion: {{appVersion}}</view>
    <view>libVersion: {{libVersion}}</view>
    <view>template: {{template}}</view>
    <view>debug: <button :class="(debug?'':'false')+' debug-btn'" @click="_debugToggleVideoDebug" hover-class="button-hover">{{debug}}</button></view>
    <view>userID: {{pusher.userID}}</view>
    <view>roomID: {{pusher.roomID}}</view>
    <view>camera: <button :class="(pusher.enableCamera?'':'false')+' debug-btn'" @click="_toggleVideo" hover-class="button-hover">{{pusher.enableCamera}}</button></view>
    <view>mic: <button :class="(pusher.enableMic?'':'false')+' debug-btn'" @click="_toggleAudio" hover-class="button-hover">{{pusher.enableMic}}</button></view>
    <view>switch camera: <button class="debug-btn" @click="switchCamera" hover-class="button-hover">{{cameraPosition||pusher.frontCamera}}</button></view>
    <view>Room:
      <button class="debug-btn" @click="_debugEnterRoom" hover-class="button-hover">Enter</button>
      <button class="debug-btn" @click="_debugExitRoom" hover-class="button-hover">Exit</button>
      <button class="debug-btn" @click="_debugGoBack" hover-class="button-hover">Go back</button>
    </view>
    <view>IM: <button class="debug-btn" @click="_debugSendRandomMessage" hover-class="button-hover">send</button></view>
    <view>user count: {{userList.length}}</view>
    <view v-for="(item,index) in (userList)" :key="item.userID">{{item.userID}}|
      mainV:<span :class="'text '+(item.hasMainVideo? 'true' : 'false')">{{item.hasMainVideo||false}}</span>|
      mainA:<span :class="'text '+(item.hasMainAudio? 'true' : 'false')">{{item.hasMainAudio||false}}</span>|
      auxV:<span :class="'text '+(item.hasAuxVideo? 'true' : 'false')">{{item.hasAuxVideo||false}}</span></view>
    <view>stream count: {{streamList.length}}</view>
    <view v-for="(item,index) in (streamList)" :key="item.streamID">{{item.userID}}|{{item.streamType}}|
      SubV:<button :class="(!item.muteVideo?'':'false')+' debug-btn'" @click="_debugToggleRemoteVideo" hover-class="button-hover" :data-user-i-d="item.userID" :data-stream-type="item.streamType">{{!item.muteVideo}}</button>|
      SubA:<button :class="(!item.muteAudio?'':'false')+' debug-btn'" @click="_debugToggleRemoteAudio" hover-class="button-hover" :data-user-i-d="item.userID" :data-stream-type="item.streamType">{{!item.muteAudio}}</button></view>
  </view>
</view></uni-shadow-root>
</template>

<script>

const __wxTemplateComponentProps = {"1v1":["wxTemplateName","pusher","streamList","debug","enableIM"],"grid":["wxTemplateName","pusher","streamList","visibleStreamList","debug","enableIM","panelName","gridPagePlaceholderStreamList","gridPageCount","gridCurrentPage","gridPlayerPerPage","isShowMoreMenu","MICVolume","BGMVolume","BGMProgress","beautyStyle","beautyStyleArray","filterIndex","filterArray","audioReverbTypeArray"],"custom":["wxTemplateName","pusher","streamList","debug"]}
import __wxTemplateComponent0 from './template/1v1/1v1.vue'
import __wxTemplateComponent1 from './template/grid/grid.vue'
import __wxTemplateComponent2 from './template/custom/custom.vue'
__wxTemplateComponentProps['1v1'] && __wxTemplateComponentProps['1v1'].forEach(prop => __wxTemplateComponent0.props[prop] = {type: null})
__wxTemplateComponentProps['grid'] && __wxTemplateComponentProps['grid'].forEach(prop => __wxTemplateComponent1.props[prop] = {type: null})
__wxTemplateComponentProps['custom'] && __wxTemplateComponentProps['custom'].forEach(prop => __wxTemplateComponent2.props[prop] = {type: null})
global['__wxVueOptions'] = {components:{,'1v1' : __wxTemplateComponent0,'grid' : __wxTemplateComponent1,'custom' : __wxTemplateComponent2}}

global['__wxRoute'] = 'trtc-room/trtc-room'
import UserController from './controller/user-controller.js'
import Pusher from './model/pusher.js'
import { EVENT, DEFAULT_COMPONENT_CONFIG } from './common/constants.js'
import Event from './utils/event.js'
import * as ENV from './utils/environment.js'
import TIM from './libs/tim-wx.js'
import MTA from './libs/mta_analysis.js'

const TAG_NAME = 'TRTC-ROOM'
const IM_GROUP_TYPE = TIM.TYPES.GRP_CHATROOM // TIM.TYPES.GRP_CHATROOM 体验版IM无数量限制，成员20个， TIM.TYPES.GRP_AVCHATROOM IM体验版最多10个，升级后无限制

let touchX = 0
let touchY = 0

Component({
  /**
   * 组件的属性列表
   */
  properties: {
    // 必要的初始化参数
    config: {
      type: Object,
      value: {
        sdkAppID: '',
        userID: '',
        userSig: '',
        template: '',
        debugMode: false, // 是否开启调试模式
        enableIM: false, // 是否开启 IM
      },
      observer: function(newVal, oldVal) {
        this._propertyObserver({
          'name': 'config', newVal, oldVal,
        })
      },
    },
  },

  /**
   * 组件的初始数据
   */
  data: {
    pusher: null,
    debugPanel: true, // 是否打开组件调试面板
    debug: false, // 是否打开player pusher 的调试信息
    streamList: [], // 用于渲染player列表,存储stram
    visibleStreamList: [], // 有音频或者视频的StreamList
    userList: [], // 扁平化的数据用来返回给用户
    template: '', // 不能设置默认值，当默认值和传入组件的值不一致时，iOS渲染失败
    cameraPosition: '', // 摄像头位置，用于debug
    panelName: '', // 控制面板名称，包括 setting-panel  memberlist-panel
    localVolume: 0,
    remoteVolumeList: [],
    enableIM: false, // 用于组件内渲染
    showIMPanel: false,
    exitIMThrottle: false,
    messageContent: '',
    messageList: [], // 仅保留10条消息
    maxMessageListLength: 10,
    messageListScrollTop: 0,
    appVersion: ENV.APP_VERSION,
    libVersion: ENV.LIB_VERSION,
    hasGridPageTipsShow: false,
    gridPageCount: 0, // grid 布局 player 分页的总页数
    gridCurrentPage: 1, // grid 布局 当前页码
    gridPlayerPerPage: 4, // grid 布局每页 player的数量, 如果大于3，在逻辑里第一页需要减1。等于3 pusher 在每一页都出现。可选值: 3,4
    gridPagePlaceholderStreamList: [], // 占位数量
    isFullscreenDevice: ENV.IS_FULLSCREEN_DEVICE,
    isShowMoreMenu: false,
    MICVolume: 50,
    BGMVolume: 50,
    BGMProgress: 0,

    beautyStyle: 'smooth',
    beautyStyleArray: [
      { value: 'smooth', label: '光滑', checked: true },
      { value: 'nature', label: '自然', checked: false },
      { value: 'close', label: '关闭', checked: false },
    ],
    filterIndex: 0,
    filterArray: [
      { value: 'standard', label: '标准' },
      { value: 'pink', label: '粉嫩' },
      { value: 'nostalgia', label: '怀旧' },
      { value: 'blues', label: '蓝调' },
      { value: 'romantic', label: '浪漫' },
      { value: 'cool', label: '清凉' },
      { value: 'fresher', label: '清新' },
      { value: 'solor', label: '日系' },
      { value: 'aestheticism', label: '唯美' },
      { value: 'whitening', label: '美白' },
      { value: 'cerisered', label: '樱红' },
    ],
    audioReverbType: 0,
    audioReverbTypeArray: ['关闭', 'KTV', '小房间', '大会堂', '低沉', '洪亮', '金属声', '磁性'],

  },
  /**
   * 生命周期方法
   */
  lifetimes: {
    created: function() {
      // 在组件实例刚刚被创建时执行
      console.log(TAG_NAME, 'created', ENV)
      MTA.App.init({
        appID: '500710685',
        eventID: '500710697',
        autoReport: true,
        statParam: true,
      })
    },
    attached: function() {
      // 在组件实例进入页面节点树时执行
      console.log(TAG_NAME, 'attached')
      this._init()
      MTA.Page.stat()
    },
    ready: function() {
      // 在组件在视图层布局完成后执行
      console.log(TAG_NAME, 'ready')
    },
    detached: function() {
      // 在组件实例被从页面节点树移除时执行
      console.log(TAG_NAME, 'detached')
      // 停止所有拉流，并重置数据
      this.exitRoom()
    },
    error: function(error) {
      // 每当组件方法抛出错误时执行
      console.log(TAG_NAME, 'error', error)
    },
  },
  pageLifetimes: {
    show: function() {
      // 组件所在的页面被展示时执行
      console.log(TAG_NAME, 'show status:', this.status)
      if (this.status.isPending) {
        // 经历了 5000 挂起事件
        this.status.isPending = false
        // 修复iOS 最小化触发5000事件后，音频推流失败的问题
        // if (ENV.IS_IOS && this.data.pusher.enableMic) {
        //   this.unpublishLocalAudio().then(()=>{
        //     this.publishLocalAudio()
        //   })
        // }
        // 经历了 5001 浮窗关闭事件，小程序底层会自动退房，恢复小程序时组件需要重新进房
        // 重新进房
        this.enterRoom({ roomID: this.data.config.roomID }).then(()=>{
          // 进房后开始推送视频或音频
          // setTimeout(()=>{
          //   this.publishLocalVideo()
          //   this.publishLocalAudio()
          // }, 2000)
        })
      } else if (ENV.IS_ANDROID && this.status.pageLife === 'hide' && this.status.isOnHideAddStream && this.data.streamList.length > 0) {
        // 微信没有提供明确的最小化事件，onHide事件，不一定是最小化
        // 获取所有的player 清空 src 重新赋值 验证无效
        // 清空 visibleStreamList 重新赋值， 验证无效
        // 退房重新进房，有效但是成本比较高

        // 将标记了 isOnHideAdd 的 stream 的 palyer 销毁并重新渲染
        const streamList = this.data.streamList
        let tempStreamList = []
        // 过滤 onHide 时新增的 stream
        for (let i = 0; i < streamList.length; i++) {
          if (streamList[i].isOnHideAdd && streamList[i].playerContext) {
            const stream = streamList[i]
            tempStreamList.push(stream)
            stream.playerContext = undefined
            streamList.splice(i, 1)
          }
        }
        // 设置渲染，销毁onHide 时新增的 player
        this._setList({
          streamList: streamList,
        }).then(() => {
          for (let i = 0; i < tempStreamList.length; i++) {
            streamList.push(tempStreamList[i])
          }
          // 设置渲染，重新创建 onHide 时新增的 player
          // setTimeout(()=>{
          this._setList({
            streamList: streamList,
          }).then(() => {
            for (let i = 0; i < tempStreamList.length; i++) {
              tempStreamList[i] = wx.createLivePlayerContext(tempStreamList[i].streamID, this)
            }
            tempStreamList = []
          })
          // }, 500)
        })
        this.status.isOnHideAddStream = false
      }
      this.status.pageLife = 'show'
    },
    hide: function() {
      // 组件所在的页面被隐藏时执行
      console.log(TAG_NAME, 'hide')
      this.status.pageLife = 'hide'
    },
    resize: function(size) {
      // 组件所在的页面尺寸变化时执行
      console.log(TAG_NAME, 'resize', size)
    },
  },
  /**
   * 组件的方法列表
   */
  methods: {
    /**
     * 初始化各项参数和用户控制模块，在组件实例触发 attached 时调用，此时不建议对View进行变更渲染（调用setData方法）
     */
    _init() {
      console.log(TAG_NAME, '_init')
      this.userController = new UserController(this)
      this._emitter = new Event()
      this.EVENT = EVENT
      this._initStatus()
      this._bindEvent()
      this._gridBindEvent()
      this._keepScreenOn()
      console.log(TAG_NAME, '_init success component:', this)
    },
    _initStatus() {
      this.status = {
        isPush: false, // 推流状态
        isPending: false, // 挂起状态，触发5000事件标记为true，onShow后标记为false
        pageLife: '', // 页面生命周期 hide, show
        isOnHideAddStream: false, // onHide后有新增Stream
      }
      this._lastTapTime = 0 // 点击时间戳 用于判断双击事件
      this._beforeLastTapTime = 0 // 点击时间戳 用于判断双击事件
      this._lastTapCoordinate = { x: 0, y: 0 }, // 点击时的坐标
      this._isFullscreen = false // 是否进入全屏状态
    },
    /**
     * 监听组件属性变更，外部变更组件属性时触发该监听
     * @param {Object} data newVal，oldVal
     */
    _propertyObserver(data) {
      console.log(TAG_NAME, '_propertyObserver', data, this.data.config)
      if (data.name === 'config') {
        const config = Object.assign({}, DEFAULT_COMPONENT_CONFIG, data.newVal)
        console.log(TAG_NAME, '_propertyObserver config:', config)
        // 由于 querystring 只支持 String 类型，做一个类型防御
        if (typeof config.debugMode === 'string') {
          config.debugMode = config.debugMode === 'true' ? true : false
        }
        // 初始化IM
        if (config.enableIM && config.sdkAppID) {
          this._initIM(config)
        }
        if (config.sdkAppID && data.oldVal.sdkAppID !== config.sdkAppID && MTA) {
          MTA.Event.stat('sdkAppID', { 'value': config.sdkAppID })
        }
        // 独立设置与pusher无关的配置
        this.setData({
          enableIM: config.enableIM,
          template: config.template,
          debugMode: config.debugMode || false,
          debug: config.debugMode || false,
        })
        this._setPusherConfig(config)
      }
    },

    //  _______             __        __  __
    //  |       \           |  \      |  \|  \
    //  | $$$$$$$\ __    __ | $$____  | $$ \$$  _______
    //  | $$__/ $$|  \  |  \| $$    \ | $$|  \ /       \
    //  | $$    $$| $$  | $$| $$$$$$$\| $$| $$|  $$$$$$$
    //  | $$$$$$$ | $$  | $$| $$  | $$| $$| $$| $$
    //  | $$      | $$__/ $$| $$__/ $$| $$| $$| $$_____
    //  | $$       \$$    $$| $$    $$| $$| $$ \$$     \
    //   \$$        \$$$$$$  \$$$$$$$  \$$ \$$  \$$$$$$$

    /**
     * 进房
     * @param {Object} params 必传 roomID 取值范围 1 ~ 4294967295
     * @returns {Promise}
     */
    enterRoom(params) {
      return new Promise((resolve, reject) => {
        console.log(TAG_NAME, 'enterRoom')
        console.log(TAG_NAME, 'params', params)
        console.log(TAG_NAME, 'config', this.data.config)
        console.log(TAG_NAME, 'pusher', this.data.pusher)
        // 1. 补齐进房参数，校验必要参数是否齐全
        if (params) {
          Object.assign(this.data.pusher, params)
          Object.assign(this.data.config, params)
        }
		console.log(this.data.config,'参数');
        if (!this._checkParam(this.data.config)) {
          reject(new Error('缺少必要参数'))
          return
        }
        // 2. 根据参数拼接 push url，赋值给 live-pusher，
        this._getPushUrl(this.data.config).then((pushUrl)=> {
          this.data.pusher.url = pushUrl
          this.setData({
            pusher: this.data.pusher,
          }, () => {
            // 真正进房成功需要通过 1018 事件通知
            console.log(TAG_NAME, 'enterRoom', this.data.pusher)
            // view 渲染成功回调后，开始推流
            this.data.pusher.getPusherContext().start()
            this.status.isPush = true
            resolve()
          })
        }).catch((res)=> {
          // 进房失败需要通过 pusher state 事件通知，目前还没有准确的事件通知
          console.error(TAG_NAME, 'enterRoom error', res)
          reject(res)
        })
        // 初始化 IM SDK
        // this._initIM(this.data.config)
        // 登录IM
        this._loginIM({ ...this.data.config, roomID: params.roomID })
      })
    },
    /**
     * 退房，停止推流和拉流，并重置数据
     * @returns {Promise}
     */
    exitRoom() {
      if (this.status.pageLife === 'hide') {
        // 如果是退后台触发 onHide，不能调用 pusher API
        console.warn(TAG_NAME, '小程序最小化时不能调用 exitRoom，如果不想听到远端声音，可以调用取消订阅，如果不想远端听到声音，可以调用取消发布')
      }
      return new Promise((resolve, reject) => {
        console.log(TAG_NAME, 'exitRoom')
        this._exitIM()
        this.data.pusher.reset()
        this.status.isPush = false
        const result = this.userController.reset()
        this.setData({
          pusher: this.data.pusher,
          userList: result.userList,
          streamList: result.streamList,
          visibleStreamList: this._filterVisibleStream(result.streamList),
        }, () => {
          // 在销毁页面时调用exitRoom时，不会走到这里
          resolve({ userList: this.data.userList, streamList: this.data.streamList })
          console.log(TAG_NAME, 'exitRoom success', this.data.pusher, this.data.streamList, this.data.userList)
          // 20200421 iOS 仍然没有1019事件通知退房，退房事件移动到 exitRoom 方法里，但不是后端通知的退房成功
          this._emitter.emit(EVENT.LOCAL_LEAVE, { userID: this.data.pusher.userID })
        })
      })
    },
    /**
     * 开启摄像头
     * @returns {Promise}
     */
    publishLocalVideo() {
      // 设置 pusher enableCamera
      console.log(TAG_NAME, 'publishLocalVideo 开启摄像头')
      return this._setPusherConfig({ enableCamera: true })
    },
    /**
     * 关闭摄像头
     * @returns {Promise}
     */
    unpublishLocalVideo() {
      // 设置 pusher enableCamera
      console.log(TAG_NAME, 'unpublshLocalVideo 关闭摄像头')
      return this._setPusherConfig({ enableCamera: false })
    },
    /**
     * 开启麦克风
     * @returns {Promise}
     */
    publishLocalAudio() {
      // 设置 pusher enableCamera
      console.log(TAG_NAME, 'publishLocalAudio 开启麦克风')
      return this._setPusherConfig({ enableMic: true })
    },
    /**
     * 关闭麦克风
     * @returns {Promise}
     */
    unpublishLocalAudio() {
      // 设置 pusher enableCamera
      console.log(TAG_NAME, 'unpublshLocalAudio 关闭麦克风')
      return this._setPusherConfig({ enableMic: false })
    },
    /**
     * 订阅远端视频 主流 小画面 辅流
     * @param {Object} params {userID,streamType} streamType 传入 small 时修改对应的主流 url 的 _definitionType 参数为 small, stream.streamType 仍为 main
     * @returns {Promise}
     */
    subscribeRemoteVideo(params) {
      console.log(TAG_NAME, 'subscribeRemoteVideo', params)
      // 设置指定 user streamType 的 muteVideo 为 false
      const config = {
        muteVideo: false,
      }
      // 本地数据结构里的 streamType 只支持 main 和 aux ，订阅 small 也是对 main 进行处理
      const streamType = params.streamType === 'small' ? 'main' : params.streamType
      const stream = this.userController.getStream({
        userID: params.userID,
        streamType: streamType,
      })
      stream.muteVideoPrev = false // 用于分页切换时保留player当前的订阅状态

      if (params.streamType === 'small' || params.streamType === 'main') {
        if (stream && stream.streamType === 'main') {
          console.log(TAG_NAME, 'subscribeRemoteVideo switch small', stream.src)
          if (params.streamType === 'small') {
            config.src = stream.src.replace('main', 'small')
            config._definitionType = 'small' // 用于设置面板的渲染
          } else if (params.streamType === 'main') {
            stream.src = stream.src.replace('small', 'main')
            config._definitionType = 'main'
          }
          console.log(TAG_NAME, 'subscribeRemoteVideo', stream.src)
        }
      }
      return this._setPlayerConfig({
        userID: params.userID,
        streamType: streamType,
        config: config,
      })
    },
    /**
     * 取消订阅远端视频
     * @param {Object} params {userID,streamType}
     * @returns {Promise}
     */
    unsubscribeRemoteVideo(params) {
      console.log(TAG_NAME, 'unsubscribeRemoteVideo', params)
      const stream = this.userController.getStream({
        userID: params.userID,
        streamType: params.streamType,
      })
      stream.muteVideoPrev = true // 用于分页切换时保留player当前的订阅状态
      // 设置指定 user streamType 的 muteVideo 为 true
      return this._setPlayerConfig({
        userID: params.userID,
        streamType: params.streamType,
        config: {
          muteVideo: true,
        },
      })
    },
    /**
     * 订阅远端音频
     * @param {Object} params userID 用户ID
     * @returns {Promise}
     */
    subscribeRemoteAudio(params) {
      console.log(TAG_NAME, 'subscribeRemoteAudio', params)
      return this._setPlayerConfig({
        userID: params.userID,
        streamType: 'main',
        config: {
          muteAudio: false,
        },
      })
    },
    /**
     * 取消订阅远端音频
     * @param {Object} params userID 用户ID
     * @returns {Promise}
     */
    unsubscribeRemoteAudio(params) {
      console.log(TAG_NAME, 'unsubscribeRemoteAudio', params)
      return this._setPlayerConfig({
        userID: params.userID,
        streamType: 'main',
        config: {
          muteAudio: true,
        },
      })
    },
    on(eventCode, handler, context) {
      this._emitter.on(eventCode, handler, context)
    },
    off(eventCode, handler) {
      this._emitter.off(eventCode, handler)
    },
    getRemoteUserList() {
      return this.data.userList
    },
    /**
     * 切换前后摄像头
     */
    switchCamera() {
      if (!this.data.cameraPosition) {
        // this.data.pusher.cameraPosition 是初始值，不支持动态设置
        this.data.cameraPosition = this.data.pusher.frontCamera
      }
      console.log(TAG_NAME, 'switchCamera', this.data.cameraPosition)
      this.data.cameraPosition = this.data.cameraPosition === 'front' ? 'back' : 'front'
      this.setData({
        cameraPosition: this.data.cameraPosition,
      }, () => {
        console.log(TAG_NAME, 'switchCamera success', this.data.cameraPosition)
      })
      // wx 7.0.9 不支持动态设置 pusher.frontCamera ，只支持调用 API switchCamer() 设置，这里修改 cameraPosition 是为了记录状态
      this.data.pusher.getPusherContext().switchCamera()
    },
    /**
     * 设置指定player view的渲染坐标和尺寸
     * @param {object} params
     * userID: string
     * streamType: string
     * xAxis: number
     * yAxis: number
     * width: number
     * height: number
     * @returns {Promise}
     */
    setViewRect(params) {
      console.log(TAG_NAME, 'setViewRect', params)
      if (this.data.template !== 'custom') {
        console.warn(`如需使用setViewRect方法，请初始化时设置template:"custom", 当前 template:"${this.data.template}"`)
      }
      console.info(`不建议使用该方法动态修改样式，避免引起微信小程序渲染问题，建议直接修改 wxml wxss 进行样式定制化`)
      if (this.data.pusher.userID === params.userID) {
        return this._setPusherConfig({
          xAxis: params.xAxis,
          yAxis: params.yAxis,
          width: params.width,
          height: params.height,
        })
      }
      return this._setPlayerConfig({
        userID: params.userID,
        streamType: params.streamType,
        config: {
          xAxis: params.xAxis,
          yAxis: params.yAxis,
          width: params.width,
          height: params.height,
        },
      })
    },
    /**
     * 设置指定 player 或者 pusher view 是否可见
     * @param {object} params
     * userID: string
     * streamType: string
     * isVisible：boolean
     * @returns {Promise}
     */
    setViewVisible(params) {
      console.log(TAG_NAME, 'setViewVisible', params)
      if (this.data.template !== 'custom') {
        console.warn(`如需使用setViewVisible方法，请初始化时设置template:"custom", 当前 template:"${this.data.template}"`)
      }
      console.info(`不建议使用该方法动态修改样式，避免引起微信小程序渲染问题，建议直接修改 wxml wxss 进行样式定制化`)
      if (this.data.pusher.userID === params.userID) {
        return this._setPusherConfig({
          isVisible: params.isVisible,
        })
      }
      return this._setPlayerConfig({
        userID: params.userID,
        streamType: params.streamType,
        config: {
          isVisible: params.isVisible,
        },
      })
    },
    /**
     * 设置指定player view的层级
     * @param {Object} params
     * userID: string
     * streamType: string
     * zIndex: number
     * @returns {Promise}
     */
    setViewZIndex(params) {
      console.log(TAG_NAME, 'setViewZIndex', params)
      if (this.data.template !== 'custom') {
        console.warn(`如需使用setViewZIndex方法，请初始化时设置template:"custom", 当前 template:"${this.data.template}"`)
      }
      console.info(`不建议使用该方法动态修改样式，避免引起微信小程序渲染问题，建议直接修改 wxml wxss 进行样式定制化`)
      if (this.data.pusher.userID === params.userID) {
        return this._setPusherConfig({
          zIndex: params.zindex || params.zIndex,
        })
      }
      return this._setPlayerConfig({
        userID: params.userID,
        streamType: params.streamType,
        config: {
          zIndex: params.zindex || params.zIndex,
        },
      })
    },
    /**
     * 播放背景音
     * @param {Object} params url
     * @returns {Promise}
     */
    playBGM(params) {
      return new Promise((resolve, reject) => {
        this.data.pusher.getPusherContext().playBGM({
          url: params.url,
          // 已经有相关事件不需要在这里监听,目前用于测试
          success: () => {
            console.log(TAG_NAME, '播放背景音成功')
            // this._emitter.emit(EVENT.BGM_PLAY_START)
            resolve()
          },
          fail: () => {
            console.log(TAG_NAME, '播放背景音失败')
            this._emitter.emit(EVENT.BGM_PLAY_FAIL)
            reject(new Error('播放背景音失败'))
          },
          // complete: () => {
          //   console.log(TAG_NAME, '背景完成')
          //   this._emitter.emit(EVENT.BGM_PLAY_COMPLETE)
          // },
        })
      })
    },
    stopBGM() {
      this.data.pusher.getPusherContext().stopBGM()
    },
    pauseBGM() {
      this.data.pusher.getPusherContext().pauseBGM()
    },
    resumeBGM() {
      this.data.pusher.getPusherContext().resumeBGM()
    },
    /**
     * 设置背景音音量
     * @param {Object} params volume
     */
    setBGMVolume(params) {
      console.log(TAG_NAME, 'setBGMVolume', params)
      this.data.pusher.getPusherContext().setBGMVolume({ volume: params.volume })
    },
    /**
     * 设置麦克风音量
     * @param {Object} params volume
     */
    setMICVolume(params) {
      console.log(TAG_NAME, 'setMICVolume', params)
      this.data.pusher.getPusherContext().setMICVolume({ volume: params.volume })
    },
    /**
     * 发送SEI消息
     * @param {Object} params message
     * @returns {Promise}
     */
    sendSEI(params) {
      return new Promise((resolve, reject) => {
        this.data.pusher.getPusherContext().sendMessage({
          msg: params.message,
          success: function(result) {
            resolve(result)
          },
        })
      })
    },
    /**
     * pusher 和 player 的截图并保存
     * @param {Object} params userID streamType
     * @returns {Promise}
     */
    snapshot(params) {
      console.log(TAG_NAME, 'snapshot', params)
      return new Promise((resolve, reject) => {
        this.captureSnapshot(params).then((result)=>{
          wx.saveImageToPhotosAlbum({
            filePath: result.tempImagePath,
            success(res) {
              wx.showToast({
                title: '已保存到相册',
              })
              console.log('save photo is success', res)
              resolve(result)
            },
            fail: function(error) {
              wx.showToast({
                icon: 'none',
                title: '保存失败',
              })
              console.log('save photo is fail', error)
              reject(error)
            },
          })
        }).catch((error)=>{
          reject(error)
        })
      })
    },
    /**
     * 获取pusher 和 player 的截图
     * @param {Object} params userID streamType
     * @returns {Promise}
     */
    captureSnapshot(params) {
      return new Promise((resolve, reject) => {
        if (params.userID === this.data.pusher.userID) {
        // pusher
          this.data.pusher.getPusherContext().snapshot({
            quality: 'raw',
            complete: (result) => {
              console.log(TAG_NAME, 'snapshot pusher', result)
              if (result.tempImagePath) {
                resolve(result)
              } else {
                console.log('snapShot 回调失败', result)
                reject(new Error('截图失败'))
              }
            },
          })
        } else {
        // player
          this.userController.getStream(params).playerContext.snapshot({
            quality: 'raw',
            complete: (result) => {
              console.log(TAG_NAME, 'snapshot player', result)
              if (result.tempImagePath) {
                resolve(result)
              } else {
                console.log('snapShot 回调失败', result)
                reject(new Error('截图失败'))
              }
            },
          })
        }
      })
    },
    /**
     * 将远端视频全屏
     * @param {Object} params userID streamType direction
     * @returns {Promise}
     */
    enterFullscreen(params) {
      console.log(TAG_NAME, 'enterFullscreen', params)
      return new Promise((resolve, reject) => {
        this.userController.getStream(params).playerContext.requestFullScreen({
          direction: params.direction || 0,
          success: (event) => {
            console.log(TAG_NAME, 'enterFullscreen success', event)
            resolve(event)
          },
          fail: (event) => {
            console.log(TAG_NAME, 'enterFullscreen fail', event)
            reject(event)
          },
        })
      })
    },
    /**
     * 将远端视频取消全屏
     * @param {Object} params userID streamType
     * @returns {Promise}
     */
    exitFullscreen(params) {
      console.log(TAG_NAME, 'exitFullscreen', params)
      return new Promise((resolve, reject) => {
        this.userController.getStream(params).playerContext.exitFullScreen({
          success: (event) => {
            console.log(TAG_NAME, 'exitFullScreen success', event)
            resolve(event)
          },
          fail: (event) => {
            console.log(TAG_NAME, 'exitFullScreen fail', event)
            reject(event)
          },
        })
      })
    },
    /**
     * 设置 player 视图的横竖屏显示
     * @param {Object} params userID streamType orientation: vertical, horizontal
     * @returns {Promise}
     */
    setRemoteOrientation(params) {
      return this._setPlayerConfig({
        userID: params.userID,
        streamType: params.streamType,
        config: {
          orientation: params.orientation,
        },
      })
    },
    // 改为：
    setViewOrientation(params) {
      return this._setPlayerConfig({
        userID: params.userID,
        streamType: params.streamType,
        config: {
          orientation: params.orientation,
        },
      })
    },
    /**
     * 设置 player 视图的填充模式
     * @param {Object} params userID streamType fillMode: contain，fillCrop
     * @returns {Promise}
     */
    setRemoteFillMode(params) {
      return this._setPlayerConfig({
        userID: params.userID,
        streamType: params.streamType,
        config: {
          objectFit: params.fillMode,
        },
      })
    },
    // 改为：
    setViewFillMode(params) {
      return this._setPlayerConfig({
        userID: params.userID,
        streamType: params.streamType,
        config: {
          objectFit: params.fillMode,
        },
      })
    },
    /**
     * 发送C2C文本消息
     * @param {*} params userID,message
     * @returns {Promise}
     */
    sendC2CTextMessage(params) {
      if (!this.tim) {
        console.warn(TAG_NAME, '未开启IM功能，该方法无法使用', params)
        return
      }
      console.log(TAG_NAME, 'sendC2CTextMessage', params)
      const message = this.tim.createTextMessage({
        to: params.userID + '',
        conversationType: TIM.TYPES.CONV_C2C,
        payload: {
          text: params.message,
        },
      })
      const promise = this.tim.sendMessage(message)
      promise.then(function(imResponse) {
        // 发送成功
        console.log(TAG_NAME, 'sendC2CTextMessage success', imResponse)
      }).catch(function(imError) {
        // 发送失败
        console.warn(TAG_NAME, 'sendC2CTextMessage error:', imError)
      })
      return promise
    },
    /**
     * 发送C2C自定义消息
     * @param {*} params: userID payload
     * @returns {Promise}
     *
     */
    sendC2CCustomMessage(params) {
      if (!this.tim) {
        console.warn(TAG_NAME, '未开启IM功能，该方法无法使用', params)
        return
      }
      console.log(TAG_NAME, 'sendC2CCustomMessage', params)
      const message = this.tim.createCustomMessage({
        to: params.userID + '',
        conversationType: TIM.TYPES.CONV_C2C,
        payload: params.payload,
      })
      const promise = this.tim.sendMessage(message)
      promise.then(function(imResponse) {
        // 发送成功
        console.log(TAG_NAME, 'sendMessage success', imResponse)
      }).catch(function(imError) {
        // 发送失败
        console.warn(TAG_NAME, 'sendMessage error:', imError)
      })
      return promise
    },
    /**
     * 发送群组文本消息
     * @param {*} params roomID message
     * @returns {Promise}
     *
     */
    sendGroupTextMessage(params) {
      if (!this.tim) {
        console.warn(TAG_NAME, '未开启IM功能，该方法无法使用', params)
        return
      }
      console.log(TAG_NAME, 'sendGroupTextMessage', params)
      const message = this.tim.createTextMessage({
        to: params.roomID + '',
        conversationType: TIM.TYPES.CONV_GROUP,
        payload: {
          text: params.message,
        },
      })
      const promise = this.tim.sendMessage(message)
      promise.then(function(imResponse) {
        // 发送成功
        console.log(TAG_NAME, 'sendGroupTextMessage success', imResponse)
      }).catch(function(imError) {
        // 发送失败
        console.warn(TAG_NAME, 'sendGroupTextMessage error:', imError)
      })
      return promise
    },
    /**
     * 发送群组自定义消息
     * @param {*} params roomID payload
     * @returns {Promise}
     *
     */
    sendGroupCustomMessage(params) {
      if (!this.tim) {
        console.warn(TAG_NAME, '未开启IM功能，该方法无法使用', params)
        return
      }
      console.log(TAG_NAME, 'sendGroupCustomMessage', params)
      const message = this.tim.createCustomMessage({
        to: params.roomID + '',
        conversationType: TIM.TYPES.CONV_GROUP,
        payload: params.payload,
      })
      const promise = this.tim.sendMessage(message)
      promise.then(function(imResponse) {
        // 发送成功
        console.log(TAG_NAME, 'sendMessage success', imResponse)
      }).catch(function(imError) {
        // 发送失败
        console.warn(TAG_NAME, 'sendMessage error:', imError)
      })
      return promise
    },

    // ______             __                                              __
    // |      \           |  \                                            |  \
    //  \$$$$$$ _______  _| $$_     ______    ______   _______    ______  | $$
    //   | $$  |       \|   $$ \   /      \  /      \ |       \  |      \ | $$
    //   | $$  | $$$$$$$\\$$$$$$  |  $$$$$$\|  $$$$$$\| $$$$$$$\  \$$$$$$\| $$
    //   | $$  | $$  | $$ | $$ __ | $$    $$| $$   \$$| $$  | $$ /      $$| $$
    //  _| $$_ | $$  | $$ | $$|  \| $$$$$$$$| $$      | $$  | $$|  $$$$$$$| $$
    // |   $$ \| $$  | $$  \$$  $$ \$$     \| $$      | $$  | $$ \$$    $$| $$
    //  \$$$$$$ \$$   \$$   \$$$$   \$$$$$$$ \$$       \$$   \$$  \$$$$$$$ \$$
    /**
     * 设置推流参数并触发页面渲染更新
     * @param {Object} config live-pusher 的配置
     * @returns {Promise}
     */
    _setPusherConfig(config, skipLog = false) {
      if (!skipLog) {
        console.log(TAG_NAME, '_setPusherConfig', config, this.data.pusher)
      }
      return new Promise((resolve, reject) => {
        if (!this.data.pusher) {
          this.data.pusher = new Pusher(config)
        } else {
          Object.assign(this.data.pusher, config)
        }
        this.setData({
          pusher: this.data.pusher,
        }, () => {
          if (!skipLog) {
            console.log(TAG_NAME, '_setPusherConfig setData compelete', 'config:', config, 'pusher:', this.data.pusher)
          }
          resolve(config)
        })
      })
    },
    /**
     * 设置指定 player 属性并触发页面渲染
     * @param {Object} params include userID,streamType,config
     * @returns {Promise}
     */
    _setPlayerConfig(params) {
      const userID = params.userID
      const streamType = params.streamType
      const config = params.config
      console.log(TAG_NAME, '_setPlayerConfig', params)
      return new Promise((resolve, reject) => {
        // 获取指定的userID streamType 的 stream
        const user = this.userController.getUser(userID)
        if (user && user.streams[streamType]) {
          Object.assign(user.streams[streamType], config)
          // user.streams引用的对象和 streamList 里的是同一个
          this.setData({
            streamList: this.data.streamList,
            visibleStreamList: this._filterVisibleStream(this.data.streamList, true),
          }, () => {
            // console.log(TAG_NAME, '_setPlayerConfig complete', params, 'streamList:', this.data.streamList)
            resolve(params)
          })
        } else {
          // 不需要reject，静默处理
          console.warn(TAG_NAME, '指定 userID 或者 streamType 不存在')
          // reject(new Error('指定 userID 或者 streamType 不存在'))
        }
      })
    },
    /**
     * 设置列表数据，并触发页面渲染
     * @param {Object} params include userList, stramList
     * @returns {Promise}
     */
    _setList(params) {
      console.log(TAG_NAME, '_setList', params, this.data.template)
      const { userList, streamList } = params
      return new Promise((resolve, reject) => {
        let visibleStreamList = []
        const data = {
          userList: userList || this.data.userList,
          streamList: streamList || this.data.streamList,
        }
        if (this.data.template === 'grid') {
          visibleStreamList = this._filterVisibleStream(streamList)
          data.visibleStreamList = visibleStreamList || this.data.visibleStreamList
          data.gridPagePlaceholderStreamList = this.data.gridPagePlaceholderStreamList

          data.gridCurrentPage = this.data.gridCurrentPage
          data.gridPageCount = this.data.gridPageCount
        }
        this.setData(data, () => {
          resolve(params)
        })
      })
    },
    /**
     * 必选参数检测
     * @param {Object} rtcConfig rtc参数
     * @returns {Boolean}
     */
    _checkParam(rtcConfig) {
      console.log(TAG_NAME, 'checkParam config:', rtcConfig)
      if (!rtcConfig.sdkAppID) {
        console.error('未设置 sdkAppID')
        return false
      }
      if (rtcConfig.roomID === undefined) {
        console.error('未设置 roomID')
        return false
      }
      if (rtcConfig.roomID < 1 || rtcConfig.roomID > 4294967296) {
        console.error('roomID 超出取值范围 1 ~ 4294967295')
        return false
      }
      if (!rtcConfig.userID) {
        console.error('未设置 userID')
        return false
      }
      if (!rtcConfig.userSig) {
        console.error('未设置 userSig')
        return false
      }
      if (!rtcConfig.template) {
        console.error('未设置 template')
        return false
      }
      return true
    },
    _getPushUrl(rtcConfig) {
      // 拼接 puhser url rtmp 方案
      console.log(TAG_NAME, '_getPushUrl', rtcConfig)
      if (ENV.IS_TRTC) {
        // 版本高于7.0.8，基础库版本高于2.10.0 使用新的 url
        return new Promise((resolve, reject) => {
          // appscene videocall live
          // cloudenv PRO CCC DEV UAT
          // encsmall 0
          // 对外的默认值是rtc ，对内的默认值是videocall
          rtcConfig.scene = !rtcConfig.scene || rtcConfig.scene === 'rtc' ? 'videocall' : rtcConfig.scene
          rtcConfig.enableBlackStream = rtcConfig.enableBlackStream || '' // 是否支持在纯音频下推送SEI消息，注意：在关闭enable-recv-message后还是无法接收
          rtcConfig.encsmall = rtcConfig.encsmall || 0 // 是否编小画面，这个特性不建议学生默认开启，只有老师端才比较有意义
          rtcConfig.cloudenv = rtcConfig.cloudenv || 'PRO'
          rtcConfig.streamID = rtcConfig.streamID || '' // 指定旁边路直播的流ID
          rtcConfig.userDefineRecordID = rtcConfig.userDefineRecordID || '' // 指定录制文件的recordid
          rtcConfig.privateMapKey = rtcConfig.privateMapKey || '' // 字符串房间号
          rtcConfig.pureAudioMode = rtcConfig.pureAudioMode || ''// 指定是否纯音频推流及录制，默认不填，值为1 或 2，其他值非法不处理
          rtcConfig.recvMode = rtcConfig.recvMode || 1 // 1. 自动接收音视频 2. 仅自动接收音频 3. 仅自动接收视频 4. 音视频都不自动接收, 不能绑定player
          let roomID = ''
          if (/^\d+$/.test(rtcConfig.roomID)) {
            // 数字房间号
            roomID = '&roomid=' + rtcConfig.roomID
          } else {
            // 字符串房间号
            roomID = '&strroomid=' + rtcConfig.roomID
          }
          setTimeout(()=> {
            const pushUrl = 'room://cloud.tencent.com/rtc?sdkappid=' + rtcConfig.sdkAppID +
                            roomID +
                            '&userid=' + rtcConfig.userID +
                            '&usersig=' + rtcConfig.userSig +
                            '&appscene=' + rtcConfig.scene +
                            '&encsmall=' + rtcConfig.encsmall +
                            '&cloudenv=' + rtcConfig.cloudenv +
                            '&enableBlackStream=' + rtcConfig.enableBlackStream +
                            '&streamid=' + rtcConfig.streamID +
                            '&userdefinerecordid=' + rtcConfig.userDefineRecordID +
                            '&privatemapkey=' + rtcConfig.privateMapKey +
                            '&pureaudiomode=' + rtcConfig.pureAudioMode +
                            '&recvmode=' + rtcConfig.recvMode
            console.warn(TAG_NAME, 'getPushUrl result:', pushUrl)
            resolve(pushUrl)
          }, 0)
        })
      }
      console.error(TAG_NAME, '组件仅支持微信 App iOS >=7.0.9, Android >= 7.0.8, 小程序基础库版 >= 2.10.0')
      console.error(TAG_NAME, '需要真机运行，开发工具不支持实时音视频')
    },
    /**
     * 获取签名和推流地址
     * @param {Object} rtcConfig 进房参数配置
     * @returns {Promise}
     */
    _requestSigServer(rtcConfig) {
      console.log(TAG_NAME, '_requestSigServer:', rtcConfig)
      const sdkAppID = rtcConfig.sdkAppID
      const userID = rtcConfig.userID
      const userSig = rtcConfig.userSig
      const roomID = rtcConfig.roomID
      const privateMapKey = rtcConfig.privateMapKey

      rtcConfig.useCloud = rtcConfig.useCloud === undefined ? true : rtcConfig.useCloud
      let url = rtcConfig.useCloud ? 'https://official.opensso.tencent-cloud.com/v4/openim/jsonvideoapp' : 'https://yun.tim.qq.com/v4/openim/jsonvideoapp'
      url += '?sdkappid=' + sdkAppID + '&identifier=' + userID + '&usersig=' + userSig + '&random=' + Date.now() + '&contenttype=json'

      const reqHead = {
        'Cmd': 1,
        'SeqNo': 1,
        'BusType': 7,
        'GroupId': roomID,
      }
      const reqBody = {
        'PrivMapEncrypt': privateMapKey,
        'TerminalType': 1,
        'FromType': 3,
        'SdkVersion': 26280566,
      }
      console.log(TAG_NAME, '_requestSigServer:', url, reqHead, reqBody)
      return new Promise((resolve, reject) => {
        wx.request({
          url: url,
          data: {
            'ReqHead': reqHead,
            'ReqBody': reqBody,
          },
          method: 'POST',
          success: (res) => {
            console.log('_requestSigServer success:', res)
            if (res.data['ErrorCode'] || res.data['RspHead']['ErrorCode'] !== 0) {
              // console.error(res.data['ErrorInfo'] || res.data['RspHead']['ErrorInfo'])
              console.error('获取roomsig失败')
              reject(res)
            }

            const roomSig = JSON.stringify(res.data['RspBody'])
            let pushUrl = 'room://cloud.tencent.com?sdkappid=' + sdkAppID + '&roomid=' + roomID + '&userid=' + userID + '&roomsig=' + encodeURIComponent(roomSig)
            // TODO 需要重新整理的逻辑 TRTC尚未支持 20200213
            // 如果有配置纯音频推流或者recordId参数
            if (rtcConfig.pureAudioPushMod || rtcConfig.recordId) {
              const bizbuf = {
                Str_uc_params: {
                  pure_audio_push_mod: 0,
                  record_id: 0,
                },
              }
              // 纯音频推流
              if (rtcConfig.pureAudioPushMod) {
                bizbuf.Str_uc_params.pure_audio_push_mod = rtcConfig.pureAudioPushMod
              } else {
                delete bizbuf.Str_uc_params.pure_audio_push_mod
              }
              // 自动录制时业务自定义id
              if (rtcConfig.recordId) {
                bizbuf.Str_uc_params.record_id = rtcConfig.recordId
              } else {
                delete bizbuf.Str_uc_params.record_id
              }
              pushUrl += '&bizbuf=' + encodeURIComponent(JSON.stringify(bizbuf))
            }
            console.log('roomSigInfo', pushUrl)
            resolve(pushUrl)
          },
          fail: (res) => {
            console.log(TAG_NAME, 'requestSigServer fail:', res)
            reject(res)
          },
        })
      })
    },
    _doubleTabToggleFullscreen(event) {
      const curTime = event.timeStamp
      const lastTime = this._lastTapTime
      const lastTapCoordinate = this._lastTapCoordinate
      const currentTapCoordinate = event.detail
      // 计算两次点击的距离
      const distence = Math.sqrt(Math.pow(Math.abs(currentTapCoordinate.x - lastTapCoordinate.x), 2) + Math.pow(Math.abs(currentTapCoordinate.y - lastTapCoordinate.y), 2))
      this._lastTapCoordinate = currentTapCoordinate
      // 已知问题：上次全屏操作后，必须等待1.5s后才能再次进行全屏操作，否则引发SDK全屏异常，因此增加节流逻辑
      const beforeLastTime = this._beforeLastTapTime
      console.log(TAG_NAME, '_doubleTabToggleFullscreen', event, lastTime, beforeLastTime, distence)

      if (curTime - lastTime > 0 && curTime - lastTime < 300 && lastTime - beforeLastTime > 1500 && distence < 20) {
        const userID = event.currentTarget.dataset.userid
        const streamType = event.currentTarget.dataset.streamtype
        if (this._isFullscreen) {
          this.exitFullscreen({ userID, streamType }).then(() => {
            this._isFullscreen = false
          }).catch(() => {
          })
        } else {
          // const stream = this.userController.getStream({ userID, streamType })
          let direction
          // // 已知问题：视频的尺寸需要等待player触发NetStatus事件才能获取到，如果进房就双击全屏，全屏后的方向有可能不对。
          // if (stream && stream.videoWidth && stream.videoHeight) {
          //   // 如果是横视频，全屏时进行横屏处理。如果是竖视频，则为0
          //   direction = stream.videoWidth > stream.videoHeight ? 90 : 0
          // }
          this.enterFullscreen({ userID, streamType, direction }).then(() => {
            this._isFullscreen = true
          }).catch(() => {
          })
        }
        this._beforeLastTapTime = lastTime
      }
      this._lastTapTime = curTime
    },
    /**
     * TRTC-room 远端用户和音视频状态处理
     */
    _bindEvent() {
      // 远端用户进房
      this.userController.on(EVENT.REMOTE_USER_JOIN, (event)=>{
        console.log(TAG_NAME, '远端用户进房', event, event.data.userID)
        this.setData({
          userList: event.data.userList,
        }, () => {
          this._emitter.emit(EVENT.REMOTE_USER_JOIN, { userID: event.data.userID })
        })
        console.log(TAG_NAME, 'REMOTE_USER_JOIN', 'streamList:', this.data.streamList, 'userList:', this.data.userList)
      })
      // 远端用户离开
      this.userController.on(EVENT.REMOTE_USER_LEAVE, (event)=>{
        console.log(TAG_NAME, '远端用户离开', event, event.data.userID)
        if (event.data.userID) {
          this._setList({
            userList: event.data.userList,
            streamList: event.data.streamList,
          }).then(() => {
            this._emitter.emit(EVENT.REMOTE_USER_LEAVE, { userID: event.data.userID })
          })
        }
        console.log(TAG_NAME, 'REMOTE_USER_LEAVE', 'streamList:', this.data.streamList, 'userList:', this.data.userList)
      })
      // 视频状态 true
      this.userController.on(EVENT.REMOTE_VIDEO_ADD, (event)=>{
        console.log(TAG_NAME, '远端视频可用', event, event.data.stream.userID)
        const stream = event.data.stream
        // 如果Android onHide 时，新增的player 无法播放 记录标识位
        if (this.status.pageLife === 'hide') {
          this.status.isOnHideAddStream = true
          stream.isOnHideAdd = true
        }
        this._setList({
          userList: event.data.userList,
          streamList: event.data.streamList,
        }).then(() => {
          // 完善 的stream 的 playerContext
          stream.playerContext = wx.createLivePlayerContext(stream.streamID, this)
          // 新增的需要触发一次play 默认属性才能生效
          // stream.playerContext.play()
          // console.log(TAG_NAME, 'REMOTE_VIDEO_ADD playerContext.play()', stream)
          this._emitter.emit(EVENT.REMOTE_VIDEO_ADD, { userID: stream.userID, streamType: stream.streamType })
        })
        console.log(TAG_NAME, 'REMOTE_VIDEO_ADD', 'streamList:', this.data.streamList, 'userList:', this.data.userList)
      })
      // 视频状态 false
      this.userController.on(EVENT.REMOTE_VIDEO_REMOVE, (event)=>{
        console.log(TAG_NAME, '远端视频移除', event, event.data.stream.userID)
        const stream = event.data.stream
        this._setList({
          userList: event.data.userList,
          streamList: event.data.streamList,
        }).then(() => {
          // 有可能先触发了退房事件，用户名下的所有stream都已清除
          if (stream.userID && stream.streamType) {
            this._emitter.emit(EVENT.REMOTE_VIDEO_REMOVE, { userID: stream.userID, streamType: stream.streamType })
          }
        })
        console.log(TAG_NAME, 'REMOTE_VIDEO_REMOVE', 'streamList:', this.data.streamList, 'userList:', this.data.userList)
      })
      // 音频可用
      this.userController.on(EVENT.REMOTE_AUDIO_ADD, (event)=>{
        console.log(TAG_NAME, '远端音频可用', event)
        const stream = event.data.stream
        this._setList({
          userList: event.data.userList,
          streamList: event.data.streamList,
        }).then(() => {
          stream.playerContext = wx.createLivePlayerContext(stream.streamID, this)
          // 新增的需要触发一次play 默认属性才能生效
          // stream.playerContext.play()
          // console.log(TAG_NAME, 'REMOTE_AUDIO_ADD playerContext.play()', stream)
          this._emitter.emit(EVENT.REMOTE_AUDIO_ADD, { userID: stream.userID, streamType: stream.streamType })
        })
        console.log(TAG_NAME, 'REMOTE_AUDIO_ADD', 'streamList:', this.data.streamList, 'userList:', this.data.userList)
      })
      // 音频不可用
      this.userController.on(EVENT.REMOTE_AUDIO_REMOVE, (event)=>{
        console.log(TAG_NAME, '远端音频移除', event, event.data.stream.userID)
        const stream = event.data.stream
        this._setList({
          userList: event.data.userList,
          streamList: event.data.streamList,
        }).then(() => {
          // 有可能先触发了退房事件，用户名下的所有stream都已清除
          if (stream.userID && stream.streamType) {
            this._emitter.emit(EVENT.REMOTE_AUDIO_REMOVE, { userID: stream.userID, streamType: stream.streamType })
          }
        })
        console.log(TAG_NAME, 'REMOTE_AUDIO_REMOVE', 'streamList:', this.data.streamList, 'userList:', this.data.userList)
      })
    },
    /**
     * pusher event handler
     * @param {*} event 事件实例
     */
    _pusherStateChangeHandler(event) {
      const code = event.detail.code
      const message = event.detail.message
      console.log(TAG_NAME, 'pusherStateChange：', code, event)
      switch (code) {
        case 0: // 未知状态码，不做处理
          console.log(TAG_NAME, message, code)
          break
        case 1001:
          console.log(TAG_NAME, '已经连接推流服务器', code)
          break
        case 1002:
          console.log(TAG_NAME, '已经与服务器握手完毕,开始推流', code)
          break
        case 1003:
          console.log(TAG_NAME, '打开摄像头成功', code)
          break
        case 1004:
          console.log(TAG_NAME, '录屏启动成功', code)
          break
        case 1005:
          console.log(TAG_NAME, '推流动态调整分辨率', code)
          break
        case 1006:
          console.log(TAG_NAME, '推流动态调整码率', code)
          break
        case 1007:
          console.log(TAG_NAME, '首帧画面采集完成', code)
          break
        case 1008:
          console.log(TAG_NAME, '编码器启动', code)
          break
        case 1018:
          console.log(TAG_NAME, '进房成功', code)
          this._emitter.emit(EVENT.LOCAL_JOIN, { userID: this.data.pusher.userID })
          break
        case 1019:
          console.log(TAG_NAME, '退出房间', code)
          // 20200421 iOS 仍然没有1019事件通知退房，退房事件移动到 exitRoom 方法里，但不是后端通知的退房成功
          // this._emitter.emit(EVENT.LOCAL_LEAVE, { userID: this.data.pusher.userID })
          break
        case 2003:
          console.log(TAG_NAME, '渲染首帧视频', code)
          break
        case 1020:
        case 1031:
        case 1032:
        case 1033:
        case 1034:
          // 通过 userController 处理 1020 1031 1032 1033 1034
          this.userController.userEventHandler(event)
          break
        case -1301:
          console.error(TAG_NAME, '打开摄像头失败: ', code)
          this._emitter.emit(EVENT.ERROR, { code, message })
          break
        case -1302:
          console.error(TAG_NAME, '打开麦克风失败: ', code)
          this._emitter.emit(EVENT.ERROR, { code, message })
          break
        case -1303:
          console.error(TAG_NAME, '视频编码失败: ', code)
          this._emitter.emit(EVENT.ERROR, { code, message })
          break
        case -1304:
          console.error(TAG_NAME, '音频编码失败: ', code)
          this._emitter.emit(EVENT.ERROR, { code, message })
          break
        case -1307:
          console.error(TAG_NAME, '推流连接断开: ', code)
          this._emitter.emit(EVENT.ERROR, { code, message })
          break
        case -100018:
          console.error(TAG_NAME, '进房失败: userSig 校验失败，请检查 userSig 是否填写正确', code, message)
          this._emitter.emit(EVENT.ERROR, { code, message })
          break
        case 5000:
          console.log(TAG_NAME, '小程序被挂起: ', code)
          // 20200421 iOS 微信点击胶囊圆点会触发该事件
          // 触发 5000 后，底层SDK会退房，返回前台后会自动进房
          break
        case 5001:
          // 20200421 仅有 Android 微信会触发该事件
          console.log(TAG_NAME, '小程序悬浮窗被关闭: ', code)
          this.status.isPending = true
          if (this.status.isPush) {
            this.exitRoom()
          }
          break
        case 1021:
          console.log(TAG_NAME, '网络类型发生变化，需要重新进房', code)
          break
        case 2007:
          console.log(TAG_NAME, '本地视频播放loading: ', code)
          break
        case 2004:
          console.log(TAG_NAME, '本地视频播放开始: ', code)
          break
        default:
          console.log(TAG_NAME, message, code)
      }
    },
    _pusherNetStatusHandler(event) {
      // 触发 LOCAL_NET_STATE_UPDATE
      this._emitter.emit(EVENT.LOCAL_NET_STATE_UPDATE, event)
    },
    _pusherErrorHandler(event) {
      // 触发 ERROR
      console.warn(TAG_NAME, 'pusher error', event)
      try {
        const code = event.detail.errCode
        const message = event.detail.errMsg
        this._emitter.emit(EVENT.ERROR, { code, message })
      } catch (exception) {
        console.error(TAG_NAME, 'pusher error data parser exception', event, exception)
      }
    },
    _pusherBGMStartHandler(event) {
      // 触发 BGM_START 已经在playBGM方法中进行处理
      // this._emitter.emit(EVENT.BGM_PLAY_START, { data: event })
    },
    _pusherBGMProgressHandler(event) {
      // BGM_PROGRESS
      this._emitter.emit(EVENT.BGM_PLAY_PROGRESS, event)
    },
    _pusherBGMCompleteHandler(event) {
      // BGM_COMPLETE
      this._emitter.emit(EVENT.BGM_PLAY_COMPLETE, event)
    },
    _pusherAudioVolumeNotify: function(event) {
      // console.log(TAG_NAME, '_pusherAudioVolumeNotify', event)
      this._emitter.emit(EVENT.LOCAL_AUDIO_VOLUME_UPDATE, event)
    },
    // player event handler
    // 获取 player ID 再进行触发
    _playerStateChange(event) {
      // console.log(TAG_NAME, '_playerStateChange', event)
      this._emitter.emit(EVENT.REMOTE_STATE_UPDATE, event)
    },
    _playerFullscreenChange(event) {
      // console.log(TAG_NAME, '_playerFullscreenChange', event)
      this._emitter.emit(EVENT.REMOTE_FULLSCREEN_UPDATE, event)
      this._emitter.emit(EVENT.VIDEO_FULLSCREEN_UPDATE, event)
    },
    _playerNetStatus(event) {
      // console.log(TAG_NAME, '_playerNetStatus', event)
      // 获取player 视频的宽高
      const stream = this.userController.getStream({
        userID: event.currentTarget.dataset.userid,
        streamType: event.currentTarget.dataset.streamtype,
      })
      if (stream && (stream.videoWidth !== event.detail.info.videoWidth || stream.videoHeight !== event.detail.info.videoHeight)) {
        console.log(TAG_NAME, '_playerNetStatus update video size', event)
        stream.videoWidth = event.detail.info.videoWidth
        stream.videoHeight = event.detail.info.videoHeight
      }
      this._emitter.emit(EVENT.REMOTE_NET_STATE_UPDATE, event)
    },
    _playerAudioVolumeNotify(event) {
      // console.log(TAG_NAME, '_playerAudioVolumeNotify', event)
      this._emitter.emit(EVENT.REMOTE_AUDIO_VOLUME_UPDATE, event)
    },
    _filterVisibleStream(streamList, skipPagination) {
      const list = streamList.filter((item) => {
        // 全部显示
        // return true
        // 只显示有视频或者有音频的 stream
        return (item.hasVideo || item.hasAudio)
      })
      // 按 userID 进行排序
      list.sort((item1, item2)=>{
        const id1 = item1.userID.toUpperCase()
        const id2 = item2.userID.toUpperCase()
        if (id1 < id2) {
          return -1
        }
        if (id1 > id2) {
          return 1
        }
        return 0
      })
      if (this.data.template === 'grid' && !skipPagination) {
        this._filterGridPageVisibleStream(list)
        // console.log(TAG_NAME, '_filterVisibleStream gridPagePlaceholderStreamList:', this.data.gridPagePlaceholderStreamList)
        if (// list.length > this.data.gridPlayerPerPage - 2 &&
          this.data.gridCurrentPage > 1 &&
          this.data.gridPagePlaceholderStreamList.length === this.data.gridPlayerPerPage) {
          // 如果stream 数量大于每页可显示数量，当前页面已经没有可显示的stream(占位数量==3) 回到上一个页面。
          this._gridPageToPrev(list)
        }
      }
      // console.log(TAG_NAME, '_filterVisibleStream list:', list)
      return list
    },
    _filterGridPageVisibleStream(list) {
      // 最多只显示 gridPlayerPerPage 个stream
      const length = list.length
      // +1 pusher
      this.data.gridPageCount = Math.ceil((length + 1) / this.data.gridPlayerPerPage)
      this.data.gridPagePlaceholderStreamList = []
      let visibleCount = 0
      // 需要显示的player区间
      let interval
      if (this.data.gridPlayerPerPage > 3) {
        if (this.data.gridCurrentPage === 1) {
          interval = [-1, this.data.gridPlayerPerPage - 1]
        } else {
          // 每页显示4个时，第一页显示3个，pusher只在第一页
          // -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
          //    1     2       3        4
          // -1 3
          // 2 7
          // 6 11
          interval = [this.data.gridCurrentPage * this.data.gridPlayerPerPage - (this.data.gridPlayerPerPage + 2), this.data.gridCurrentPage * this.data.gridPlayerPerPage - 1]
        }
      } else {
        // 每页显示3个，每页都有pusher
        interval = [this.data.gridCurrentPage * this.data.gridPlayerPerPage - (this.data.gridPlayerPerPage + 1), this.data.gridCurrentPage * this.data.gridPlayerPerPage]
      }

      for (let i = 0; i < length; i++) {
        if ( i > interval[0] && i < interval[1]) {
          list[i].isVisible = true
          list[i].muteVideo = list[i].muteVideoPrev === undefined ? list[i].muteVideo : list[i].muteVideoPrev
          visibleCount++
        } else {
          list[i].isVisible = false
          list[i].muteVideo = true
        }
      }
      // 第一页，不需要占位
      if (this.data.gridCurrentPage !== 1) {
        for (let i = 0; i < this.data.gridPlayerPerPage - visibleCount; i++) {
          this.data.gridPagePlaceholderStreamList.push({ id: 'holder-' + i })
        }
      }
      return list
    },
    /**
     * 保持屏幕常亮
     */
    _keepScreenOn() {
      setInterval(() => {
        wx.setKeepScreenOn({
          keepScreenOn: true,
        })
      }, 20000)
    },
    //  ______  __       __        ______             __                                              __
    //  |      \|  \     /  \      |      \           |  \                                            |  \
    //   \$$$$$$| $$\   /  $$       \$$$$$$ _______  _| $$_     ______    ______   _______    ______  | $$
    //    | $$  | $$$\ /  $$$        | $$  |       \|   $$ \   /      \  /      \ |       \  |      \ | $$
    //    | $$  | $$$$\  $$$$        | $$  | $$$$$$$\\$$$$$$  |  $$$$$$\|  $$$$$$\| $$$$$$$\  \$$$$$$\| $$
    //    | $$  | $$\$$ $$ $$        | $$  | $$  | $$ | $$ __ | $$    $$| $$   \$$| $$  | $$ /      $$| $$
    //   _| $$_ | $$ \$$$| $$       _| $$_ | $$  | $$ | $$|  \| $$$$$$$$| $$      | $$  | $$|  $$$$$$$| $$
    //  |   $$ \| $$  \$ | $$      |   $$ \| $$  | $$  \$$  $$ \$$     \| $$      | $$  | $$ \$$    $$| $$
    //   \$$$$$$ \$$      \$$       \$$$$$$ \$$   \$$   \$$$$   \$$$$$$$ \$$       \$$   \$$  \$$$$$$$ \$$

    /**
     * 初始化 IM SDK
     * @param {Object} config sdkAppID
     */
    _initIM(config) {
      if (!config.enableIM || !config.sdkAppID || this.tim) {
        return
      }
      console.log(TAG_NAME, '_initIM', config)
      // 初始化 sdk 实例
      const tim = TIM.create({
        SDKAppID: config.sdkAppID,
      })
      // 0 普通级别，日志量较多，接入时建议使用
      // 1 release级别，SDK 输出关键信息，生产环境时建议使用
      // 2 告警级别，SDK 只输出告警和错误级别的日志
      // 3 错误级别，SDK 只输出错误级别的日志
      // 4 无日志级别，SDK 将不打印任何日志
      if (config.debugMode) {
        tim.setLogLevel(1)
      } else {
        tim.setLogLevel(4)
      }
      // 取消监听
      tim.off(TIM.EVENT.SDK_READY, this._onIMReady)
      tim.off(TIM.EVENT.MESSAGE_RECEIVED, this._onIMMessageReceived)
      tim.off(TIM.EVENT.SDK_NOT_READY, this._onIMNotReady)
      tim.off(TIM.EVENT.KICKED_OUT, this._onIMKickedOut)
      tim.off(TIM.EVENT.ERROR, this._onIMError)
      // 监听事件
      tim.on(TIM.EVENT.SDK_READY, this._onIMReady, this)
      tim.on(TIM.EVENT.MESSAGE_RECEIVED, this._onIMMessageReceived, this)
      tim.on(TIM.EVENT.SDK_NOT_READY, this._onIMNotReady, this)
      tim.on(TIM.EVENT.KICKED_OUT, this._onIMKickedOut, this)
      tim.on(TIM.EVENT.ERROR, this._onIMError, this)
      this.tim = tim
      wx.tim = tim
    },
    _loginIM(params) {
      if (!this.tim) {
        return
      }
      console.log(TAG_NAME, '_loginIM', params)
      return this.tim.login({
        userID: params.userID,
        userSig: params.userSig,
      })
    },
    _logoutIM() {
      if (!this.tim) {
        return
      }
      console.log(TAG_NAME, '_logoutIM')
      return this.tim.logout()
    },
    _exitIM() {
      // 方法需要调用限制，否则重复解散群 退群会有warn
      if (this.data.exitIMThrottle || !this.tim) {
        return
      }
      this.data.exitIMThrottle = true
      const userList = this.getRemoteUserList()
      const roomID = this.data.config.roomID
      const userID = this.data.config.userID
      this._searchGroup({ roomID }).then((imResponse) => {
        // 查询群资料，判断是否为群主
        if (imResponse.data.group.ownerID === userID && userList.length === 0) {
          // 如果 userList 为 0 群主可以解散群，并登出IM
          this._dismissGroup({ roomID }).then(()=>{
            this.data.exitIMThrottle = false
            this._logoutIM()
          }).catch((imError) => {
            this.data.exitIMThrottle = false
            this._logoutIM()
          })
        } else if (imResponse.data.group.ownerID === userID) {
          this.data.exitIMThrottle = false
          // 群主不能退群只能登出
          this._logoutIM()
        } else {
          // 普通成员退群并登出IM
          this._quitGroup({ roomID }).then(()=>{
            this.data.exitIMThrottle = false
            this._logoutIM()
          }).catch((imError) => {
            this.data.exitIMThrottle = false
            this._logoutIM()
          })
        }
      }).catch((imError) => {
        this.data.exitIMThrottle = false
        // 查询异常直接登出
        this._logoutIM()
      })
    },
    _searchGroup(params) {
      if (!this.tim) {
        return
      }
      console.log(TAG_NAME, '_searchGroup', params)
      const tim = this.tim
      const promise = tim.searchGroupByID(params.roomID + '')
      promise.then(function(imResponse) {
        // const group = imResponse.data.group // 群组信息
        console.log(TAG_NAME, '_searchGroup success', imResponse)
      }).catch(function(imError) {
        console.warn(TAG_NAME, '_searchGroup fail，TIM 报错信息不影响后续逻辑，可以忽略', imError) // 搜素群组失败的相关信息
      })
      return promise
    },
    /**
     * 创建 AVchatroom
     * @param {*} params roomID
     * @returns {Promise}
     */
    _createGroup(params) {
      if (!this.tim) {
        return
      }
      console.log(TAG_NAME, '_createGroup', params)
      const promise = this.tim.createGroup({
        groupID: params.roomID + '',
        name: params.roomID + '',
        type: IM_GROUP_TYPE,
      })
      promise.then((imResponse) => { // 创建成功
        console.log(TAG_NAME, '_createGroup success', imResponse.data.group) // 创建的群的资料
      }).catch((imError) => {
        console.warn(TAG_NAME, '_createGroup error', imError) // 创建群组失败的相关信息
      })
      return promise
    },
    /**
     * 进入 AVchatroom
     * @param {*} params roomID
     * @returns {Promise}
     */
    _joinGroup(params) {
      if (!this.tim) {
        return
      }
      console.log(TAG_NAME, '_joinGroup', params)
      const promise = this.tim.joinGroup({ groupID: params.roomID + '', type: IM_GROUP_TYPE })
      promise.then((imResponse) => {
        switch (imResponse.data.status) {
          case TIM.TYPES.JOIN_STATUS_WAIT_APPROVAL: // 等待管理员同意
            break
          case TIM.TYPES.JOIN_STATUS_SUCCESS: // 加群成功
          case TIM.TYPES.JOIN_STATUS_ALREADY_IN_GROUP: // 已经在群中
            // console.log(imResponse.data.group) // 加入的群组资料
            // wx.showToast({
            //   title: '进群成功',
            // })
            console.log(TAG_NAME, '_joinGroup success', imResponse)
            break
          default:
            break
        }
      }).catch((imError) => {
        console.warn(TAG_NAME, 'joinGroup error', imError) // 申请加群失败的相关信息
      })
      return promise
    },
    _quitGroup(params) {
      if (!this.tim) {
        return
      }
      console.log(TAG_NAME, '_quitGroup', params)
      const promise = this.tim.quitGroup(params.roomID + '')
      promise.then((imResponse) => {
        console.log(TAG_NAME, '_quitGroup success', imResponse)
      }).catch((imError) => {
        console.warn(TAG_NAME, 'quitGroup error', imError)
      })
      return promise
    },
    _dismissGroup(params) {
      if (!this.tim) {
        return
      }
      console.log(TAG_NAME, '_dismissGroup', params)
      const promise = this.tim.dismissGroup(params.roomID + '')
      promise.then((imResponse) => {
        console.log(TAG_NAME, '_dismissGroup success', imResponse)
      }).catch((imError) => {
        console.warn(TAG_NAME, '_dismissGroup error', imError)
      })
      return promise
    },
    _onIMReady(event) {
      console.log(TAG_NAME, 'IM.READY', event)
      this._emitter.emit(EVENT.IM_READY, event)
      const roomID = this.data.config.roomID
      // 查询群组是否存在
      this._searchGroup({ roomID }).then((res) => {
        // console.log(TAG_NAME, 'searchGroup', res)
        // 存在直接进群
        this._joinGroup({ roomID })
      }).catch(() => {
        // 不存在则创建，如果是avchatroom 创建后进群
        this._createGroup({ roomID }).then((res) => {
          // 进群
          this._joinGroup({ roomID })
        }).catch((imError)=> {
          if (imError.code === 10021) {
            console.log(TAG_NAME, '群已存在，直接进群', event)
            this._joinGroup({ roomID })
          }
        })
      })
      // 收到离线消息和会话列表同步完毕通知，接入侧可以调用 sendMessage 等需要鉴权的接口
      // event.name - TIM.EVENT.IM_READY
    },
    _onIMMessageReceived(event) {
      // 收到推送的单聊、群聊、群提示、群系统通知的新消息，可通过遍历 event.data 获取消息列表数据并渲染到页面
      console.log(TAG_NAME, 'IM.MESSAGE_RECEIVED', event)
      // messageList 仅保留10条消息
      const messageData = event.data
      const roomID = this.data.config.roomID + ''
      const userID = this.data.config.userID + ''
      for (let i = 0; i < messageData.length; i++) {
        const message = messageData[i]
        // console.log(TAG_NAME, 'IM.MESSAGE_RECEIVED', message, this.data.config, TIM.TYPES.MSG_TEXT)
        if (message.to === roomID + '' || message.to === userID) {
          // 遍历messageData 获取当前room 或者当前user的消息
          console.log(TAG_NAME, 'IM.MESSAGE_RECEIVED', message, message.type, TIM.TYPES.MSG_TEXT)
          if (message.type === TIM.TYPES.MSG_TEXT) {
            this._pushMessageList({
              name: message.from,
              message: message.payload.text,
            })
          } else {
            if (message.type === TIM.TYPES.MSG_GRP_SYS_NOTICE && message.payload.operationType === 2) {
              // 群系统通知
              this._pushMessageList({
                name: '系统通知',
                message: `欢迎 ${userID}`,
              })
            }
            // 其他消息暂不处理
          }
        }
      }
      this._emitter.emit(EVENT.IM_MESSAGE_RECEIVED, event)
    },
    _onIMNotReady(event) {
      console.log(TAG_NAME, 'IM.NOT_READY', event)
      this._emitter.emit(EVENT.IM_NOT_READY, event)
      // 收到 SDK 进入 not ready 状态通知，此时 SDK 无法正常工作
      // event.name - TIM.EVENT.IM_NOT_READY
    },
    _onIMKickedOut(event) {
      console.log(TAG_NAME, 'IM.KICKED_OUT', event)
      this._emitter.emit(EVENT.IM_KICKED_OUT, event)
      // 收到被踢下线通知
      // event.name - TIM.EVENT.KICKED_OUT
      // event.data.type - 被踢下线的原因，例如 :
      //    - TIM.TYPES.KICKED_OUT_MULT_ACCOUNT 多实例登录被踢
      //    - TIM.TYPES.KICKED_OUT_MULT_DEVICE 多终端登录被踢
      //    - TIM.TYPES.KICKED_OUT_USERSIG_EXPIRED 签名过期被踢。使用前需要将SDK版本升级至v2.4.0或以上。
    },
    _onIMError(event) {
      console.log(TAG_NAME, 'IM.ERROR', event)
      this._emitter.emit(EVENT.IM_ERROR, event)
      // 收到 SDK 发生错误通知，可以获取错误码和错误信息
      // event.name - TIM.EVENT.ERROR
      // event.data.code - 错误码
      // event.data.message - 错误信息
    },

    //  ________                                  __             __
    //  |        \                                |  \           |  \
    //   \$$$$$$$$______   ______ ____    ______  | $$  ______  _| $$_     ______
    //     | $$  /      \ |      \    \  /      \ | $$ |      \|   $$ \   /      \
    //     | $$ |  $$$$$$\| $$$$$$\$$$$\|  $$$$$$\| $$  \$$$$$$\\$$$$$$  |  $$$$$$\
    //     | $$ | $$    $$| $$ | $$ | $$| $$  | $$| $$ /      $$ | $$ __ | $$    $$
    //     | $$ | $$$$$$$$| $$ | $$ | $$| $$__/ $$| $$|  $$$$$$$ | $$|  \| $$$$$$$$
    //     | $$  \$$     \| $$ | $$ | $$| $$    $$| $$ \$$    $$  \$$  $$ \$$     \
    //      \$$   \$$$$$$$ \$$  \$$  \$$| $$$$$$$  \$$  \$$$$$$$   \$$$$   \$$$$$$$
    //                                  | $$
    //                                  | $$
    //                                   \$$
    // 以下为 debug & template 相关函数

    _toggleVideo() {
      if (this.data.pusher.enableCamera) {
        this.unpublishLocalVideo()
      } else {
        this.publishLocalVideo()
      }
    },
    _toggleAudio() {
      if (this.data.pusher.enableMic) {
        this.unpublishLocalAudio()
      } else {
        this.publishLocalAudio()
      }
    },
    _debugToggleRemoteVideo(event) {
      console.log(TAG_NAME, '_debugToggleRemoteVideo', event.currentTarget.dataset)
      const userID = event.currentTarget.dataset.userID
      const streamType = event.currentTarget.dataset.streamType
      const stream = this.data.streamList.find((item)=>{
        return item.userID === userID && item.streamType === streamType
      })
      if (stream.muteVideo) {
        this.subscribeRemoteVideo({ userID, streamType })
        // this.setViewVisible({ userID, streamType, isVisible: true })
      } else {
        this.unsubscribeRemoteVideo({ userID, streamType })
        // this.setViewVisible({ userID, streamType, isVisible: false })
      }
    },
    _debugToggleRemoteAudio(event) {
      console.log(TAG_NAME, '_debugToggleRemoteAudio', event.currentTarget.dataset)
      const userID = event.currentTarget.dataset.userID
      const streamType = event.currentTarget.dataset.streamType
      const stream = this.data.streamList.find((item)=>{
        return item.userID === userID && item.streamType === streamType
      })
      if (stream.muteAudio) {
        this.subscribeRemoteAudio({ userID })
      } else {
        this.unsubscribeRemoteAudio({ userID })
      }
    },
    _debugToggleVideoDebug() {
      this.setData({
        debug: !this.data.debug,
      })
    },
    _debugExitRoom() {
      this.exitRoom()
    },
    _debugEnterRoom() {
      Object.assign(this.data.pusher, this.data.config)
      this.enterRoom({ roomID: this.data.config.roomID }).then(()=>{
        setTimeout(()=>{
          this.publishLocalVideo()
          this.publishLocalAudio()
        }, 2000)
        // 进房后开始推送视频或音频
      })
    },
    _debugGoBack() {
      wx.navigateBack({
        delta: 1,
      })
    },
    _debugTogglePanel() {
      this.setData({
        debugPanel: !this.data.debugPanel,
      })
    },
    _debugSendRandomMessage() {
      const userList = this.getRemoteUserList()
      if (userList.length === 0 || !this.tim) {
        return false
      }
      const roomID = this.data.config.roomID
      const message = `Hello! ${userList[0].userID} ${9999 * Math.random()}`
      const userID = userList[0].userID

      this.sendC2CTextMessage({
        userID: userID,
        message: message,
      })
      const promise = this.sendGroupTextMessage({
        roomID: roomID,
        message: message,
      })
      // 消息上屏
      this._pushMessageList({
        name: userID,
        message: message,
      })

      promise.then(function(imResponse) {
        // 发送成功
        console.log(TAG_NAME, '_debugSendRandomMessage success', imResponse)
        wx.showToast({
          title: '发送成功',
          icon: 'success',
          duration: 1000,
        })
      }).catch(function(imError) {
        // 发送失败
        console.warn(TAG_NAME, '_debugSendRandomMessage error', imError)
        wx.showToast({
          title: '发送失败',
          icon: 'none',
          duration: 1000,
        })
      })
    },
    _toggleAudioVolumeType() {
      if (this.data.pusher.audioVolumeType === 'voicecall') {
        this._setPusherConfig({
          audioVolumeType: 'media',
        })
      } else {
        this._setPusherConfig({
          audioVolumeType: 'voicecall',
        })
      }
    },
    _toggleSoundMode() {
      if (this.data.userList.length === 0 ) {
        return
      }
      const stream = this.userController.getStream({
        userID: this.data.userList[0].userID,
        streamType: 'main',
      })
      if (stream) {
        if (stream.soundMode === 'speaker') {
          stream['soundMode'] = 'ear'
        } else {
          stream['soundMode'] = 'speaker'
        }
        this._setPlayerConfig({
          userID: stream.userID,
          streamType: 'main',
          config: {
            soundMode: stream['soundMode'],
          },
        })
      }
    },
    /**
     * 退出通话
     */
    _hangUp() {
      this.exitRoom()
      wx.navigateBack({
        delta: 1,
      })
    },
    /**
     * 切换订阅音频状态
     */
    handleSubscribeAudio() {
      if (this.data.pusher.enableMic) {
        this.unpublishLocalAudio()
      } else {
        this.publishLocalAudio()
      }
    },
    /**
     * 切换订阅远端视频状态
     * @param {Object} event native 事件对象
     */
    _handleSubscribeRemoteVideo(event) {
      const userID = event.currentTarget.dataset.userID
      const streamType = event.currentTarget.dataset.streamType
      const stream = this.data.streamList.find((item)=>{
        return item.userID === userID && item.streamType === streamType
      })
      if (stream.muteVideo) {
        this.subscribeRemoteVideo({ userID, streamType })
      } else {
        this.unsubscribeRemoteVideo({ userID, streamType })
      }
    },
    /**
     *
     * @param {Object} event native 事件对象
     */
    _handleSubscribeRemoteAudio(event) {
      const userID = event.currentTarget.dataset.userID
      const streamType = event.currentTarget.dataset.streamType
      const stream = this.data.streamList.find((item)=>{
        return item.userID === userID && item.streamType === streamType
      })
      if (stream.muteAudio) {
        this.subscribeRemoteAudio({ userID })
      } else {
        this.unsubscribeRemoteAudio({ userID })
      }
    },
    /**
     * grid布局, 唤起 memberlist-panel
     */
    _switchMemberListPanel() {
      this.setData({
        panelName: this.data.panelName !== 'memberlist-panel' ? 'memberlist-panel' : '',
      })
    },
    /**
     * grid布局, 唤起 setting-panel
     */
    _switchSettingPanel() {
      this.setData({
        panelName: this.data.panelName !== 'setting-panel' ? 'setting-panel' : '',
      })
    },
    _switchBGMPanel() {
      this.setData({
        panelName: this.data.panelName !== 'bgm-panel' ? 'bgm-panel' : '',
      })
    },
    _handleMaskerClick() {
      this.setData({
        panelName: '',
      })
    },

    _setPuserProperty(event) {
      console.log(TAG_NAME, '_setPuserProperty', event)
      const key = event.currentTarget.dataset.key
      const valueType = event.currentTarget.dataset.valueType
      let value = event.currentTarget.dataset.value
      const config = {}
      if (valueType === 'boolean') {
        value = value === 'true' ? true : false
        config[key] = !this.data.pusher[key]
      }
      if (valueType === 'number' && value.indexOf('|') > 0) {
        value = value.split('|')
        // console.log(this.data.pusher, this.data.pusher[key], key, value)
        if ( this.data.pusher[key] === Number(value[0])) {
          config[key] = Number(value[1])
        } else {
          config[key] = Number(value[0])
        }
      }
      if (valueType === 'string' && value.indexOf('|') > 0) {
        value = value.split('|')
        if ( this.data.pusher[key] === value[0]) {
          config[key] = value[1]
        } else {
          config[key] = value[0]
        }
      }
      this._setPusherConfig(config)
    },
    _setPlayerProperty(event) {
      console.log(TAG_NAME, '_setPlayerProperty', event)
      const userID = event.currentTarget.dataset.userid
      const streamType = event.currentTarget.dataset.streamtype
      const key = event.currentTarget.dataset.key
      let value = event.currentTarget.dataset.value
      const stream = this.userController.getStream({
        userID: userID,
        streamType: streamType,
      })
      if (!stream) {
        return
      }
      const config = {}
      if (value === 'true') {
        value = true
      } else if (value === 'false') {
        value = false
      }
      if (typeof value === 'boolean') {
        config[key] = !stream[key]
      } else if (typeof value === 'string' && value.indexOf('|') > 0) {
        value = value.split('|')
        if (stream[key] === value[0]) {
          config[key] = value[1]
        } else {
          config[key] = value[0]
        }
      }
      console.log(TAG_NAME, '_setPlayerProperty', config)
      this._setPlayerConfig({ userID, streamType, config })
    },
    _changeProperty(event) {
      const propertyName = event.currentTarget.dataset.propertyName
      const newData = {}
      newData[propertyName] = event.detail.value
      this.setData(newData)
      const volume = newData[propertyName] / 100
      switch (propertyName) {
        case 'MICVolume':
          this.setMICVolume({ volume })
          break
        case 'BGMVolume':
          this.setBGMVolume({ volume })
          break
      }
    },
    _switchStreamType(event) {
      const userID = event.currentTarget.dataset.userid
      const streamType = event.currentTarget.dataset.streamtype
      const stream = this.userController.getStream({
        userID: userID,
        streamType: streamType,
      })
      if (stream && stream.streamType === 'main') {
        if (stream._definitionType === 'small') {
          this.subscribeRemoteVideo({ userID, streamType: 'main' })
        } else {
          this.subscribeRemoteVideo({ userID, streamType: 'small' })
        }
      }
    },
    _handleSnapshotClick(event) {
      wx.showToast({
        title: '开始截屏',
        icon: 'none',
        duration: 1000,
      })
      const userID = event.currentTarget.dataset.userid
      const streamType = event.currentTarget.dataset.streamtype
      this.snapshot({ userID, streamType })
    },
    /**
     * grid布局, 绑定事件
     */
    _gridBindEvent() {
      // 远端音量变更
      this.on(EVENT.REMOTE_AUDIO_VOLUME_UPDATE, (event) => {
        const data = event.data
        const userID = data.currentTarget.dataset.userid
        const streamType = data.currentTarget.dataset.streamtype
        const volume = data.detail.volume
        // console.log(TAG_NAME, '远端音量变更', userID, streamType, volume, event)
        const stream = this.userController.getStream({
          userID: userID,
          streamType: streamType === 'aux' ? 'main' : streamType, // 远端推辅流后，音量回调会从辅流的 player 返回，而不是主流player 返回。需要等 native SDK修复。
        })
        if (stream) {
          stream.volume = volume
        }
        this.setData({
          streamList: this.data.streamList,
          visibleStreamList: this._filterVisibleStream(this.data.streamList, true),
        }, () => {
        })
      })
      this.on(EVENT.BGM_PLAY_PROGRESS, (event) => {
        // console.log(TAG_NAME, '_gridBindEvent on BGM_PLAY_PROGRESS', event)
        const BGMProgress = event.data.detail.progress / event.data.detail.duration * 100
        this.setData({ BGMProgress })
      })
      this.on(EVENT.LOCAL_AUDIO_VOLUME_UPDATE, (event) => {
        // console.log(TAG_NAME, '_gridBindEvent on LOCAL_AUDIO_VOLUME_UPDATE', event)
        // const data = event.data
        const volume = event.data.detail.volume
        // 避免频繁输出log
        this._setPusherConfig({ volume }, true)
      })
    },
    _handleGridTouchStart(event) {
      touchX = event.changedTouches[0].clientX
      touchY = event.changedTouches[0].clientY
    },
    _handleGridTouchEnd(event) {
      const x = event.changedTouches[0].clientX
      const y = event.changedTouches[0].clientY

      if (x - touchX > 50 && Math.abs(y - touchY) < 50) {
        // console.log(TAG_NAME, '向右滑 当前页面', this.data.gridCurrentPage, this.data.gridPageCount)
        this._gridPagePrev()
      } else if (x - touchX < -50 && Math.abs(y - touchY) < 50) {
        // console.log(TAG_NAME, '向左滑 当前页面', this.data.gridCurrentPage, this.data.gridPageCount)
        this._gridPageNext()
      }
    },
    _gridPageToPrev(streamList) {
      const visibleStreamList = this._filterGridPageVisibleStream(streamList)
      if (this.data.gridPagePlaceholderStreamList.length === this.data.gridPlayerPerPage) {
        this.data.gridCurrentPage--
        this._gridPageToPrev(streamList)
      } else {
        return visibleStreamList
      }
    },
    _gridPageNext() {
      this.data.gridCurrentPage++
      if (this.data.gridCurrentPage > this.data.gridPageCount) {
        this.data.gridCurrentPage = 1
      }
      this._gridPageSetData()
    },
    _gridPagePrev() {
      this.data.gridCurrentPage--
      if (this.data.gridCurrentPage < 1) {
        this.data.gridCurrentPage = this.data.gridPageCount
      }
      this._gridPageSetData()
    },
    _gridPageSetData() {
      this._gridShowPageTips()
      const visibleStreamList = this._filterVisibleStream(this.data.streamList)
      this.setData({
        gridCurrentPage: this.data.gridCurrentPage,
        gridPageCount: this.data.gridPageCount,
        visibleStreamList: visibleStreamList,
        streamList: this.data.streamList,
        gridPagePlaceholderStreamList: this.data.gridPagePlaceholderStreamList,
      }, () => {

      })
    },
    _gridShowPageTips(event) {
      if (this.data.gridPageCount < 2) {
        return
      }
      console.log(TAG_NAME, '_gridShowPageTips', this.data)
      if (this.data.hasGridPageTipsShow) {
        clearTimeout(this.data.hasGridPageTipsShow)
      }
      this.animate('.pages-container', [
        { opacity: 1 },
      ], 100, ()=>{

      })
      this.data.hasGridPageTipsShow = setTimeout(()=>{
        this.animate('.pages-container', [
          { opacity: 1 },
          { opacity: 0.3 },
        ], 600, ()=>{

        })
      }, 3000)
    },
    _toggleFullscreen(event) {
      console.log(TAG_NAME, '_toggleFullscreen', event)
      const userID = event.currentTarget.dataset.userID
      const streamType = event.currentTarget.dataset.streamType
      if (this._isFullscreen) {
        this.exitFullscreen({ userID, streamType }).then(() => {
          this._isFullscreen = false
        }).catch(() => {
        })
      } else {
        // const stream = this.userController.getStream({ userID, streamType })
        const direction = 0
        // 已知问题：视频的尺寸需要等待player触发NetStatus事件才能获取到，如果进房就双击全屏，全屏后的方向有可能不对。
        // if (stream && stream.videoWidth && stream.videoHeight) {
        //   // 如果是横视频，全屏时进行横屏处理。如果是竖视频，则为0
        //   direction = stream.videoWidth > stream.videoHeight ? 90 : 0
        // }
        this.enterFullscreen({ userID, streamType, direction }).then(() => {
          this._isFullscreen = true
        }).catch(() => {
        })
      }
    },
    _toggleMoreMenu() {
      this.setData({
        isShowMoreMenu: !this.data.isShowMoreMenu,
      })
    },
    _toggleIMPanel() {
      if (!this.data.enableIM) {
        wx.showToast({
          icon: 'none',
          title: '当前没有开启IM功能，请设置 enableIM:true',
        })
      }
      this.setData({
        showIMPanel: !this.data.showIMPanel,
      })
    },
    _handleBGMOperation(event) {
      const operationName = event.currentTarget.dataset.operationName
      if (this[operationName]) {
        this[operationName]({ url: 'https://trtc-1252463788.cos.ap-guangzhou.myqcloud.com/web/assets/bgm-test.mp3' })
      }
    },
    _selectBeautyStyle: function(event) {
      console.log(TAG_NAME, '_selectBeautyStyle', event)
      // this.data.beauty = (event.detail.value === 'close' ? 0 : 9)
      const value = event.detail.value
      this.setData({
        // beauty: (value === 'close' ? 0 : 9),
        beautyStyle: value,
      }, () => {
        this._setPusherConfig({
          beautyLevel: value === 'close' ? 0 : 9,
          beautyStyle: value === 'close' ? 'smooth' : value,
        })
      })
    },
    _selectFilter: function(event) {
      console.log(TAG_NAME, '_selectFilter', event)
      const index = parseInt(event.detail.value)
      this.setData({ filterIndex: index }, () => {
        this._setPusherConfig({
          filter: this.data.filterArray[index].value,
        })
      })
    },
    _selectAudioReverbType: function(event) {
      console.log(TAG_NAME, '_selectAudioReverbType', event)
      const audioReverbType = parseInt(event.detail.value)
      this._setPusherConfig({ audioReverbType })
    },
    _sendIMMessage(event) {
      console.log(TAG_NAME, '_sendIMMessage', event)
      if (!this.data.messageContent) {
        return
      }
      const roomID = this.data.config.roomID
      const message = this.data.messageContent
      const userID = this.data.config.userID
      this.sendGroupTextMessage({ roomID, message })
      // 消息上屏
      this._pushMessageList({
        name: userID,
        message: message,
      })
      this.setData({
        messageContent: '',
      })
    },
    _inputIMMessage(event) {
      // console.log(TAG_NAME, '_inputIMMessage', event)
      this.setData({
        messageContent: event.detail.value,
      })
    },
    _pushMessageList(params) {
      if (this.data.messageList.length === this.data.maxMessageListLength) {
        this.data.messageList.shift()
      }
      this.data.messageList.push(params)
      this.setData({
        messageList: this.data.messageList,
        messageListScrollTop: this.data.messageList.length * 100,
      }, () => {
      })
    },
  },
})
export default global['__wxComponents']['trtc-room/trtc-room']
</script>
<style platform="mp-weixin">
@import "./template/1v1/1v1.css";
@import "./template/grid/grid.css";
@import "./template/custom/custom.css";

.pusher {
  width: 100%;
  height: 100%;
}

.player {
  width: 100%;
  height: 100%;
}

.debug-info{
  max-width: 100vw;
  max-height: 90vh;
  box-sizing: border-box;
  overflow-y: scroll;
  position: absolute;
  z-index: 9999;
  background-color: rgba(0, 0, 0, .5);
  color: #fff;
  bottom: 20rpx;
  left: 0;
  padding: 10rpx;
  font-size: 12px;
}
.debug-info-btn .debug-btn,
.debug-info .debug-btn{
  padding: 0 8px;
  min-height: 18px;
  width: auto;
  font-size: 12px;
  line-height: 18px;
  display: inline-block;
  color: #06ae56;
  background-color: #f2f2f2;
}
.debug-info .debug-btn.false{
  color: rgb(114, 114, 114);
}
.debug-info-btn .debug-btn,
.debug-info .button-hover {
  background-color: rgb(219, 219, 219);
}
.debug-info .close-btn{
  position: absolute;
  top: 0;
  right: 0;
  padding: 5px 10px;
}
.debug-info .text.true{
  color: #1fff8b;
}
.debug-info .text.false{
  color: #ff2e2e;
}
.debug-info-btn{
  position: absolute;
  z-index: 9998;
  bottom: 160rpx;
  left: 0;
}

.trtc-room-container .btn {
  display: inline-block;
  width: auto;
  height: 60rpx;
  min-height: 60rpx;
  line-height: 60rpx;
  font-size: 12px;
  font-weight: normal;
  padding: 0 10rpx;
  color: #006eff;
  background-color: #f2f2f2;
  margin: 0 16rpx;
}
.trtc-room-container .btn.active{
  color: #f2f2f2;
  background-color: #006eff;
}
.trtc-room-container .btn-hover{
  background-color: #d1d1d1;
}

.im-panel{
  position: absolute;
  z-index: 9;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 90vw;
  height: 320rpx;
  top: 50vh;
  left: 50vw;
  transform: translate(-50%, -50%);
  padding: 20rpx 0;
  border-radius: 10rpx;
  font-size: 12px;
  /* bottom: 25vh; */
  color: #fff;
  background-color: rgba(0, 0, 0, 0.8);
}
.im-panel .close-btn {
  position: absolute;
  top: 0;
  right: -3px;
  padding: 5px 10px;
  z-index: 99;
}
.message-panel-body{
  width: 100%;
  height: 80%;
  overflow-x: hidden;
  overflow-y: scroll;
}
.message-scroll-container{
  height: 100%;
  /* box-sizing: border-box;
  padding: 0 20rpx; */
}
.message-list{
  width: 100%;
  box-sizing: border-box;
  padding: 0 20rpx;
  /* display: flex;
  flex-direction: column; */
}
.message-item{
  width: 100%;
  /* height: 36rpx; */
  /* padding: 0 20rpx; */
  padding-bottom: 10rpx;
  display: flex;
  flex-direction: row;
}
.message-item .user-name{
  width: 20%;
  color: #2483ff;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.user-name.mine{
  color: #ff7424;
}
.message-item .separate{
  padding:0 5px;
  color: #fff;
}
.message-item .message-content{
  word-wrap:break-word; 
  word-break:break-all;
  padding-left: 20rpx;
  position: relative;
  max-width: 80%; 
  box-sizing: border-box;
}
.message-content::after{
  content: ':';
  position: absolute;
  left: 0;
  top: 0;
}
.message-panel-bottom{
  width: 100%;
  height: 50rpx;
  box-sizing: border-box;
  padding: 0 20rpx 0;
  margin-top: 20rpx;
  display: flex;
  flex-direction: row;
}
.message-input-container {
  flex-grow: 1;
}
.message-input-container .message-input {
  font-size: 12px;
  padding-left: 20rpx;
  border-radius: 10rpx;
  height: 100%;
  background-color: rgba(0,0,0,0.1);
}
.message-send-btn .btn{
  margin-right: 0;
  height: 50rpx;
  min-height: 50rpx;
  line-height: 50rpx;
}

.volume-animation{
  position: absolute;
  width: 80rpx;
  height: 80rpx;
  left: 0;
  top: initial;
  bottom: 20rpx;
  z-index: 9;
  /* transform: translate(-50%, 0); */
}
.volume-animation .image{
  position: absolute;
  width: 80rpx;
  height: 80rpx;
}
.volume-animation .audio-active{
  animation: viewlinear 1.5s linear infinite;
} 
@keyframes viewlinear {
  
  /** 第一种写法**/
  0% {
    height: 0;
  }
  100% {
    height: 100%;
  }
}

.none,
.view-container.none,
.template-grid .view-container.none,
.template-1v1 .view-container.none{
  display: none !important;
}
</style>