互动视频
  • iOS
  • Android
  • macOS
  • Windows
  • Linux
  • Web : JavaScript
  • 小程序
  • Electron
  • 概述
  • 限制说明
  • SDK 下载
  • 快速开始
  • 设备管理
  • 常用功能
  • 推拉流进阶
  • 音频进阶
  • 废弃接口
  • API 文档
  • 常见错误码
  • 常见问题
  • 视频直播
  • 视频通话
  • 娃娃机
  • 文档中心
  • 互动视频
  • 视频通话
  • 功能实现流程

功能实现流程

更新时间:2023-11-02 17:35

1 实现流程

以下是简单的SDK调用流程,详细API接口请参考接口文档

1.1 初始化 SDK

// 声明变量
var zg;

// 初始化实例
zg = new ZegoClient();

// 配置必要参数
zg.config({
  appid: appid,         // 必填,应用id,请从 ZEGO 控制台-https://console.zego.im 获取
  idName: idName,       // 必填,用户自定义id,全局唯一
  nickName:  nickName,  // 必填,用户自定义昵称
  remoteLogLevel: 1,     // 上传日志最低级别,建议跟 logLevel 一致
  logLevel: 1,          // 日志级别,debug:0,info:1,warn:2,error:3,report:99,disable:100(数字越大,日志越少),建议选择 1
  server: server        // 必填,接入服务器地址,请从 ZEGO 控制台-https://console.zego.im 获取
  logUrl: logUrl        // 必填,logServer 地址,请从 ZEGO 控制台-https://console.zego.im 获取
});

1.2 获取登录 token

登录 token 的获取详见后文 3 安全方案 中的 3.1 房间登录安全

源码片段如下:


// 获取登录 token
function loadLoginToken() {
        var xmlhttp;
        xmlhttp = new XMLHttpRequest();
        xmlhttp.onreadystatechange = function() {
            if (xmlhttp.readyState == 4) {
                if (xmlhttp.status == 200) {
                    trace("login token success:" + xmlhttp.responseText);
                    loginToken = xmlhttp.responseText;
                    doLogin();
                }
                else {
                    trace("login token failed");
                    alert("获取登录信息失败");
                }
            }
        };

        //从开发者后台获取token
        xmlhttp.open("GET", loginTokenUrl + "?app_id=" + appid + "&id_name=" + idName, true);
        xmlhttp.send();
    }

1.3 登录房间

登录房间成功是后续信令操作的前提。源码片段如下,仅供参考:

zg.login(roomId, 2, loginToken, function(streamList) {
    //登录成功处理逻辑
}, function(err) {
    alert("login error: " + err.msg);
});

登录错误码列表如下:

错误码 含义
ZegoClient.Success 成功
ZegoClient.Error.Param 参数错误
ZegoClient.Error.Timeout 超时
ZegoClient.Error.Network 网络错误
ZegoClient.Error.Kickout 用户被踢
ZegoClient.Error.Server 服务返回错误
ZegoClient.Error.Unknown 未知错误

1.4 枚举设备

SDK提供接口可以枚举当前的麦克风,摄像头,扬声器设备。源代码片段如下,仅供参考:

zg.enumDevices(function(deviceInfo) {
    if (deviceInfo.microphones) {
        for (var i = 0; i < deviceInfo.microphones.length; i++) {
            trace("microphone: " + deviceInfo.microphones[i].label);
        }
    }

    if (deviceInfo.cameras) {
        for (var j = 0; j < deviceInfo.cameras.length; j++) {
            trace("camera: " + deviceInfo.cameras[i].label);
        }
    }

});

1.5 本地预览

在开播前可以预览当前画面,可以指定麦克风和摄像头的设备Id,也可以使用默认设备。源代码片段如下,仅供参考:

function doPreview(audioInput, videoInput) {
    var avConstraints = {
        audio: true,
        audioInput: audioInput,
        video: true,
        videoInput: videoInput,
        videoQuality: videoQuality,
        horizontal: true
    };

     //WebRTC可以共用相同设备,所以预览时必须指定开播的streamId
    zg.startPreview( localVideo, avConstraints, function() {
        trace("preview success");

        doPublish();
    }, function(error) {
        alert("start device error " , error);
    });
}
  • 使用默认设备时,audioInput和videoInput参数不用填
  • 推纯音频或纯视频时,只需将对应的video或audio设为false,然后在播放拉流接口中传入第四个播放模式参数,例:zg.startPlayingStream(streamId, video,null,{playType:'audio'});

