前言
WebEx 是 Cisco 旗下的影音串流服務,包含webex meeting 和 webex Teams。
目前webex 許多 API response time都稍微久,不過他在跨平台上還是做得不錯的。
webex meeting 目前 API 許久沒維護,所以以下教學以 webex Teams 為主。
連結:
網頁版teams: https://teams.webex.com/signin
開發者頁面:https://developer.webex.com/
互動式教學:https://developer.cisco.com/learning/
詳細 JS SDK 文件:https://github.com/webex/webex-js-sdk/blob/master/packages/node_modules/%40webex/plugin-meetings/README.md
Restful API 文件:https://developer.webex.com/docs/api/v1/
Widge文件:https://code.s4d.io/widget-demo/production/index.html
開發
1.創建 access token: 註冊帳號後到此拉下去一點即可看到 https://developer.webex.com/docs/api/getting-started
2.或是可以創建guest issuer 之後發請求取得access token,但是guest issuer必須要付費帳號才能創建
以下為取得 guest issuer 取得access token 的過程
Copy const https = require('https');
const jwt = require('jsonwebtoken');
const payload = {
sub: 'patientId',
name: 'PatientName',
iss: 'Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi9mYTVjODdjZS02ODcwLTRjOWQtYWM3Yi03ZWFhY2QxZTZjMTM',
};
const guestToken = jwt.sign(payload, Buffer.from('SnLNSN9HGwnDa2ZttWPwCVA6kgZ8+GrwOGMJEcYr2l0==', 'base64'), {
expiresIn: '8h',
});
const options = {
hostname: 'api.ciscospark.com',
port: 443,
path: '/v1/jwt/login',
method: 'POST',
headers: {
Authorization: `Bearer ${guestToken}`,
},
};
function generateAccessToken() {
return new Promise((resolve, reject) => {
const req = https.request(options, res => {
let body = '';
res.on('data', function (chunk) {
body = body + chunk;
});
res.on('end', function () {
const { token } = JSON.parse(body);
resolve(token);
});
});
req.on('error', e => {
console.error(e);
});
req.end();
});
}
module.exports = {
generateAccessToken,
};
測試
可以開啟webex teams APP,如果有人打過去,不論是用email 或是 房間ID 都會在APP顯示
完整範例:
Copy /* eslint-disable no-unused-vars */
import React, { useEffect, useImperativeHandle, useRef, useState } from 'react';
import * as Webex from 'webex';
import { createStyles, makeStyles } from '@material-ui/core/styles';
import styles from './styles';
const useStyles = makeStyles(createStyles(styles));
const WebexVideo = ({ generateAccessToken, roomId }) => {
const classes = useStyles();
const [currentMeeting, setCurrentMeeting] = useState('');
const [userAdmitted, setUserAdmitted] = useState(false);
const inputEl = useRef(null);
const localVideo = useRef(null);
const remoteVideo = useRef(null);
const remoteAudio = useRef(null);
const connect = async accessToken => {
const webex = Webex.init({
config: {
meetings: {
deviceType: 'WEB',
},
},
credentials: {
access_token: accessToken,
},
});
webex.meetings.on('meeting:added', async addedMeetingEvent => {
try {
if (addedMeetingEvent.type === 'INCOMING' || addedMeetingEvent.type === 'JOIN') {
const addedMeeting = addedMeetingEvent.meeting;
await addedMeeting.acknowledge(addedMeetingEvent.type);
if (window.confirm('Answer incoming call')) {
joinMeeting(addedMeeting);
bindMeetingEvents(addedMeeting);
} else {
addedMeeting.decline();
}
}
} catch (err) {
console.error(err);
}
});
// Register our device with Webex cloud
if (!webex.meetings.registered) {
try {
await webex.meetings.register();
// Sync our meetings with existing meetings on the server
await webex.meetings.syncMeetings();
return webex;
} catch (err) {
throw err;
}
} else {
return webex;
}
};
const dial = async () => {
try {
const accessToken = await generateAccessToken();
const webex = await connect(accessToken);
const destination = roomId || inputEl.current.value;
const meeting = await webex.meetings.create(destination);
// Save meeting
setCurrentMeeting(meeting);
// Call our helper function for binding events to meetings
bindMeetingEvents(meeting);
// Pass the meeting to our join meeting helper
joinMeeting(meeting);
} catch (err) {
console.error(err);
}
};
const addMediaStream = async meeting => {
try {
const mediaSettings = {
receiveVideo: true,
receiveAudio: true,
receiveShare: false,
sendVideo: true,
sendAudio: true,
sendShare: false,
};
const mediaStreams = await meeting.getMediaStreams(mediaSettings);
const [localStream, localShare] = mediaStreams;
meeting.addMedia({
localShare,
localStream,
mediaSettings,
});
} catch (err) {
console.error(err);
}
};
const joinMeeting = async meeting => {
try {
meeting.on('meeting:self:lobbyWaiting', addedMeetingEvent => {
// user waiting in lobby
// TODO: calling API to change User status
});
meeting.on('meeting:self:guestAdmitted', addedMeetingEvent => {
// user waiting in lobby
// TODO: calling API to change User status
addMediaStream(meeting);
});
await meeting.join();
addMediaStream(meeting);
} catch (err) {
console.error(err);
}
};
const bindMeetingEvents = meeting => {
const receiveMediaSource = (type, media) => {
type.srcObject = media.stream;
type.onloadedmetadata = () => {
type.play();
};
};
meeting.on('error', err => {
console.error(err);
});
// Handle media streams changes to ready state
meeting.on('media:ready', media => {
if (!media) {
return;
}
if (media.type === 'local') {
// set srcObject as media.stream
const video = localVideo.current;
receiveMediaSource(video, media);
}
if (media.type === 'remoteVideo') {
// set srcObject as media.stream
const video = remoteVideo.current;
receiveMediaSource(video, media);
}
if (media.type === 'remoteAudio') {
// set srcObject as media.stream
const audio = remoteAudio.current;
receiveMediaSource(audio, media);
}
});
// Handle media streams stopping
meeting.on('media:stopped', media => {
const clearVideoStream = () => {
localVideo.current.srcObject = null;
remoteVideo.current.srcObject = null;
remoteAudio.current.srcObject = null;
};
clearVideoStream();
});
// Update participant info
meeting.members.on('members:update', delta => {
// const { full: membersData } = delta;
// const memberIDs = Object.keys(membersData);
// Update participant info
});
meeting.on('all', event => {
// console.log(event);
});
};
const hangUp = () => {
try {
currentMeeting.leave();
setCurrentMeeting('');
} catch (err) {
console.error(err);
}
}
});
return (
<>
<div className={classes.localVideoContainer}>
<video className={classes.localVideo} ref={localVideo} muted autoPlay playsInline />
</div>
<div className={classes.remoteVideoContainer}>
<audio ref={remoteAudio} autoPlay playsInline />
<video className={classes.remoteVideo} ref={remoteVideo} autoPlay playsInline />
</div>
{!roomId && (
<div>
<input ref={inputEl} type="text" placeholder="roomID or SIP or Email" />
<button onClick={() => dial()}>dial</button>
<button onClick={() => hangUp()}>hang up</button>
</div>
)}
</>
);
});
WebexVideo.displayName = 'WebexVideo';
export default WebexVideo;
React Widget
https://github.com/webex/react-widgets
使用React Widget demo local 會有錯誤,要輸入 npm rebuild node-sass
與使用Node.js v10.16.0
包含space widget (右) 與 Recents Widget (左)
可能錯誤
Copy 1.Unhandled Rejection (BadRequest): (6400007) incompatible device: reasons are received invalid message type 'ANSWER' in state 'ROAP_STATE_INIT'
2.state: LEFT invalid for current operation.
3.Unhandled Rejection (Conflict): Conflict PUT
4.User has excessive device registrations POST
以上錯誤都建議先更換guest issuer token 試試,第四個錯誤官方建議清除session https://idbroker.webex.com/idb/profile#/tokens 不過測試後無效
2. 開發頁面登入 session錯誤
時常登入使用者後又說頁面找不到404,或是使用者重登後卻沒有更新的情況,建議 瀏覽器硬加載
3. 出現 user must admitted first,使用房間管理員到介面把guest加入meeting