OWT Server信令分析 (上) [Open WebRTC Toolkit]

OWT(Open WebRTC Toolkit) Server信令分析 (上)


目录

  1. OWT Server信令分析
  2. OWT Server信令交互过程解析

信令分析因为包含一些代码和格式,文章很长,所以分成上下两篇记录,OWT(Open WebRTC Toolkit)相关文章
相关文章:

  1. Ubuntu环境安装OWT Server[Open WebRTC Toolkit]
  2. Docker环境安装OWT Server[Open WebRTC Toolkit]
  3. OWT Server整体架构分析 [Open WebRTC Toolkit]
  4. OWT Server信令分析 (上) [Open WebRTC Toolkit]
  5. OWT Server信令分析 (下) [Open WebRTC Toolkit]
  6. OWT Server进程结构和JS代码处理流程 [Open WebRTC Toolkit]
  7. OWT Server REST API

1. OWT Server信令分析

  1. OWT信令协议分为RESTful API和SocketIO长连接两部分,RESTful API由Management API提供,SocketIO长连接则由WebRTC Portal提供。
  2. 不过不是直接调用Management API,而是调用Conference Sample Server的接口,它是对Management API的一个封装,源码在owt-client-javascript https://github.com/open-webrtc-toolkit/owt-client-javascript/tree/master/src/samples/conference项目中。
  3. OWT Server WebRTC的信令交互过程如下:
A POST /tokens/
A SocketIO connect
A SocketIO login
A SocketIO publish
A SocketIO soac offer
A SocketIO soac candidate
Portal SocketIO soac answer

B POST /tokens/
B SocketIO connect
B SocketIO login
B SocketIO subscribe
B SocketIO soac offer
B SocketIO soac candidate
Portal SocketIO soac answer

SocketIO logout

2. OWT Server信令交互过程解析

  1. 以owt-client-javascript作为客户端,owt-server作为服务端为例。
  2. owt server安装完成后,可以在https://localhost:3004或者https://ip:3004看到通话界面。
  3. owt-client-javascript入口函数为src/samples/conference/public/scripts/index.js的window.onload(通话界面按F12可见),简化代码如下:
    window.onload = function() {
        var simulcast = getParameterByName('simulcast') || false;
        var shareScreen = getParameterByName('screen') || false;
        myRoom = getParameterByName('room');
        var isHttps = (location.protocol === 'https:');
        var mediaUrl = getParameterByName('url');
        var isPublish = getParameterByName('publish');
        createToken(myRoom, 'user', 'presenter', function(response) {
            var token = response;
            conference.join(token).then(resp => {
                ...
            }, function(err) {
                ...
            });
        }, serverUrlBase);
    };
  1. 可以看到,客户端首先会创建token,拿到token后再加入会议室。

1. HTTP POST请求创建token

  1. 用户加入房间时,会发送POST请求让服务端返回token字符串。
  2. POST ${host}/v1/rooms/{roomId}/tokens
  3. request body:
object(TokenRequest):
{
    room: string,     // 可选,为空时会传入事先创建好的房间名为sampleRoom的roomId
    preference: object(Preference),     // Preference of this token would be used to connect through, refers to Data Model.
    user: string,     // Participant's user defined ID
    role: string      // Participant's role
}
object(Preference):
{
    isp: string,
    region: string
}

示例:
{
  "user": "user_a",
  "role": "presenter",
  "room": "5dca71a45778c64ff39d9485"
}
  1. 其中:
    1. user字段为用户id。
    2. role字段的取值含义由服务端配置,表明用户能否发布和订阅音视频流,在source/data_access/defaults.js中定义了如下默认的role:
      1. 使用presenter,表示音频和视频都能发布、订阅。