1.6 开始直播

开始直播前必须先开始预览。源代码片段如下,仅供参考:

function doPublish() {
    var result = zg.startPublishingStream(publishStreamId, localVideo);
    if (!result) {
        alert("publish " + publishStreamId + " return " + result);
    }
}

1.7 直播状态回调

直播开始,直播失败,重试直播都通过此回调通知。源代码如下,仅供参考:

zg.onPublishStateUpdate = function(type, streamid, error) {
    if (type == 0) {
        trace("publish " + streamid + " success");
    }
    else if (type == 2) {
        trace("publish " + streamid + " retry");
    }
    else {
        // trace("publish " + streamid + "error " + error.code);
        alert("publish " + streamid + " error " + error.msg);
    }
};

1.8 开始拉流

指定播放流的<video标签,画面会渲染在指定的<video>标签里。源代码如下,仅供参考:

function doPlay(streamid) {
    trace("play " + streamid);
    var remoteVideo = document.createElement("video");
    remoteVideo.setAttribute("autoplay", "");
    remoteVideo.setAttribute("webkit-playsinline", "");

    var videos = util.getById("videos");
    videos.appendChild(remoteVideo);

    remoteVideoMap[streamid] = remoteVideo;

    var result = zg.startPlayingStream(streamid, remoteVideo);
    if (!result) {
        alert("play " + streamid + " return " + result);
    }
}

1.9 拉流状态回调

拉流开始,拉流失败,拉流重试都通过此回调通知。源代码如下,仅供参考:

zg.onPlayStateUpdate = function(type, streamid, error) {
    if (type == 0) {
        trace("play " + streamid + " success");
    }
    else if (type == 2) {
        trace("play " + streamid + " retry");
    }
    else {
        // trace("publish " + streamid + "error " + error.code);
        alert("play " + streamid + " error " + error.msg);
    }
};

2 屏幕共享

2.1 使用说明

  • web屏幕共享功能,目前只支持桌面端(例如window和mac)的chrome和火狐浏览器,其中chrome需要下载即构共享插件

  • 分享功能只能获取到系统扬声器声音,外部麦克风需另外推流(栗子:电脑播放音乐可以采集到,对着电脑说话声音采集不到)

  • 当同时推多路流时,建议音频只推一路,防止回音

2.2 插件安装

  • 点击下载即构共享插件,并解压;

  • 打开你的 Chrome 浏览器,点击屏幕右上方的扩展按钮,选择 更多工具 > 扩展程序, 打开开发者模式 > 加载已解压的扩展程序 > 选择 解压的 即构共享插件文件夹,即可完成安装

2.3 方法调用

火狐获取屏幕区域需要在代码中传入参数,chrome则会以弹窗形式给用户提供选择

2.3.1 startScreenShotFirFox

火狐获取屏幕共享媒体流

参数 含义 类型
mediaSource 屏幕共享区域 只能是'screen', 'application', 'window'中一个
auido 是否捕捉声音 bool
callback(suc,mediaStream) 获取共享媒体流回调 suc是bool类型代表成功与否,mediaStream为获取到的媒体流

screen 代表整个屏幕;application 代表某个应用; window 代表某个应用的某个窗口

2.3.2 startScreenShotChrome

chrome获取屏幕共享媒体流

参数 含义 类型
callback(suc,mediaStream) 获取共享媒体流回调 suc是bool类型代表成功与否,mediaStream为获取到的媒体流

2.3.3 stopScreenShot

stopScreenShot( )

2.4推流示例(chrome)

   zg.startScreenShotChrome(function (suc,mediastream) {

       previewVideo.srcObject = mediastream;//本地预览

       // 与正常推流一样,需要先调用sdk预览接口,成功后再推流 ,
       // 需要注意的是  mediaStreamConstraints.externalCapture = true (必须)          
       zg.startPreview(localVideo, mediaStreamConstraints, function() {

         zg.startPublishingStream(streamid, localVideo, extraInfo);

       }, error)

    })

[更多github示例]

3 安全方案

3.1 房间登录安全

3.1.1 基本流程

  1. JS与业务后台建立通信,获取 Token 信息。
  2. JS调用 ZegoClient.login 登录 Zego 服务器,传入 Token 信息,验证通过后,完成登录。
  3. ZegoClient 会保持与 Zego 服务器的长连接,处理发送或接收的消息。
  4. JS调用 ZegoClient.logout 登出 Zego 服务器。

