# @besovideo/webrtc-player

![npm](https://img.shields.io/npm/v/@besovideo/webrtc-player) ![NPM](https://img.shields.io/npm/l/@besovideo/webrtc-player) ![npm](https://img.shields.io/npm/dt/@besovideo/webrtc-player)

[![NPM](https://nodei.co/npm/@besovideo/webrtc-player.png)](https://nodei.co/npm/@besovideo/webrtc-player/)

## 简介 Introduction

> 设备视频播放、对讲封装
[Webrtc 与 平台接口关系](README.webrtc.md)

## 安装 Installation

**use npm**

```shell
npm i @besovideo/webrtc-player
```

**use yarn**

```
yarn add @besovideo/webrtc-player
```

## 示例 Example

**视频**

```typescript
import {
  PuPlayer,
  IPuPlayerProps,
  IPuPlayerInstance,
  clearAllDialog,
  callEndPointUtils,
  globalConfigHandler
} from "@besovideo/webrtc-player";
import "@besovideo/webrtc-player/dist/main.es.css";

const { instance } = PuPlayer({
  // (可选) 容器节点 注意一个容器内只能存在一个实例  当container为假值（包括false、null、undefined）时 将返回实例引用的dom节点 容器必须指定高度 参考高德地图
  container: document.getElementById("player_container"),
  // 必填 设备选项
  puOption: {
    // 设备id
    id: "PU_123456",
    // 设备通道号
    index: 0,
  },
  // 必填 用户授权令牌
  token: "Y3UrQ1VfYWRtestYDHYUAStest",
  // (可选) peer connection 连接媒体配置
  defaultMediaOption: {
    // 启用音频
    audio: true,
    // 启用视频
    video: true,
  },
  // (可选) 传输协议
  type: "webrtc" | "ws-bvrtc" | "auto";
  // (可选) 是否静音
  muted: false,
  // (可选) 指定video如何适应容器 (fill: 填充容器 ; contain :保持视频比例适应容器)
  videoFit: "fill",
  // (可选) 指定请求url路径前缀 可使用protocol+host 如：http://192.168.88.11:9780
  apiPrefix: "/test_api/prefix",

  // (可选) 用于测试播放视频 (存在时优先使用url播放，为浏览器直接播放)
  // testUrl: "https://cdn.plyr.io/static/demo/View_From_A_Blue_Moon_Trailer-576p.mp4",
  // (可选) 连接建立后回调
  onConnected: () => {},
  // (可选) 连接建立失败时的回调
  onConnectedFailed: () => {},
  // (可选) 连接断开后回调
  onDisConnected: () => {},
  // (可选) token 失效回调
  onTokenFail: () => void,
  // (可选) 双击是否全屏
  fullScreenOnDblclick: true,
  // (可选) 启用控制器
  enableController: false,
  // (可选) 禁用控制器的控制项 下面示例中禁用了云台控制
  disabledControls: ["ptzControl"],
  // (可选) 设置最大重连间隔(s)，默认不自动重连，setDefaultProps设置的maxReconnectInterval高于props
  maxReconnectInterval: 30,
  // (可选) 设置webrtc下视频预览，当没有视频时有音频情况下，是否展示音频波形
  noVideoRendeAudioWaveform: false,
  // (可选) 设置语言支持中文zh、英文en，默认中文
  locale: "zh",
  // (可选) bvrtc配置
  bvrtcConfig: {
    // (可选) 是否启用webcodecs解码，默认启用
    enableWebcodes: boolean,
    // (可选) 设置解码实时性 范围 0-10, 0 -- 流畅性最大, 10 -- 实时性最大
    realTimeLevel: number
  }
  //配置本地录像
  localRecordCfg?: {
    //多久产生一个录像文件 5 - 60， 默认 20
    minuteOneFile ?: number; 
  },
  // (可选) 播放窗口水印配置，支持 string 或 string[] 多行水印
  watermark?: {
    text: "测试水印",
  }
});

// setVideoFit 更改videoFit
instance.setVideoFit("contain");
// setToken 更改请求时的token
instance.setToken("newTTTTToken");
// setPuOption 更改请求的设备信息
instance.setPuOption({
  id: "PU_12345test",
  index: 123,
});
// setApiPrefix 更改请求时的url前缀
instance.setApiPrefix("/测试改前缀");
// open 的可选参数muted如果为undefined, 是否静音将使用之前的状态
// open 异步 先关闭再开始建立连接 (可选参数: muted:boolean = false 是否静音打开,可选参数: protocol = type 选择协议(type = "auto" | "webrtc" | "ws-bvrtc") 不填默认为auto)
instance.open();
// close 异步 释放dialog 并关闭 当前的 peer connection
instance.close();
// destroy 执行close 并从dom移除PuPlayer节点
instance.destroy();
// moveTo 将节点移动至新的容器
instance.moveTo(document.getElementById("new_player_container"));
// getCurrentContainer 获得当前所在的容器
instance.getCurrentContainer();
// hidden 隐藏 异步 (可选参数: muted?:boolean 是否静音 可不传)
instance.hidden(true);
// display 显示 (可选参数: muted?:boolean 是否静音 可不传)
instance.display();
// play 播放 异步
instance.play();
// mute 切换静音 (可选参数: muted:boolean = false 是否静音)
instance.mute(true);
// 获取puPlayer的div
instance.getPlayerElement();
// 获取内部引用的video元素
instance.getVideoElement();

//播放器状态
instance.playerStatus : {
  // 全屏状态管理
  fullscreen: {
    // 获取全屏状态
    Get(): boolean,
    // 设置全屏状态
    Set(boolean): void;
    // 添加事件监听
    addEventListener(handle: ((bFullscreen: boolean) => void)):void;
    // 移除事件监听
    removeEventListener(handle: ((bFullscreen: boolean) => void)):void;
  },
  volume: {
    // 获取音量 0 - 1 之间
    Get(): number,
    // 设置音量 0 - 1 之间
    Set(volume: number): void;
    // 添加事件监听
    addEventListener(handle: ((volume: number) => void)):void;
    // 移除事件监听
    removeEventListener(handle: ((volume: number) => void)):void;
  },
  information: {
    // 获取信息窗口显示状态，是否显示
    Get(): boolean,
    // 设置信息窗口显示状态，是否显示
    Set(bShow: boolean): void;
    // 添加事件监听
    addEventListener(handle: ((bShow: boolean) => void)):void;
    // 移除事件监听
    removeEventListener(handle: ((bShow: boolean) => void)):void;
  },
  record：{
    // 获取录像状态，是否在录像
    //"recordStart" 录像已经开始
    //"recordStop"  录像已结束
    Get(): {type: "recordStart" | "recordStop"},
    // 设置录像状态
    Set(status: {type: "recordStart" | "recordStop"}): void;
    // 添加事件监听, type == "recordStop" recordInfo 才有值
    addEventListener(handle: ((status: {
      type: "recordStart" | "recordStop", 
      recordInfo?: { 
        data: Blob;        // 数据
        startTime: number; // 录像开始时间 utc ms
        endTime: number;   // 录像结束时间 utc ms
        duration: number;  // 录像时长 ms
        fileName: string;  // 文件名
        puOptions: {
          id: string;       // 设备名称
          index: number;    // 通道ID
          name?: string;          //设备名称，用于内部UI显示
          indexName?: string;     //通道名称，用于内部UI显示
        },
        //调用将取消自动下载，返回下载函数
        cancelAutoDownload: () => {
          //调用该接口，浏览器下载文件， filename 不填为默认的文件名，否则为[filename].webm
          download(filename?: string): void;
        }
      }
     }) => void)):void;
    // 移除事件监听
    removeEventListener(handle: ((status: {
      type: "recordStart" | "recordStop", 
      recordInfo?: { 
        data: Blob;        // 数据
        startTime: number; // 录像开始时间 utc ms
        endTime: number;   // 录像结束时间 utc ms
        duration: number;  // 录像时长 ms
        fileName: string;  // 文件名
        puOptions: {
          id: string;       // 设备名称
          index: number;    // 通道ID
          name?: string;          //设备名称，用于内部UI显示
          indexName?: string;     //通道名称，用于内部UI显示
        },
        //调用将取消自动下载，返回下载函数
        cancelAutoDownload: () => {
          //调用该接口，浏览器下载文件， filename 不填为默认的文件名，否则为[filename].webm
          download(filename?: string): void;
        }
      }
     }) => void)):void;
  },
  rotate: {
    // 获取旋转状态，顺时针方向
    // "Rotate0" 0度 
    // "Rotate90" 90度 
    // "Rotate180" 180度 
    // "Rotate270" 270度
    Get(): {type: "Rotate0" | "Rotate90" | "Rotate180" | "Rotate270"},
    // 设置画面旋转状态
    Set(status: {type: "Rotate0" | "Rotate90" | "Rotate180" | "Rotate270"}): void;
    // 添加事件监听
    addEventListener(handle: ((status: {type: "Rotate0" | "Rotate90" | "Rotate180" | "Rotate270"}) => void)):void;
    // 移除事件监听
    removeEventListener(handle: ((status: {type: "Rotate0" | "Rotate90" | "Rotate180" | "Rotate270"}) => void)):void;
  },
  ptzControl: {
    // 获取云台控制面板显示状态，是否显示
    Get(): {show?: boolean},
    // 设置云台控制面板显示状态
    Set(status: {show?: boolean}): void;
    // 添加事件监听
    addEventListener(handle: ((status: {show: boolean})) => void)):void;
    // 移除事件监听
    removeEventListener(handle: ((status: {show: boolean})) => void)):void;
  }，
  mike: {
    // 获取对讲状态，是否在对讲
    Get(): {talking?: boolean},
    // 设置对讲状态
    Set(status: {talking?: boolean}): void;
    // 添加事件监听
    addEventListener(handle: ((status: {talking?: boolean})) => void)):void;
    // 移除事件监听
    removeEventListener(handle: ((status: {talking?: boolean})) => void)):void;
  }，
  screenshot：{
    //进行截图
    exec(): void;
    // 添加事件监听，获取截图结果, 
    // cancelAutoDownload 函数，调用会取消掉下载图片
    addEventListener(handle: ((
        status: {
        img?: Blob, 
        fileName?: string, 
        cancelAutoDownload?: ()=>{
          download(fileName?: string): void //下载图片
        }
      })) => void)):void;
    // 移除事件监听
    removeEventListener(handle: ((
        status: {
        img?: Blob, 
        fileName?: string, 
        cancelAutoDownload?: ()=>{
          download(fileName?: string): void //下载图片
        }
      })) => void)):void;
  }，
  informationGet： () => Promise<{
    //音频统计信息
    audio?: {
      //编码相关
      codec: {
        //https://developer.mozilla.org/en-US/docs/Web/API/RTCCodecStats/channels
        channels?: number;  //通道数
        //https://developer.mozilla.org/en-US/docs/Web/API/RTCCodecStats/clockRate
        clockRate: number;  //时钟频率
        //https://developer.mozilla.org/en-US/docs/Web/API/RTCCodecStats/mimeType
        mimeType: string;   //mimetype
        //https://developer.mozilla.org/en-US/docs/Web/API/RTCCodecStats/sdpFmtpLine
        sdpFmtpLine?: string;
      };
      //传输相关
      rtp: {
        sampleRates: string;  //采样率
        bitRate: number;      //bit速率
        packageLostTotal: number;       //丢包总数
        avaragePackageLostRate: number; //平均丢包率
        packageLostRate: number;        //瞬时丢包率
      };
    };
    //视频统计信息
    video?: {
      //编码相关
      codec: {
        type: string;         //编码类型 video/H264 video/H265  (webcodec)
        sampleRates: number;  //采样率
        width: number;        //视频宽度
        height: number;       //视频高度
      };
      //传输相关
      rtp: {
        sampleRates: number;  //视频采样率
        bitRate: number;      //bit 率
        packageLostTotal: number;     //总丢包数
        avaragePackageLostRate: number; //平均丢包率
        packageLostRate: number;   //瞬时丢包率
      };
    };

    // 播放相关
    meta: {
      lastSeconds: number;  //持续时间 秒数
      transType: "webrtc" | "bvrtc"; //传输方式
    };     
  }>
}

// clearAllDialog 异步 释放全部打开的dialog 不管是音视频还是对讲
clearAllDialog();
```

- Controls 控制项列表

```typescript
// 全部控制项
[
  "fullscreen",
  "ptzControl",
  "rotate",
  "record",
  "screenshot",
  "volumeSlider",
  "volume",
  "mike",
  "information"
];
// 在创建实例时 传入参数disabledControls数组 可关闭相关功能
// 如不使用全屏按钮、云台控制器
disabledControls: ["fullscreen", "ptzControl"];
```

**对讲**

```typescript
import {
  Intercom,
  IIntercomProps,
  IIntercomInstance,
} from "@besovideo/webrtc-player";
import "@besovideo/webrtc-player/dist/main.es.css";

const { instance } = Intercom({
  // 必填 设备选项
  puOption: {
    // 设备id
    id: "PU_123456",
    // 设备通道号
    index: 0,
  },
  // 必填 用户授权令牌
  token: "Y3UrQ1VfYWRtestYDHYUAStest",
  // (可选) 连接建立后回调
  onConnected: () => {},
  // (可选) 连接建立失败时的回调
  onConnectedFailed: () => {},
  // (可选) 连接断开后回调
  onDisConnected: () => {},
  //(可选) 无法获取用户麦克风时
  onGetUserMediaFailed: () => {},
  // (可选) 指定请求url的前缀 方便代理请求
  apiPrefix: "/test_api/prefix",
});

// open 异步 先关闭再开始建立连接 (可选参数: protocol = type 选择协议(type = "auto" | "webrtc" | "ws-bvrtc") 不填默认为auto)
install.open(type);
```

**会议**

```typescript
import {
  Conference,
  IConferenceProps,
  IConferenceInstance,
} from "@besovideo/webrtc-player";
import "@besovideo/webrtc-player/dist/main.es.css";

const { instance } = Conference({
  // 必填 设备选项
  confOption: {
    // 会议id
    id: "test89312",
    // 会议成员ID
    index: 0,
  },
  // 必填 用户授权令牌
  token: "Y3UrQ1VfYWRtestYDHYUAStest",
  // (可选) 连接建立后回调
  onConnected: () => {},
  // (可选) 连接建立失败时的回调
  onConnectedFailed: () => {},
  // (可选) 连接断开后回调
  onDisConnected: () => {},
  //(可选) 无法获取用户麦克风时
  onGetUserMediaFailed: () => {},
  // (可选) 指定请求url的前缀 方便代理请求
  // apiPrefix: "/test_api/prefix",
});

// open 异步 先关闭再开始建立连接 (可选参数: protocol = type 选择协议(type = "auto" | "webrtc" | "ws-bvrtc") 不填默认为auto)
install.open(type);
```


**视频回放**

```typescript
import {PlayBack} from "@besovideo/webrtc-player";

const { instance } = PlayBack({
  // 必填 文件ID
  fileID: string;
  // 必填 文件名选填，控件下载文件时会使用这个，不填将是 【请填写Filename】
  fileName?: string;
  //必填 登录的token
  token: string;
  // 必填 设备ID 或者 NRUID， 填写PUID就是从设备回放文件，填写NRUID 就是从平台回放
  puOption?: {
      puId?: string;
      nruId?: string;
  },

  // (可选) 内部访问平台接口的路径前缀 
  apiPrefix?: string;
  // (可选)  type 参数为 "ws-bvrtc" 时的播放控制 
  bvrtcConfig?: {
      // (可选) 是否使用webcodecs解码，用于h265解码硬件加速，兼容性没有软解好
      enableWebcodes?: number,
      // (可选) 设置解码实时性 范围 0-10, 0 -- 流畅性最大, 10 -- 实时性最大
      realTimeLevel?: number,
  },
  //（可选） 控制 回放 是否 音频 或者 视频， 默认打开全部轨道
  defaultMediaOption?: { audio: boolean, video: boolean },
  // 必填 父容器传递个div就行了
  container: HTMLDivElement; 

  //（可选）语言 默认中文
  locale?: "zh" | "en",
  // 可选 默认 web 回放方式 "ws-bvrtc" (兼容性强，非原生) | "web"（原生，有些媒体文件播不了）
  type?: "ws-bvrtc" | "web",
  //（可选） 右键菜单的zindex 排序，有时候右键菜单 显示不出来
  zIndexMenu?: number,

  //（可选） 控制是否显示弹幕，默认显示
  displayMode?: 'playback',
  //（可选）播放窗口水印配置，支持全局配置和实例覆盖
  watermark?: {
      text: "测试水印",
  }
})

// 打开回放
instance.open() 

```

**水印配置**

`PuPlayer`、`PlayBack`、`CreateCallEndPoint` 和 `callEndPointUtils.pushLocalMedia` 支持水印层。水印是播放窗口上的 DOM/CSS 叠层，不会写入本地录像文件。

```typescript
import {
  globalConfigHandler,
  PuPlayer,
  PlayBack,
  CreateCallEndPoint,
  callEndPointUtils,
} from "@besovideo/webrtc-player";

interface IWatermarkConfig {
  // 是否启用水印
  enable?: boolean;
  // 水印文本；数组会按多行显示
  text?: string | string[];
  // 水印透明度
  opacity?: number;
  // 字号
  fontSize?: number;
  // 字重
  fontWeight?: number | string;
  // 文字颜色
  color?: string;
  // 描边颜色
  strokeColor?: string;
  // 描边宽度
  strokeWidth?: number;
  // 旋转角度
  rotate?: number;
  // 最小横向间距
  gapX?: number;
  // 最小纵向间距
  gapY?: number;
  // 大屏自适应密度：最多显示的列数
  maxColumns?: number;
  // 大屏自适应密度：最多显示的行数
  maxRows?: number;
  // 可选，水印层级；默认不设置
  zIndex?: number;
}

// 全局配置：影响之后创建的播放器、回放、通话和推流预览实例
globalConfigHandler.setDefaultProps({
  watermark: {
    // 支持 string 或 string[]，数组会按多行显示
    text: ["测试水印", "仅供内部使用"],
    // 是否启用水印，默认 true；设置为 false 可关闭
    enable: true,
    // 水印透明度，默认 0.28
    opacity: 0.28,
    // 字号，默认 16
    fontSize: 16,
    // 字重，默认 normal
    fontWeight: "normal",
    // 文字颜色，默认 #fff
    color: "#fff",
    // 描边颜色，默认 #000，用于提升浅色/复杂画面上的可读性
    strokeColor: "#000",
    // 描边宽度，默认 2
    strokeWidth: 2,
    // 旋转角度，默认 -20
    rotate: -20,
    // 最小横向间距，默认 180
    gapX: 180,
    // 最小纵向间距，默认 120
    gapY: 120,
    // 大屏自适应密度：最多约 4 列，默认 4
    maxColumns: 4,
    // 大屏自适应密度：最多约 3 行，默认 3
    maxRows: 3,
    // 可选，水印层级；默认不设置，位于视频上方、遮罩和控制层下方
    // zIndex: 1,
  },
});

// 单实例覆盖全局配置
PuPlayer({
  // ...
  watermark: {
    text: "当前设备水印",
    maxColumns: 3,
    maxRows: 2,
  },
});

// 单实例关闭水印
PlayBack({
  // ...
  watermark: {
    enable: false,
  },
});

// 通话端点窗口覆盖全局配置，本地和远端视频区域都会显示水印
CreateCallEndPoint({
  // ...
  watermark: {
    text: "通话水印",
  },
});

// 推送本地媒体流预览覆盖全局配置
callEndPointUtils.pushLocalMedia({
  // ...
  watermark: {
    text: "推流预览水印",
  },
});
```

水印间距会根据播放窗口大小自动调整。`gapX` / `gapY` 表示最小间距，`maxColumns` / `maxRows` 用于限制大屏下最多显示的水印密度：

```typescript
实际横向间距 = Math.max(gapX, 播放窗口宽度 / maxColumns)
实际纵向间距 = Math.max(gapY, 播放窗口高度 / maxRows)
```

***媒体流推送***
``` typescript
const callEndPointUtils: {
  pushLocalMedia: (params: {
    // token
    token: string;
    // div 用于渲染
    container?: HTMLDivElement;
    // 播放窗口水印配置，支持全局配置和实例覆盖
    watermark?: IWatermarkConfig;
    // 是否获取视频
    video?: boolean;
    // 是否获取音频
    audio?: boolean;
    // 推窗口流，
    displayMedia?: {
      // 必选
      video: {
          // 窗口选择页面，哪一个优先显示
          displaySurface?: "monitor" | "window" | "application" | "browser";
          // 画面中是否显示鼠标
          cursor?: "always" | "motion" | "never";
      };
      // 是否推窗口音频
      audio?: boolean
      // 停止分享事件通知，此时webrtc流没有关闭，要主动调用close
      onInactive?: () => void;
    };
    // 连接失败
    onClose?: () => void;
    // peer connection 连接状态发生改变
    onStatusChange?: (status: "CONNECTEDING" | "CONNECTED") => void;
    // 试用回调，说明AVcall 没认证
    onTrial?: (info: {
      totalTimeMs: number;   // 单个Call可试用总时长
      currentTimeMs: number; // 当前已试用时间
      isTrialOver: boolean;  // 是否试用结束
    }) => void;
  }) => Promise<{
    // 关闭已经打开的推流, code == 'SUCCESS' 有效
    close?: () => Promise<void>;
    code: "SUCCESS" | "GET_MEDIA_ERROR" | "CREATE_LOCALE_SDP_ERROR" | "SET_LOCAL_SDP_ERROR" | "POST_SDP_ERROR" | "GET_REMOTE_SDP_ERROR" | "SET_REMOTE_SDP_ERROR" | "CREATE_VIDEO_ELE_ERROR" | "WAIT_TRACK_ERROR" | "WAIT_PEER_CONNECTED_ERROR" | "BUSY_ERROR";
    message?: string | undefined;
    // code == 'SUCCESS' 有效， 推送的媒体流信息
    streamIdInfo?: {
        id: string; // https://bvcsp.apifox.cn/api-379317162 返回的 id 设备ID
        index: number; // https://bvcsp.apifox.cn/api-379317162 返回的 index 通道号
        mediaStream: MediaStream; // 媒体流用于播放
        dialogId: string;         // dialog id
    };
  }>
}
```


**Vue 示例**

```vue
<template>
  <div ref="testRef" class="container"></div>
  <div ref="test2Ref" class="container2"></div>
  <button @click="close">关闭</button>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";
import { PuPlayer, IPuPlayerInstance } from "@besovideo/webrtc-player";
import "@besovideo/webrtc-player/dist/main.es.css";

export default defineComponent({
  name: "Test",
  props: {
    // 设备id号
    puId: {
      type: String,
      required: true,
    },
    // 设备通道号
    index: {
      type: Number,
      required: true,
    },
  },
  setup(props) {
    const testRef = ref(null);
    const test2Ref = ref(null);
    const instanceRef = ref<IPuPlayerInstance | null>(null);

    const init = async () => {
      const { instance } = PuPlayer({
        container: testRef.value,
        puOption: { id: props.puId, index: props.index },
        token:
          "Y3UrQ1VfYWRtaW4rYWRtaW4rMTQzOSsxOTMwMTUrMTYyNTIwMjU3OCthN2I0Y2JiZDliMzkwZWUy",
      });
      console.log("初始化播放器完成：", instance);
      instanceRef.value = instance;

      try {
        await instance.open();
      } catch (e) {}

      setTimeout(() => {
        // 测试移动到另一个容器内
        console.log("移动节点");
        instance.moveTo(test2Ref.value);
      }, 5000);
    };

    onMounted(() => {
      init();
    });

    return {
      testRef,
      test2Ref,
      close: () => instanceRef.value?.close(),
    };
  },
});
</script>

<style lang="scss" scoped>
.container {
  height: 300px;
}
</style>
```

**特别说明**

- 项目是原生 JS、JQuery 写的,项目中没有导入导出功能的,可以使用暴露出来的"bvPlayer"实例对象进行使用

- 在项目中引入 besovideo/webrtc-player dist 文件夹下面的"main.browser.css"和"main.browser.js"两个文件
```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <link href="./main.browser.css" type="text/css" rel="stylesheet" />
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="./main.browser.js"/>
    
    <title>Document</title>
  </head>
  <body>
    
    
  </body>

  <script>
    const playerEle = document.createElement("div")
    playerEle.style.height = "300px"
    playerEle.style.width = "300px"
    playerEle.style.backgroundColor = "black"
    document.body.appendChild(playerEle);
      const {instance} = window.bvPlayer.PuPlayer({
        // (可选) 容器节点 注意一个容器内只能存在一个实例  当container为假值（包括false、null、undefined）时 将返回实例引用的dom节点 容器必须指定高度 参考高德地图
        container: playerEle,
        // 必填 设备选项
        puOption: {
          // 设备id
          id: "PU_123456",
          // 设备通道号
          index: 0,
        },
        // 必填 用户授权令牌
        token: "Y3UrQ1VfYWRtestYDHYUAStest",
      });
      instance.open()
    </script>
</html>

```
## 升级 Upgrade

```shell
yarn upgrade @besovideo/webrtc-player@latest

```

## 开发 Dev

```shell
# 开发运行
yarn
yarn start

# 打包和发布（可直接使用 make publish ）
# npm login / yarn login
yarn version
yarn build
yarn publish


```

[环境变量切换方式](https://github.com/entropitor/dotenv-cli/pull/44)