const DEFAULT_ROLES = [
  {
    role: 'presenter',
  publish: { audio: true, video: true },
  subscribe: { audio: true, video: true }
  },
  {
    role: 'viewer',
  publish: {audio: false, video: false },
  subscribe: {audio: true, video: true }
  },
  {
    role: 'audio_only_presenter',
  publish: {audio: true, video: false },
  subscribe: {audio: true, video: false }
  },
  {
    role: 'video_only_viewer',
  publish: {audio: false, video: false },
  subscribe: {audio: false, video: true }
  },
  {
    role: 'sip',
  publish: { audio: true, video: true },
  subscribe: { audio: true, video: true }
  }
];
  1. response body为加密后的token字符串,示例:
eyJ0b2tlbklkIjoiNjM2MjE2YWExNmY5ZWI0MjkxNjMwY2RiIiwiaG9zdCI6ImludHJhLXJ0Yy5zbWFydGVkdS5sZW5vdm8uY29tOjgwODAiLCJzZWN1cmUiOnRydWUsInNpZ25hdHVyZSI6Ik56YzBNamRsWWpNMk5EQXdOV0ZpWkRNME1HUXdPRE16TjJFek1XUTFNMlU0TVRjeU5UQTFaall6TnpGaU1UQXdNVFJsTkRRMU1ESXhaV05tTm1NMVpBPT0ifQ==

1. 客户端相关代码

  1. 客户端相关代码:
    1. 位置:src/samples/conference/public/scripts/rest-sample.js
var createToken = function (room, user, role, callback, host) {
    var body = {
        room: room,
        user: user,
        role: role
    };
    send('POST', '/tokens/', body, callback, host);
};
  1. 客户端发送的POST请求不是直接调用Management API,而是调用Conference Sample Server的接口,它是对Management API的一个封装,源码在owt-client-javascript https://github.com/open-webrtc-toolkit/owt-client-javascript/tree/master/src/samples/conference项目中。
  2. Conference Sample Server相关代码:
    1. 位置:src/samples/conference/samplertcservice.js
app.post('/tokens', function(req, res) { 
  'use strict'; 
  var room = req.body.room || sampleRoom, // sampleRoom会自动创建,见下文
    user = req.body.user,
    role = req.body.role;

  //Note: The actual *ISP* and *region* information should be retrieved from the *req* object and filled in the following 'preference' data.
  var preference = {isp: 'isp', region: 'region'};
  icsREST.API.createToken(room, user, role, preference, function(token) {
    res.send(token);
  }, function(err) {
    res.status(401).send(err);
  });
});
  1. 会调用createToken方法,将请求转发给Management API:
    1. 位置:src/sdk/rest/API.js
  /**
     * @function createToken
     * @desc This function creates a new token when a new participant to a room needs to be added.
     * @memberOf OWT_REST.API
     * @param {string} room                          -Room ID
     * @param {string} user                          -Participant's user ID
     * @param {string} role                          -Participant's role
     * @param {object} preference                    -Preference of this token would be used to connect through
     * @param {function} callback                    -Callback function on success
     * @param {function} callbackError               -Callback function on error
     * @example
  var roomId = '51c10d86909ad1f939000001';
  var user = '[email protected]';
  var role = 'guest';
  // Only isp and region are supported in preference currently, please see server's document for details.
  var preference = {isp: 'isp', region: 'region'};
  OWT_REST.API.createToken(roomId, user, role, preference, function(token) {
    console.log ('Token created:' token);
  }, function(status, error) {
    // HTTP status and error
    console.log(status, error);
  });
     */
  var createToken = function(room, user, role, preference, callback, callbackError) {
    if (typeof room !== 'string' || typeof user !== 'string' || typeof role !== 'string') {
      if (typeof callbackError === 'function')
        callbackError(400, 'Invalid argument.');
      return;
    }
    send('POST', 'rooms/' + room + '/tokens/', {preference: preference, user: user, role: role}, callback, callbackError);
  };

2. 服务端相关代码

  1. owt server服务端中Management API接收请求:
    2. 位置:source/management_api/resource/v1/index.js
//Create token.
router.post('/rooms/:room/tokens', tokensResource.create);

