Meetings plugin for the Cisco Webex JS SDK.
WARNING: This plugin is currently under active development, is not stable, and breaking changes can and will happen!
To create supported meetings and calls using Webex Teams, use @ciscospark/plugin-phone.
npm install --save @webex/plugin-meetings
This is a plugin for the Cisco Webex JS SDK . Please see our developer portal and the API docs for full details.
Examples
let convoId = `ObiwanAnnouncementsConversationUUID`;
return spark.meetings.create(convoId).then((meeting) ==> {...});
let sipUri = `obiwan@example.com`;
return spark.meetings.create(sipUri).then((meeting) ==> {...});
// TODO: list out the specific packages needed to just pull in plugin meetings instead of the full
// ciscospark package?
import ciscospark as spark from 'ciscospark';
let destination = `obiwan@example.com`;
let activeMeeting;
function setMedia(media){
if (media.type === 'local') {
document.getElementById('local-media').srcObject = media.stream;
} else if (media.type === 'remote') {
document.getElementById('remote-media').srcObject = media.stream;
}
}
function handleAudioChange(audio){
// perform some actions after audio has been muted/unmuted
}
function handleVideoChange(video) {
// perform some action after video has been muted/unmuted
}
return spark.meetings
.create(destination)
.then((meeting) => {
activeMeeting = meeting;
activeMeeting.on('media:ready', () => setMedia));
activeMeeting.on('media:audioChanged', handleAudioChange);
activeMeeting.on('media:videoChanged', handleVideoChange);
activeMeeting.join({resourceId: <DeviceId>});
});
from above, we build off of the join...
...
meeting.join({resourceId: <DeviceId>}).then((res) => {
// backend services determine you are the owner, so no hostpin, or intent is required
// now you are in the meeting
});
...
...
// join as host (in place of them)
meeting.join({resourceId: <DeviceId>, hostPin: <WebexHostPin>, moderator: true}).then((res) => {
// now you are in the meeting
});
...
// join as attendee
meeting.join({resourceId: <DeviceId>, moderator: false}).then((res) => {
// if host hasn't started the meeting, now you are in the lobby, else if host has started the meeting, you are in the meeting
});
// join as host automatically
meeting.join({resourceId: <DeviceId>, hostPin: <WebexHostPin>, moderator: true}).then((res) => {
// now you are in the meeting
});
// join as host with ask user option
// join as attendee
meeting.join({resourceId: <DeviceId>}).then((res) => {
}).catch((err) => {
if (err.joinIntentRequired) {
// at this point you can ask the user to join as host or join as guest
// if join as host, requires a pin
...
// join as host simply makes the join call again with the proper hostpin/moderator parameters
meeting.join({resourceId: <DeviceId>, hostPin: <WebexHostPin>, moderator: true}).then(() => {
// you are now in the meeting
});
...
// join as guest simply makes the call again with moderator parameter
meeting.join(({resourceId: <DeviceId>, moderator: false})).then(() => {
// if host hasn't started the meeting, now you are in the lobby, else if host has started the meeting, you are in the meeting
});
}
});
const link = ...; // a valid pmr link
const pin = ...; // a valid host pin assoicated to the link
// claiming a pmr, and updating the cached values for the stored PMR
spark.meetings.personalMeetingRoom.claim(link, pin).then((pmr) => {
console.log(pmr); // do something else with the pmr
});
spark.meetings.personalMeetingRoom.get().then((pmr) => {
// do some stuff with the pmr values
console.log(`PMR INFO:
link-${spark.meetings.personalMeetingRoom.meetingLink}-
uri-${spark.meetings.personalMeetingRoom.sipUri}-
tollFree-${spark.meetings.personalMeetingRoom.pmr.callInNumbersInfo.callInTollFreeNumber.number}-
toll-${spark.meetings.personalMeetingRoom.pmr.callInNumbersInfo.callInTollNumber.number}-
accessCode-${spark.meetings.personalMeetingRoom.pmr.meetingNumber}
`);
});
member.participant ... // Object server participant object, advanced use only
member.id ... // String key for storing
member.name ... // String plain text name
member.isAudioMuted ... // Boolean
member.isVideoMuted ... // Boolean
member.isSelf ... // Boolean is this member YOUR user?
member.isHost ... // Boolean
member.isGuest ... // Boolean
member.isInLobby ... // Boolean
member.isInMeeting ... // Boolean
member.isNotAdmitted ... // Boolean -- waiting to be admitted to the meeting, will also have isInLobby true
member.isContentSharing ... // Boolean
member.status ... // String -- advanced use only
member.isDevice ... // Boolean
member.isUser ... // Boolean
member.associatedUser ... // String -- member.id if isDevice is true
member.isRecording ... // Boolean
// more coming...
You can access the members object on each individual meeting instance, it has some key events to listen to, and maintains what happens for members of a meeting with some key properties.
meeting.members ...
meeting.membersCollection ... // the members collection, object {id0: member0, ... idN: memberN}
meeting.locusUrl ... // current locusUrl being used
meeting.hostId ... // active host id for the meeting
meeting.selfId ... // active self id for the meeting
meeting.mediaShareContentId ... // active content sharer id for the meeting
// You can add a guest to the meeting by inviting them, this is proxied by meeting.invite
// use an emailAddress and a boolean value alertIfActive to notify server side (usually true)
meeting.members.addMember(emailAddress, alertIfActive)
// You can admit the guest to the meeting once they are waiting in the lobby, you can do this in bulk, proxied by meeting.admit
// use member ids, can be singular, but has to be put into an array
meeting.members.admitMembers([memberIds])
// members collection updated
meeting.members.on('members:update', (payload) => {
const delta = payload.delta; // the changes to the members list
const full = payload.full; // the full members collection
const updated = delta.updated; // only the updates, includes removals, as they will have updated status and member properties
const added = delta.added; // added members to the meeting
Object.keys(full).forEach((key) => {
const member = full[key];
console.log(`Member: ... ${member.x}`);
});
Object.keys(updated).forEach((key) => {
const member = updated[key];
console.log(`Member Updated: ... ${member.x}`);
});
Object.keys(added).forEach((key) => {
const member = added[key];
console.log(`Member Added: ... ${member.x}`);
});
});
// content updates
meeting.members.on('members:content:update', (payload) => {
console.log(`who started sharing: ${payload.activeContentSharingId};`);
console.log(`who stopped sharing: ${payload.endedContentSharingId};`);
});
// host updates
meeting.members.on('members:host:update', (payload) => {
console.log(`who started hosting: ${payload.activeHostId};`);
console.log(`who stopped hosting: ${payload.endedHostId};`);
})
// self updates, not typically used
meeting.members.on('members:self:update', (payload) => {
console.log(`active self id: ${payload.activeSelfId};`);
console.log(`ended self Id: ${payload.endedSelfId};`);
})
Note: accessor methods get are simply helpers, one can always use dot notation to work on the objects in the same way
// operate with a Transform stream
meeting
.getStats() // get the stats instance, creates a new one if one does not exist on the meeting object
.withMedia() // add the media type, to use the media peer connection, can also add on withScreen() for screen peer connection
.build() // finish up building the getStats instance and get a reference back
.getMedia() // get the media instance off of getStats
.withId() // add an ID to the media type of stats, optional
.withEvents() // trigger 'stats:update' events on media, see 'Events' below, optional
.withHistory() // collect history with DEFAULT_HISTORY_MAX size (default is 30 minutes, 1800 entries), optional
.withAggregator() // aggregate data from the stream each time, to have a summary of each stat, optional
.withInterval() // find the actual values between each registered interval, optional
.withFilter(meeting.mediaProperties.mediaPeerConnection) // create the Transform stream filter on the media instance to convert the data to
// a nice human readable format, and parse out relevant stats, defined by DEFAULT_TRANSFORM_REGEX, see nodejs Transform Stream
.build() // finish up building the media stats instance and get a reference back
.onData((filtered) => { // do something with each filtered data beyond any of the added options, pass a callback
console.log(filtered);
});
// OR
// for raw get stats without a filter, getting just a Readable stream back, see nodejs Readable Stream
// not for use with events, history, aggregator, interval, will result in undefined behavior
meeting
.getStats() // get the stats instance, creates a new one if one does not exist on the meeting object
.withMedia() // add the media type, to use the media peer connection, can also add on withScreen() for screen peer connection
.build() // finish up building the getStats instance and get a reference back
.getMedia() // get the media instance off of getStats
.withStream(meeting.mediaProperties.mediaPeerConnection); // create the Readable stream on the media instance to be able to set up a data pipe
// or a simple listener for the stats reports coming back, to do your own stats filtering/analysis
meeting.getStats().getMedia().getStream().on('data', (data) => {
// do something with the raw getStats report
});
// afterwards for each you may just call to get a stats instance, without having to create it's configuration using with()
meeting.getStats()...
// functionally equivalent to the builder
// pass in with a standard options object which pieces you want to create on the stats instance
meeting.getStats({ // create the stats reference
screen: { // with screen option
id: true, // with an id on the screen stats
history: true, // with history
interval: true, // with interval
filter: meeting.mediaProperties.sharePeerConnection, // with filter
events: true // with events
},
media: { // can add media or exclude
...
}
});
meeting.getStats().getScreen().onData((filtered) => {
console.log(filtered);
});
// afterwards, to have the same instance you may just call
meeting.getStats()...
// you can always add additional configurations later on based on events, or some timing, using the builder above
// say you created the object above with the options method, and you decided you wanted to add an aggregator to it
meeting
.getStats()
.getScreen()
.withAggregator();
// and now you have an aggregator instance to work with on that screen stats object
meeting.stats.screen.aggregator...
// note: using the aggregator is probably not necessary unless the WebRTC spec changes, or browsers change behavior and no longer automatically
// summarize their data
meeting
.getStats() // get the stats instance on the meeting
.getMedia() // get the media peer connection related stats, can also do getScreen()
.getAggregator() // get the aggregator instance on that media stats
.getSummary(); // get the last found summary object
meeting
.getStats() // get the stats instance on the meeting
.getMedia() // get the media peer connection related stats, can also do getScreen()
.getEvents() // get the events instance on that media stats
.on('stats:update', (stats) => {
// listen to the stats update event that comes through here
// do something with the stats event, see Events below
});
meeting
.getStats() // get the stats instance on the meeting
.getMedia() // get the media peer connection related stats, can also do getScreen()
.getHistory() // get the history instance on that media stats
.getHistory(); // get the actual array of history stats objects
meeting
.getStats() // get the stats instance on the meeting
.getMedia() // get the media peer connection related stats, can also do getScreen()
.getInterval() // get the interval instance on that media stats
.getInterval(); // get the actual interval object that was last saved
// create
meeting
.getStats()
.withMedia()
.build();
meeting
.getStats()
.getMedia()
.withId()
.withFilter(meeting.mediaProperties.mediaPeerConnection)
.withEvents()
.withHistory()
.withInterval()
.withAggregator()
.build();
meeting
.getStats()
.withScreen()
.build();
meeting
.getStats()
.getScreen()
.withId()
.withStream(meeting.mediaProperties.sharePeerConnection);
// do stuff
meeting
.getStats()
.getMedia()
.getStream()
.on('data', (data) => {
console.log(data);
});
meeting
.getStats()
.getMedia()
.onData((filtered) => {
console.log(filtered);
});
meeting
.getStats()
.getMedia()
.getEvents()
.on('stats:update', (stats) => {
console.log(stats);
});
There are several events submitted by this package that you can subscribe to.
Event Name | Description |
---|---|
media:ready |
Fired when remote or local media has been acquired |
media:audioChanged |
Fired whenever local audio streams are muted and unmuted |
media:videoChanged |
Fired whenever local video streams are muted and unmuted |
media:stopped |
Fired when remote or local media has been torn down |
stats:update |
Fired when a getStats key event occurs (first audio/video bytes received/sent) |
--- | --- |
media:ready has the following format |
{
type, // local or remote
stream; // the MediaStream
}
media:audioChanged
and media:videoChanged
has the following format
{
type, // local or error
status, // string representation of state
muted, // boolean true or false for muted or not
result; // the promise error or resolution value
}
media:stopped
has the following format
{
type; // local or remote
}
stats:update
has the following format
{
type, // local or remote
key, // which key it matched, i.e., rtpOutAudio, etc, defined by DEFAULT_EVENT_VIDEO|AUDIO_RECEIVE|SEND_KEYS
data, // the data at the point that the event was fired
stat, // the accessor for the stat itself, i.e, bytesSent, bytesReceived
id, // the id specified when creating the events instance
kind; // audio or video
}
To use webpack-dev-server
to load this package, run npm run samples:serve
.
Files placed in the packages/node_modules/samples/browser-plugin-meetings
folder will be served statically.
Files in the src
folder will be compiled, bundled, and served as a static asset at bundle.js
inside that directory.
This package is maintained by Cisco Webex for Developers.
Pull requests welcome. Please see CONTRIBUTING.md for more details.
© 2016-2018 Cisco and/or its affiliates. All Rights Reserved.