信令分析因为包含一些代码和格式,文章很长,所以分成上下两篇记录,OWT(Open WebRTC Toolkit)相关文章
相关文章:
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
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);
};
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"
}
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 }
}
];
eyJ0b2tlbklkIjoiNjM2MjE2YWExNmY5ZWI0MjkxNjMwY2RiIiwiaG9zdCI6ImludHJhLXJ0Yy5zbWFydGVkdS5sZW5vdm8uY29tOjgwODAiLCJzZWN1cmUiOnRydWUsInNpZ25hdHVyZSI6Ik56YzBNamRsWWpNMk5EQXdOV0ZpWkRNME1HUXdPRE16TjJFek1XUTFNMlU0TVRjeU5UQTFaall6TnpGaU1UQXdNVFJsTkRRMU1ESXhaV05tTm1NMVpBPT0ifQ==
var createToken = function (room, user, role, callback, host) {
var body = {
room: room,
user: user,
role: role
};
send('POST', '/tokens/', body, callback, host);
};
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);
});
});
/**
* @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 = 'user-id@company.com';
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);
};
//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);
});
};
/*
* 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);
});
});
}
});
};
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);
});
};
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
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);
});
});
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);
});
})();
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
}
[
{
"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"
}
]
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.
}
{
"name": "sampleRoom"
}