/*
 * Post Token. Creates a new token for a determined room of a service.
 */
exports.create = function (req, res, next) {
    var authData = req.authData;
    authData.user = (req.authData.user || (req.body && req.body.user));
    authData.role = (req.authData.role || (req.body && req.body.role));
    var origin = ((req.body && req.body.preference) || {isp: 'isp', region: 'region'});

    generateToken(req.params.room, authData, origin, function (tokenS) {
        if (tokenS === undefined) {
            log.info('Name and role?');
            return next(new e.BadRequestError('Name or role not valid'));
        }
        if (tokenS === 'error') {
            log.info('RequestHandler does not respond');
            return next(new e.CloudError('Failed to get portal'));
        }
        log.debug('Created token for room ', req.params.room, 'and service ', authData.service._id);
        res.send(tokenS);
    });
};
  1. 会调用generateToken函数生成token字符串,代码如下:
/*
 * Generates new token.
 * The format of a token is:
 * {tokenId: id, host: erizoController host, signature: signature of the token};
 */
var generateToken = function(currentRoom, authData, origin, callback) {
    const databaseGenerateToken = function(token) {
        return new Promise((resolve, reject) => {
            dataAccess.token.create(token, (id) => {
                if (id) {
                    resolve(id);
                } else {
                    reject(new Error('Failed to get token ID.'));
                }
            });
        }).then(id => {
            return getTokenString(id, token);
        });
    };

    var currentService = authData.service,
        user = authData.user,
        role = authData.role,
        r,
        tr,
        token,
        tokenS;

    if (user === undefined || user === '') {
        callback(undefined);
        return;
    }

    if (!authData.room.roles.find((r) => (r.role === role))) {
        callback(undefined);
        return;
    }

    token = {};
    token.user = user;
    token.room = currentRoom;
    token.role = role;
    token.service = currentService._id;
    token.creationDate = new Date();
    token.origin = origin;
    token.code = Math.floor(Math.random() * 100000000000) + '';

    // Values to be filled from the erizoController
    token.secure = false;

    requestHandler.schedulePortal (token.code, origin, function (ec) {
        if (ec === 'timeout') {
            callback('error');
            return;
        }

        if(ec.via_host !== '') {
            if(ec.via_host.indexOf('https') == 0) {
                token.secure = true;
                token.host = ec.via_host.substr(8);
            } else {
                token.secure = false;
                token.host = ec.via_host.substr(7);
            }

        } else {
            token.secure = ec.ssl;
            if (ec.hostname !== '') {
                token.host = ec.hostname;
            } else {
                token.host = ec.ip;
            }

            token.host += ':' + ec.port;
        }

/* 相关对象示例,方便理解: 
token:  {
  user: 'user',
  room: '62b95bd27ff8054480cbb73a',
  role: 'presenter',
  service: '62b95b9166c14841ed16f845',
  creationDate: '2022-11-04T09:09:11.650Z',
  origin: { isp: 'isp', region: 'region' },
  code: '37024002700',
  secure: true,
  host: '52.81.xxx.190:8080'
} , 
ec:  {
  ip: '52.81.xxx.190',
  hostname: '',
  port: 8080,
  via_host: '',
  ssl: true,
  state: 2,
  max_load: 0.85,
  capacity: { isps: [], regions: [] }
}
*/
        if (!global.config.server.enableWebTransport){
            databaseGenerateToken(token).then(tokenS => {
                callback(tokenS);
            });
        } else {
            // TODO: Schedule QUIC agent and portal parallelly.
            requestHandler.scheduleQuicAgent(token.code, origin, info => {
                if (info !== 'timeout') {
                    let hostname = info.hostname;
                    if (!hostname) {
                        hostname = info.ip;
                    }
                    // TODO: Rename "echo".
                    token.webTransportUrl = 'https://' + hostname + ':' + info.port + '/';
                }
                databaseGenerateToken(token).then(tokenS => {
                    callback(tokenS);
                });
            });
        }
    });
};
  1. generateToken函数中,token对象包含内容有:
    1. 用户名:user
    2. 房间id:roomId
    3. 权限:role
    4. serviceId:serviceid
    5. 创建时间戳:creationData
    6. 偏好:origin
    7. 随机数:code
  2. requestHandler.schedulePortal函数对WebRTC Portal进行调度,提供客户端的请求数据(互联网服务提供商ISP和地域),要求分配一个WebRTC Portal的地址。
    1. 调度逻辑后续分析。
  3. 生成token字符串代码逻辑如下:
    1. 位置:source/management_api/resource/v1/tokensResource.js