请注意:

生成 Token 信息需要业务后台自行开发。

3.1.2 login_token 信息

  • login_token 信息为标准 json 格式 字符串 ,具体为:
{
    "ver": 1,
    "hash": xxxxx,
    "nonce": xxxxx,
    "expired": xxxxx,
}

字段说明如下:

参数名 类型 说明
ver int 版本号,填 1 即可
hash String hash = MD5(app_id+app_sign_32+id_name+nonce+expired)
nonce String 随机串,需要保证同一 user_id 在失效时间内不重复,建议按 guid 生成
expired int64 失效时间,unix_timestamp;单位:秒

expired为失效时间点,需要用当前时间加上失效时长

对于 hash 计算中使用字段的更详细说明:

参数名 类型 说明
app_id int ZEGO 分配给客户的 app_id,App 唯一标识
app_sign_32 String 通过 app_sign 运算获得。算法:剔除 app_sign 里的 "0x", "," 字符后,获取前面 32 字节即为 app_sign_32(具体算法可参考下述代码)
id_name String 与初始化时config里设置的idName保持一致
nonce String 随机串,与 json 中的 nonce 一致
expired int64 失效时间,与 json 中的 expired 一致

请注意:

  1. login_token 传输过程中,会经过 base64 加密。

  2. 每次登录都要重新获取 login_token。

  3. 业务方开发的JS需要和业务后台建立一种安全通讯和鉴权机制,业务方使用自有的账户体系或第三方认证体系的登录完成后,业务方JS和业务后台交互获取该 login_token, AppSecret 是存储在业务后台的。

3.2 login_token 生成示例代码

node.js语言 login_token 生成示例代码如下:

   /**
         * 拉流端获取登录token
         * @param appId  即构分配的appId
         * @param appSign 即构分配的appSign
         * @param idName 这里的idname需要和websdk前端传入的idname一致,
         // 否则校验失败(因为这里的idname是为了校验和前端传进来的idname是否一致)
         * @return
         */
        function getZeGouToken(appId, appSign, idName) {

                let nonce = new Date ().getTime ().toString ();
                let time = Math.floor (new Date ().getTime () / 1000 + 30 * 60);
                let appSign32 = appSign.replace (/0x/g, '').replace (/,/g, '').substring (0, 32);
                console.log ('appSign:' + time + '    ' + appSign32 + '    ' + nonce);

                if (appSign32.length < 32) {
                        console.log ('private sign erro!!!!');
                        return null;
                }

                let sourece = md5 (appId + appSign32 + idName + nonce + time);
                console.log ('hash:' + sourece);

                let jsonStr = JSON.stringify({
                        'ver': 1,
                        'expired': time,
                        'nonce': nonce,
                        'hash': sourece
                });
                console.log ('json', jsonStr);
                return   Buffer.from(jsonStr).toString('base64');
        }

go 语言 login_token 生成示例代码如下:

func makeTokenSample(appid uint32, app_sign string, idname string, expired_add int64) (ret string, err error){
    nonce := UniqueId()
    expired := time.Now().Unix() + expired_add      //单位:秒

    app_sign = strings.Replace(app_sign, "0x","",-1)
    app_sign = strings.Replace(app_sign, ",", "", -1)
    if len(app_sign) < 32 {
      return "", fmt.Errorf("app_sign wrong")
    }

    app_sign_32 := app_sign[0:32]
    source := fmt.Sprintf("%d%s%s%s%d",appid,app_sign_32,idname,nonce,expired)
    sum := GetMd5String(source)

    token := tokenInfo{}
    token.Ver = 1
    token.Hash = sum
    token.Nonce = nonce
    token.Expired = expired

    buf, err := json.Marshal(token)
    if err != nil {
    return "", err
    }

    encodeString := base64.StdEncoding.EncodeToString(buf)
    return encodeString, nil
}

//  获取MD5加密
func GetMd5String(s string) string {
    h := md5.New()
    h.Write([]byte(s))
    return hex.EncodeToString(h.Sum(nil))
}

php 语言 login_token 生成示例代码如下:

public function getToken(int $app_id, string $app_sign, string $idname, int $expired_add)
{
    $nonce = uniqid();
    $expired = time() + $expired_add; //单位:秒

    $app_sign = str_replace("0x", "", $app_sign);
    $app_sign = str_replace(",", "", $app_sign);
    if(strlen($app_sign) < 32) {
        return false;
    }
    $app_sign_32 = substr($app_sign, 0, 32);

    $source = $app_id.$app_sign_32.$idname.$nonce.$expired;
    $sum = md5($source);

    $tokenInfo = [
        'ver' => 1,
        'hash'  => $sum,
        'nonce' => $nonce,
        'expired' => $expired,
    ];
    $token = base64_encode(json_encode($tokenInfo));
    return $token;
}

java 语言 login_token 生成示例代码如下:

package demo;

import org.json.JSONObject;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.UUID;
import org.apache.commons.codec.binary.Base64;


public class ZegouUtils {

    public static void main(String[] args) {
        String appid = "0000000000";  //即构分配的appId
        String appSign = "0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00";  //即构分配的appSign
        String idName = "xxxxxxx"; //业务系统用户唯一标识
      String Token = getZeGouToken(appid,appSign,idName);
      System.out.println("--Token--:"+Token);
  }

    /**
     * 拉流端获取登录token
     * @param appId  即构分配的appId
     * @param appSign 即构分配的appSign
     * @param idName 业务系统用户唯一标识
     * @return
     */
    public static String getZeGouToken(String appId,String appSign,String idName){

        String nonce= UUID.randomUUID().toString().replaceAll("-", "");
        long time=new Date().getTime()/1000+30*60;
        String appSign32=new String(appSign.replace("0x", "").replace(",", "").substring(0, 32));
        System.out.println("appSign:"+time+"    "+appSign32+"    "+nonce);

        if(appSign32.length()<32){
            System.out.println("private sign erro!!!!");
            return null;
        }

        String sourece= getPwd(appId+appSign32+idName+nonce+time);
        System.out.println("hash:"+sourece);

        JSONObject json=new JSONObject();
        json.put("ver", 1);
        json.put("hash", sourece);
        json.put("nonce", nonce);
        json.put("expired",time); //unix时间戳,单位为秒
        org.apache.commons.codec.binary.Base64 base64 = new org.apache.commons.codec.binary.Base64();
        System.out.println("json"+json.toString());
        return base64.encodeAsString(json.toString().getBytes());
    }


    /**
     * 获取MD5加密
     * @param pwd 需要加密的字符串
     * @return String字符串 加密后的字符串
     */
    public static String getPwd(String pwd) {
        try {
            // 创建加密对象
            MessageDigest digest = MessageDigest.getInstance("md5");

            // 调用加密对象的方法,加密的动作已经完成
            byte[] bs = digest.digest(pwd.getBytes());
            // 接下来,我们要对加密后的结果,进行优化,按照mysql的优化思路走
            // mysql的优化思路:
            // 第一步,将数据全部转换成正数:
            String hexString = "";
            for (byte b : bs) {
                // 第一步,将数据全部转换成正数:
                int temp = b & 255;
                // 第二步,将所有的数据转换成16进制的形式
                // 注意:转换的时候注意if正数>=0&&<16,那么如果使用Integer.toHexString(),可能会造成缺少位数
                // 因此,需要对temp进行判断
                if (temp < 16 && temp >= 0) {
                    // 手动补上一个“0”
                    hexString = hexString + "0" + Integer.toHexString(temp);
                } else {
                    hexString = hexString + Integer.toHexString(temp);
                }
            }
            return hexString;
        } catch (NoSuchAlgorithmException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return "";
    }

}

4 常见错误

4.1 登录房间时,控制台输出返回ZegoClient.Error.Timeout

解决方案: 请检查 初始化配置 ZegoClient.config 里面每个参数的类型是否正确,一般情况下都是类型不正确引起的问题。

4.2 登录房间时,控制台输出返回result=1000001002

原因: 观众角色不允许创建房间,也就是这个房间不存在。

解决方案:

1)在ZegoClient.config里面的option.audienceCreateRoom设置为TRUE

2)在ZegoClient.login里面将role设置为1,主播角色。

4.3 登录房间时,控制台输出返回token Error的报错

原因:计算token的时候,算法不对引起,可通过这个链接:http://sig-wstoken.zego.im/tokenindex 来验证是否正确:

  1. 首先验证 hash 字段是否有错,这里要注意 id_name 是string类型,不是数值类型; expired 是 uninx 的时间戳,不是北京时间。

  2. login_token由业务侧后台生成后,将json里面的字段 经过base64加密 后再下发给web端。

详情请参考: 4.1 房间登录安全

本篇目录