var getTokenString = function (id, token) {
    return dataAccess.token.key().then(function(serverKey) {
        var toSign = id + ',' + token.host + ',' + token.webTransportUrl,
            hex = crypto.createHmac('sha256', serverKey).update(toSign).digest('hex'),
            signed = Buffer.from(hex).toString('base64'),

            tokenJ = {
                tokenId: id,
                host: token.host,
                secure: token.secure,
                webTransportUrl: token.webTransportUrl,
                signature: signed
            },
            tokenS = Buffer.from(JSON.stringify(tokenJ)).toString('base64');

        return tokenS;

    }).catch(function(err) {
        log.error('Get serverKey error:', err);
    });
};
  1. token示例,方便理解:
token:
{
  user: 'user',
  room: '62b95bd27ff8054480cbb73a',
  role: 'presenter',
  service: '62b95b9166c14841ed16f845',
  creationDate: '2022-11-04T09:09:11.650Z',
  origin: { isp: 'isp', region: 'region' },
  code: '37024002700',
  secure: true,
  host: '52.81.201.190:8080'
} 

tokenJ:
{
  tokenId: '6364d6b7fd05c63e3b648f4c',
  host: '52.81.201.190:8080',
  secure: true,
  webTransportUrl: 'undefined',
  signature: 'OWZiYWY3NGY5ZWZjZWIxZjlkN2Y0MWMwMzliY2E1YTc1N2VjNzU3ZmQ5Y2QzNGJmNDIzNmIxYzFkOTU3NjIxZA=='
}

tokenS:
eyJ0b2tlbklkIjoiNjM2NGQ2YjdmZDA1YzYzZTNiNjQ4ZjRjIiwiaG9zdCI6IjUyLjgxLjIwMS4xOTA6ODA4MCIsInNlY3VyZSI6dHJ1ZSwic2lnbmF0dXJlIjoiT1daaVlXWTNOR1k1WldaalpXSXhaamxrTjJZME1XTXdNemxpWTJFMVlUYzFOMlZqTnpVM1ptUTVZMlF6TkdKbU5ESXpObUl4WXpGa09UVTNOakl4WkE9PSJ9

3. 关于sampleRoom

  1. Conference Sample Server接收tokens请求时,如果request body没有携带roomId,会将默认的sampleRoom的roomId赋予。
    1. 位置:src/samples/conference/samplertcservice.js
app.post('/tokens', function(req, res) { 
  'use strict'; 
  var room = req.body.room || sampleRoom, // sampleRoom会自动创建,见下文
    user = req.body.user,
    role = req.body.role;

  //Note: The actual *ISP* and *region* information should be retrieved from the *req* object and filled in the following 'preference' data.
  var preference = {isp: 'isp', region: 'region'};
  icsREST.API.createToken(room, user, role, preference, function(token) {
    res.send(token);
  }, function(err) {
    res.status(401).send(err);
  });
});
  1. 在启动时就会自动创建一个房间名为"sampleRoom"的房间,相关代码:
var sampleRoom;
var pageOption = { page: 1, per_page: 100 };
(function initSampleRoom () {
  // 获取room列表,判断room列表中是否有room name为sampleRoom,有则获取roomId
  icsREST.API.getRooms(pageOption, function(rooms) {
    console.log(rooms.length + ' rooms in this service.');
    for (var i = 0; i < rooms.length; i++) {
      if (sampleRoom === undefined && rooms[i].name === 'sampleRoom') {
        sampleRoom = rooms[i]._id;
        console.log('sampleRoom Id:', sampleRoom);
      }
      if (sampleRoom !== undefined) {
        break;
      }
    }
    // 没有sampleRoom,则创建,创建成功后返回roomId
    var tryCreate = function(room, callback) {
      var options = {};
      icsREST.API.createRoom(room.name, options, function(roomId) {
        console.log('Created room:', roomId._id);
        callback(roomId._id);
      }, function(status, err) {
        console.log('Error in creating room:', err, '[Retry]');
        setTimeout(function() {
          tryCreate(room, options, callback);
        }, 100);
      }, room);
    };

    var room;
    if (!sampleRoom) {
      room = {
        name: 'sampleRoom'
      };
      tryCreate(room, function(Id) {
        sampleRoom = Id;
        console.log('sampleRoom Id:', sampleRoom);
      });
    }
  }, function(stCode, msg) {
    console.log('getRooms failed(', stCode, '):', msg);
  });
})();
  1. 获取房间列表接口为:GET ${host}/v1/rooms
    1. 见:
  2. request body:null
  3. response body:
object(Room):
{
  _id: string,
  name: string,                        // name of the room
  participantLimit: number,            // -1 means no limit
  inputLimit: number,                  // the limit for input stream
  roles: [ object(Role) ],             // role definition
  views: [ object(View) ],             // view definition, represents a mixed stream, empty list means no mixing
  mediaIn: object(MediaIn),            // the input media constraints allowed for processing
  mediaOut: object(MediaOut),          // the output media constraints
  transcoding: object(Transcoding),    // the transcoding control
  notifying: object(Notifying),        // notification control
  selectActiveAudio: boolean,          // select 3 most active audio streams for the room
  sip: object(Sip)                     // SIP configuration
}

object(Role): {
  role: string,        // name of the role
  publish: {
    video: boolean,    // whether the role can publish video
    audio: boolean     // whether the role can publish audio
  },
  subscribe: {
    video: boolean,    // whether the role can subscribe video
    audio: boolean     // whether the role can subscribe audio
  }
}

object(View): {
  label: string,
  audio: object(ViewAudio),           // audio setting for the view
  video: object(ViewVideo) | false    // video setting for the view
}

object(ViewAudio): {
  format: {                // object(AudioFormat)
    codec: string,         // "opus", "pcmu", "pcma", "aac", "ac3", "nellymoser"
    sampleRate: number,    // "opus/48000/2", "isac/16000/2", "isac/32000/2", "g722/16000/1"
    channelNum: number     // E.g "opus/48000/2", "opus" is codec, 48000 is sampleRate, 2 is channelNum
  },
  vad: boolean             // whether enable Voice Activity Detection for mixed audio
}

object(ViewVideo):{
  format: {                                   // object(VideoFormat)
    codec: string,                            // "h264", "vp8", "h265", "vp9"
    profile: string                           // For "h264" output only, CB", "B", "M", "H"
  },
  parameters: {
    resolution: object(Resolution),           // valid resolutions see [media constraints](#6-Media-Constraints)
    framerate: number,                        // valid values in [6, 12, 15, 24, 30, 48, 60]
    bitrate: number,                          // Kbps
    keyFrameInterval: number,                 // valid values in [100, 30, 5, 2, 1]
  },
  maxInput: number,                           // input limit for the view, positive integer
  bgColor: {
    r: 0 ~ 255,
    g: 0 ~ 255,
    b: 0 ~ 255
  },
  motionFactor: number,                       // float, affact the bitrate
  keepActiveInputPrimary: boolean,            // keep active audio's related video in primary region in the view
  layout: {
    fitPolicy: string,                        // "letterbox" or "crop".
    templates: {
      base: string,                           // template base, valid values ["fluid", "lecture", "void"].
      custom: [                               // user customized layout applied on base
        { region: [ object(Region) ] },       // a region list of length K represents a K-region-layout
        { region: [ object(Region) ] }        // detail of Region refer to the object(Region)
      ]
     }
  }
}

object(Resolution): {
  width: number,    // resolution width
  height: number    // resolution height
}

object(Region): {
  id: string,
  shape: string,      // "rectangle"
  area: {
    left: number,     // the left corner ratio of the region, [0, 1]
    top: number,      // the top corner ratio of the region, [0, 1]
    width: number,    // the width ratio of the region, [0, 1]
    height: number    // the height ratio of the region, [0, 1]
  }
}

object(MediaIn): {
  audio: [ object(AudioFormat) ]// Refers to the AudioFormat above.
  video: [ object(VideoFormat) ]      // Refers to the VideoFormat above.
}

object(MediaOut): {
  audio: [ object(AudioFormat) ],       // Refers to the AudioFormat above.
  video: {
    format: [ object(VideoFormat) ],    // Refers to the VideoFormat above.
    parameters: {
      resolution: [ string ],           // Array of resolution.E.g. ["x3/4", "x2/3", ... "cif"]
      framerate: [ number ],            // Array of framerate.E.g. [5, 15, 24, 30, 48, 60]
      bitrate: [ number ],              // Array of bitrate.E.g. [500, 1000, ... ]
      keyFrameInterval: [ number ]      // Array of keyFrameInterval.E.g. [100, 30, 5, 2, 1]
    }
  }
}

object(Transcoding): {
  audio: boolean,                  // if allow transcoding format(opus, pcmu, ...) for audio
  video: {
    parameters: {
      resolution: boolean,         // if allow transcoding resolution for video
      framerate: boolean,          // if allow transcoding framerate for video
      bitrate: boolean,            // if allow transcoding bitrate for video
      keyFrameInterval: boolean    // if allow transcoding KFI for video
    },
    format: boolean                // if allow transcoding format(vp8, h264, ...) for video
  }
}

object(Notifying): {
  participantActivities: boolean,    // whether enable notification for participantActivities
  streamChange: boolean              // whether enable notification for streamChange
}

object(Sip): {
  sipServer: string,    // host or IP address for the SIP server
  username: string,     // username of SIP account
  password: string      // password of SIP account
}
  1. object(Room)示例:
[
    {
        "mediaIn": {
            "audio": [
                {
                    "codec": "opus",
                    "sampleRate": 48000,
                    "channelNum": 2
                },
                {
                    "codec": "isac",
                    "sampleRate": 16000
                },
                {
                    "codec": "isac",
                    "sampleRate": 32000
                },
                {
                    "codec": "g722",
                    "sampleRate": 16000,
                    "channelNum": 1
                },
                {
                    "codec": "pcma"
                },
                {
                    "codec": "pcmu"
                },
                {
                    "codec": "aac"
                },
                {
                    "codec": "ac3"
                },
                {
                    "codec": "nellymoser"
                },
                {
                    "codec": "ilbc"
                }
            ],
            "video": [
                {
                    "codec": "h264"
                },
                {
                    "codec": "vp8"
                },
                {
                    "codec": "vp9"
                }
            ]
        },
        "mediaOut": {
            "video": {
                "parameters": {
                    "resolution": [
                        "x3/4",
                        "x2/3",
                        "x1/2",
                        "x1/3",
                        "x1/4",
                        "hd1080p",
                        "hd720p",
                        "svga",
                        "vga",
                        "qvga",
                        "cif"
                    ],
                    "framerate": [
                        6,
                        12,
                        15,
                        24,
                        30,
                        48,
                        60
                    ],
                    "bitrate": [
                        "x0.8",
                        "x0.6",
                        "x0.4",
                        "x0.2"
                    ],
                    "keyFrameInterval": [
                        100,
                        30,
                        5,
                        2,
                        1
                    ]
                },
                "format": [
                    {
                        "codec": "vp8"
                    },
                    {
                        "codec": "h264",
                        "profile": "CB"
                    },
                    {
                        "codec": "h264",
                        "profile": "B"
                    },
                    {
                        "codec": "vp9"
                    }
                ]
            },
            "audio": [
                {
                    "codec": "opus",
                    "sampleRate": 48000,
                    "channelNum": 2
                },
                {
                    "codec": "isac",
                    "sampleRate": 16000
                },
                {
                    "codec": "isac",
                    "sampleRate": 32000
                },
                {
                    "codec": "g722",
                    "sampleRate": 16000,
                    "channelNum": 1
                },
                {
                    "codec": "pcma"
                },
                {
                    "codec": "pcmu"
                },
                {
                    "codec": "aac",
                    "sampleRate": 48000,
                    "channelNum": 2
                },
                {
                    "codec": "ac3"
                },
                {
                    "codec": "nellymoser"
                },
                {
                    "codec": "ilbc"
                }
            ]
        },
        "transcoding": {
            "video": {
                "parameters": {
                    "resolution": true,
                    "framerate": true,
                    "bitrate": true,
                    "keyFrameInterval": true
                },
                "format": true
            },
            "audio": true
        },
        "notifying": {
            "participantActivities": true,
            "streamChange": true
        },
        "inputLimit": -1,
        "participantLimit": -1,
        "selectActiveAudio": false,
        "roles": [
            {
                "role": "presenter",
                "publish": {
                    "audio": true,
                    "video": true
                },
                "subscribe": {
                    "audio": true,
                    "video": true
                }
            },
            {
                "role": "viewer",
                "publish": {
                    "audio": false,
                    "video": false
                },
                "subscribe": {
                    "audio": true,
                    "video": true
                }
            },
            {
                "role": "audio_only_presenter",
                "publish": {
                    "audio": true,
                    "video": false
                },
                "subscribe": {
                    "audio": true,
                    "video": false
                }
            },
            {
                "role": "video_only_viewer",
                "publish": {
                    "audio": false,
                    "video": false
                },
                "subscribe": {
                    "audio": false,
                    "video": true
                }
            },
            {
                "role": "sip",
                "publish": {
                    "audio": true,
                    "video": true
                },
                "subscribe": {
                    "audio": true,
                    "video": true
                }
            }
        ],
        "_id": "62b95bd27ff8054480cbb73a",
        "name": "sampleRoom",
        "views": [
            {
                "audio": {
                    "format": {
                        "codec": "opus",
                        "sampleRate": 48000,
                        "channelNum": 2
                    },
                    "vad": true
                },
                "video": {
                    "parameters": {
                        "resolution": {
                            "width": 640,
                            "height": 480
                        },
                        "framerate": 24,
                        "keyFrameInterval": 100
                    },
                    "bgColor": {
                        "r": 0,
                        "g": 0,
                        "b": 0
                    },
                    "layout": {
                        "templates": {
                            "base": "fluid",
                            "custom": []
                        },
                        "fitPolicy": "letterbox"
                    },
                    "format": {
                        "codec": "vp8"
                    },
                    "maxInput": 16,
                    "motionFactor": 0.8,
                    "keepActiveInputPrimary": false
                },
                "label": "common"
            }
        ],
        "__v": 0,
        "id": "62b95bd27ff8054480cbb73a"
    }
]
  1. 生成房间接口:POST ${host}/v1/rooms
  2. request body:
object(RoomConfig):          // Configuration used to create room
{
    name:name,               // Name of the room, required
    options: object(Room)    // Subset of the object(Room) definition above, optional.
}
  1. object(RoomConfig) 示例:
{
  "name": "sampleRoom"
}
  1. response body:同object(Room)。

你可能感兴趣的:(流媒体服务器,WebRTC,webrtc,java,开发语言)