<commit_before>'use strict';

import debug from 'debug';
import uuidv4 from 'uuid/v4';
import utils from './utils';

import { EventEmitter } from 'events';

const DEBUG = debug('sylkrtc:Conference');


class Participant extends EventEmitter {
    constructor(publisherId, identity, conference) {
        super();
        this._id = uuidv4();
        this._publisherId = publisherId;
        this._identity = identity;
        this._conference = conference;
        this._state = null;
        this._pc = null;
        this._videoSubscriptionPaused = false;
        this._audioSubscriptionPaused = false;
        this._videoPublishingPaused = false;
        this._audioPublishingPaused = false;
    }

    get id() {
        return this._id;
    }

    get publisherId() {
        return this._publisherId;
    }

    get identity() {
        return this._identity;
    }

    get conference() {
        return this._conference;
    }

    get videoPaused() {
        return this._videoSubscriptionPaused;
    }

    get state() {
        return this._state;
    }

    get streams() {
        if (this._pc !== null) {
            return this._pc.getRemoteStreams();
        } else {
            return [];
        }
    }


    attach() {
        if (this._state !== null) {
            return;
        }
        this._setState('progress');
        this._sendAttach();
    }

    detach() {
        if (this._state !== null) {
            this._sendDetach();
        }
    }

    pauseVideo() {
        this._sendUpdate({video: false});
        this._videoSubscriptionPaused = true;
    }

    resumeVideo() {
        this._sendUpdate({video: true});
        this._videoSubscriptionPaused = false;
    }

    _setState(newState) {
        const oldState = this._state;
        this._state = newState;
        DEBUG(`Participant ${this.id} state change: ${oldState} -> ${newState}`);
        this.emit('stateChanged', oldState, newState);
    }

    _handleOffer(offerSdp) {
        DEBUG('Handling SDP for participant offer: %s', offerSdp);

        // Create the RTCPeerConnection
        const pcConfig = this.conference._pcConfig;
        const pc = new RTCPeerConnection(pcConfig);
        pc.addEventListener('addstream', (event) => {
            DEBUG('Stream added');
            this.emit('streamAdded', event.stream);
        });
        pc.addEventListener('icecandidate', (event) => {
            if (event.candidate !== null) {
                DEBUG('New ICE candidate %o', event.candidate);
            } else {
                DEBUG('ICE candidate gathering finished');
            }
            this._sendTrickle(event.candidate);
        });
        this._pc = pc;

        // no need for a local stream since we are only going to receive media here
        pc.setRemoteDescription(
            new RTCSessionDescription({type: 'offer', sdp: offerSdp}),
            // success
            () => {
                utils.createLocalSdp(pc, 'answer')
                    .then((sdp) => {
                        DEBUG('Local SDP: %s', sdp);
                        this._sendAnswer(sdp);
                    })
                    .catch((reason) => {
                        DEBUG(reason);
                        this._close();
                    });
            },
            // failure
            (error) => {
                DEBUG('Error setting remote description: %s', error);
                this._close();
            }
        );
    }

    _sendAttach() {
        const req = {
            sylkrtc: 'videoroom-feed-attach',
            session: this.conference.id,
            publisher: this._publisherId,
            feed: this.id
        };
        DEBUG('Sending request: %o', req);
        this.conference._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Error attaching to participant %s: %s', this._publisherId, error);
            }
        });
    }

    _sendDetach() {
        const req = {
            sylkrtc: 'videoroom-feed-detach',
            session: this.conference.id,
            feed: this.id
        };
        DEBUG('Sending request: %o', req);
        this.conference._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Error detaching to participant %s: %s', this._publisherId, error);
            }
            this._close();
        });
    }

    _sendTrickle(candidate) {
        const req = {
            sylkrtc: 'videoroom-session-trickle',
            session: this.id,
            candidates: candidate !== null ? [candidate] : []
        };
        this.conference._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Trickle error: %s', error);
                this._close();
            }
        });
    }

    _sendAnswer(sdp) {
        const req = {
            sylkrtc: 'videoroom-feed-answer',
            session: this.conference.id,
            feed: this.id,
            sdp: sdp
        };
        DEBUG('Sending request: %o', req);
        this.conference._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Answer error: %s', error);
                this._close();
            }
        });
    }

    _sendUpdate(options = {}) {
        const req = {
            sylkrtc: 'videoroom-session-update',
            session: this.id,
            options: options
        };
        DEBUG('Sending update participant request %o', req);
        this.conference._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Answer error: %s', error);
            }
        });
    }

    _close() {
        DEBUG('Closing Participant RTCPeerConnection');
        if (this._pc !== null) {
            for (let stream of this._pc.getLocalStreams()) {
                utils.closeMediaStream(stream);
            }
            for (let stream of this._pc.getRemoteStreams()) {
                utils.closeMediaStream(stream);
            }
            this._pc.close();
            this._pc = null;
            this._setState(null);
        }
    }
}


class ConferenceCall extends EventEmitter {
    constructor(account) {
        super();
        this._account = account;
        this._id = null;
        this._pc = null;
        this._participants = new Map();
        this._terminated = false;
        this._state = null;
        this._localIdentity = new utils.Identity(account.id, account.displayName);
        this._remoteIdentity = null;
        this._activeParticpants = [];
        this._pcConfig = null;  // saved on initialize, used later for subscriptions
        this._delay_established = false;  // set to true when we need to delay posting the state change to 'established'
        this._setup_in_progress = false;  // set while we set the remote description and setup the peer copnnection
    }

    get account() {
        return this._account;
    }

    get id() {
        return this._id;
    }

    get direction() {
        // make this object API compatible with `Call`
        return 'outgoing';
    }

    get state() {
        return this._state;
    }

    get localIdentity() {
        return this._localIdentity;
    }

    get remoteIdentity() {
        return this._remoteIdentity;
    }

    get participants() {
        return Array.from(new Set(this._participants.values()));
    }

    get activeParticipants() {
        return this._activeParticpants;
    }

    getLocalStreams() {
        if (this._pc !== null) {
            return this._pc.getLocalStreams();
        } else {
            return [];
        }
    }

    getRemoteStreams() {
        let streams = [];
        for (let participant of new Set(this._participants.values())) {
            streams = streams.concat(participant.streams);
        }
        return streams;
    }

    scaleLocalTrack(oldTrack, divider) {
        DEBUG('Scaling track by %d', divider);

        let sender;

        for (sender of this._pc.getSenders()) {
            if (sender.track === oldTrack) {
                DEBUG('Found sender to modify track %o', sender);
                break;
            }
        }

        sender.setParameters({encodings: [{scaleResolutionDownBy: divider}]})
            .then(() => {
                DEBUG("Scale set to %o", divider);
                DEBUG('Active encodings %o', sender.getParameters().encodings);
            })
            .catch((error) => {
                DEBUG('Error %o', error)
            });
    }

    configureRoom(ps, cb=null) {
        if (!Array.isArray(ps)) {
            return;
        }
        this._sendConfigureRoom(ps, cb);
    }

    terminate() {
        if (this._terminated) {
            return;
        }
        DEBUG('Terminating conference');
        this._sendTerminate();
    }

    inviteParticipants(ps) {
        if (this._terminated) {
            return;
        }
        if (!Array.isArray(ps) || ps.length === 0) {
            return;
        }
        DEBUG('Inviting participants: %o', ps);
        const req = {
            sylkrtc: 'videoroom-invite',
            session: this.id,
            participants: ps
        };
        this._sendRequest(req, null);
    }

    // Private API

    _initialize(uri, options={}) {
        if (this._id !== null) {
            throw new Error('Already initialized');
        }

        if (uri.indexOf('@') === -1) {
            throw new Error('Invalid URI');
        }

        if (!options.localStream) {
            throw new Error('Missing localStream');
        }

        this._id = uuidv4();
        this._remoteIdentity = new utils.Identity(uri);

        options = Object.assign({}, options);
        const pcConfig = options.pcConfig || {iceServers:[]};
        this._pcConfig = pcConfig;
        this._initialParticipants = options.initialParticipants || [];
        const offerOptions = options.offerOptions || {};
        // only send audio / video through the publisher connection
        offerOptions.offerToReceiveAudio = false;
        offerOptions.offerToReceiveVideo = false;
        delete offerOptions.mandatory;

        // Create the RTCPeerConnection
        this._pc = new RTCPeerConnection(pcConfig);
        this._pc.addEventListener('icecandidate', (event) => {
            if (event.candidate !== null) {
                DEBUG('New ICE candidate %o', event.candidate);
            } else {
                DEBUG('ICE candidate gathering finished');
            }
            this._sendTrickle(event.candidate);
        });

        this._pc.addStream(options.localStream);
        this.emit('localStreamAdded', options.localStream);
        DEBUG('Offer options: %o', offerOptions);
        utils.createLocalSdp(this._pc, 'offer', offerOptions)
            .then((sdp) => {
                DEBUG('Local SDP: %s', sdp);
                this._sendJoin(sdp);
            })
            .catch((reason) => {
                this._localTerminate(reason);
            });
    }

    _handleEvent(message) {
        DEBUG('Conference event: %o', message);
        switch (message.event) {
            case 'session-state':
                let oldState = this._state;
                let newState = message.state;
                this._state = newState;

                if (newState === 'accepted') {
                    this.emit('stateChanged', oldState, newState, {});
                    const sdp = utils.mungeSdp(message.sdp);
                    DEBUG('Remote SDP: %s', sdp);
                    this._setup_in_progress = true;
                    this._pc.setRemoteDescription(
                        new RTCSessionDescription({type: 'answer', sdp: sdp}),
                        // success
                        () => {
                            this._setup_in_progress = false;
                            if (!this._terminated) {
                                if (this._delay_established) {
                                    oldState = this._state;
                                    this._state = 'established';
                                    DEBUG('Setting delayed established state!');
                                    this.emit('stateChanged', oldState, this._state, {});
                                    this._delay_established = false;
                                }
                                DEBUG('Conference accepted');
                                if (this._initialParticipants.length > 0 ) {
                                    setTimeout(() => {
                                            this.inviteParticipants(this._initialParticipants);
                                    }, 50);
                                }
                            }
                        },
                        // failure
                        (error) => {
                            DEBUG('Error processing conference accept: %s', error);
                            this.terminate();
                        }
                    );
                } else if (newState === 'established') {
                    if (this._setup_in_progress) {
                        this._delay_established = true;
                    } else {
                        this.emit('stateChanged', oldState, newState, {});
                    }
                } else if (newState === 'terminated') {
                    this.emit('stateChanged', oldState, newState, {reason: message.reason});
                    this._terminated = true;
                    this._close();
                } else {
                    this.emit('stateChanged', oldState, newState, {});
                }
                break;
            case 'initial-publishers':
                // this comes between 'accepted' and 'established' states
                for (let p of message.publishers) {
                    participant = new Participant(p.id, new utils.Identity(p.uri, p.display_name), this);
                    this._participants.set(participant.id, participant);
                    this._participants.set(p.id, participant);
                }
                break;
            case 'publishers-joined':
                for (let p of message.publishers) {
                    DEBUG('Participant joined: %o', p);
                    participant = new Participant(p.id, new utils.Identity(p.uri, p.display_name), this);
                    this._participants.set(participant.id, participant);
                    this._participants.set(p.id, participant);
                    this.emit('participantJoined', participant);
                }
                break;
            case 'publishers-left':
                for (let pId of message.publishers) {
                    participant = this._participants.get(pId);
                    if (participant) {
                        this._participants.delete(participant.id);
                        this._participants.delete(pId);
                        this.emit('participantLeft', participant);
                    }
                }
                break;
            case 'feed-attached':
                participant = this._participants.get(message.feed);
                if (participant) {
                    participant._handleOffer(message.sdp);
                }
                break;
            case 'feed-established':
                participant = this._participants.get(message.feed);
                if (participant) {
                    participant._setState('established');
                }
                break;
            case 'configure':
                let activeParticipants = [];
                let originator;
                const mappedOriginator = this._participants.get(message.originator);

                if (mappedOriginator) {
                    originator = mappedOriginator.identity;
                } else if (message.originator === this.id) {
                    originator = this.localIdentity;
                } else if (message.originator === 'videoroom'){
                    originator = message.originator;
                }

                for (let pId of message.active_participants) {
                    participant = this._participants.get(pId);
                    if (participant) {
                        activeParticipants.push(participant);
                    } else if (pId === this.id) {
                        activeParticipants.push({
                            id: this.id,
                            publisherId: this.id,
                            identity: this.localIdentity,
                            streams: this.getLocalStreams()
                        })
                    }
                }
                this._activeParticpants = activeParticipants;
                let roomConfig = {originator: originator, activeParticipants: this._activeParticpants}
                this.emit('roomConfigured', roomConfig);
                break;
            default:
                break;
        }
    }

    _sendConfigureRoom(ps, cb = null) {<commit_msg>Fixed missing semicolumns<commit_after>'use strict';

import debug from 'debug';
import uuidv4 from 'uuid/v4';
import utils from './utils';

import { EventEmitter } from 'events';

const DEBUG = debug('sylkrtc:Conference');


class Participant extends EventEmitter {
    constructor(publisherId, identity, conference) {
        super();
        this._id = uuidv4();
        this._publisherId = publisherId;
        this._identity = identity;
        this._conference = conference;
        this._state = null;
        this._pc = null;
        this._videoSubscriptionPaused = false;
        this._audioSubscriptionPaused = false;
        this._videoPublishingPaused = false;
        this._audioPublishingPaused = false;
    }

    get id() {
        return this._id;
    }

    get publisherId() {
        return this._publisherId;
    }

    get identity() {
        return this._identity;
    }

    get conference() {
        return this._conference;
    }

    get videoPaused() {
        return this._videoSubscriptionPaused;
    }

    get state() {
        return this._state;
    }

    get streams() {
        if (this._pc !== null) {
            return this._pc.getRemoteStreams();
        } else {
            return [];
        }
    }


    attach() {
        if (this._state !== null) {
            return;
        }
        this._setState('progress');
        this._sendAttach();
    }

    detach() {
        if (this._state !== null) {
            this._sendDetach();
        }
    }

    pauseVideo() {
        this._sendUpdate({video: false});
        this._videoSubscriptionPaused = true;
    }

    resumeVideo() {
        this._sendUpdate({video: true});
        this._videoSubscriptionPaused = false;
    }

    _setState(newState) {
        const oldState = this._state;
        this._state = newState;
        DEBUG(`Participant ${this.id} state change: ${oldState} -> ${newState}`);
        this.emit('stateChanged', oldState, newState);
    }

    _handleOffer(offerSdp) {
        DEBUG('Handling SDP for participant offer: %s', offerSdp);

        // Create the RTCPeerConnection
        const pcConfig = this.conference._pcConfig;
        const pc = new RTCPeerConnection(pcConfig);
        pc.addEventListener('addstream', (event) => {
            DEBUG('Stream added');
            this.emit('streamAdded', event.stream);
        });
        pc.addEventListener('icecandidate', (event) => {
            if (event.candidate !== null) {
                DEBUG('New ICE candidate %o', event.candidate);
            } else {
                DEBUG('ICE candidate gathering finished');
            }
            this._sendTrickle(event.candidate);
        });
        this._pc = pc;

        // no need for a local stream since we are only going to receive media here
        pc.setRemoteDescription(
            new RTCSessionDescription({type: 'offer', sdp: offerSdp}),
            // success
            () => {
                utils.createLocalSdp(pc, 'answer')
                    .then((sdp) => {
                        DEBUG('Local SDP: %s', sdp);
                        this._sendAnswer(sdp);
                    })
                    .catch((reason) => {
                        DEBUG(reason);
                        this._close();
                    });
            },
            // failure
            (error) => {
                DEBUG('Error setting remote description: %s', error);
                this._close();
            }
        );
    }

    _sendAttach() {
        const req = {
            sylkrtc: 'videoroom-feed-attach',
            session: this.conference.id,
            publisher: this._publisherId,
            feed: this.id
        };
        DEBUG('Sending request: %o', req);
        this.conference._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Error attaching to participant %s: %s', this._publisherId, error);
            }
        });
    }

    _sendDetach() {
        const req = {
            sylkrtc: 'videoroom-feed-detach',
            session: this.conference.id,
            feed: this.id
        };
        DEBUG('Sending request: %o', req);
        this.conference._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Error detaching to participant %s: %s', this._publisherId, error);
            }
            this._close();
        });
    }

    _sendTrickle(candidate) {
        const req = {
            sylkrtc: 'videoroom-session-trickle',
            session: this.id,
            candidates: candidate !== null ? [candidate] : []
        };
        this.conference._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Trickle error: %s', error);
                this._close();
            }
        });
    }

    _sendAnswer(sdp) {
        const req = {
            sylkrtc: 'videoroom-feed-answer',
            session: this.conference.id,
            feed: this.id,
            sdp: sdp
        };
        DEBUG('Sending request: %o', req);
        this.conference._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Answer error: %s', error);
                this._close();
            }
        });
    }

    _sendUpdate(options = {}) {
        const req = {
            sylkrtc: 'videoroom-session-update',
            session: this.id,
            options: options
        };
        DEBUG('Sending update participant request %o', req);
        this.conference._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Answer error: %s', error);
            }
        });
    }

    _close() {
        DEBUG('Closing Participant RTCPeerConnection');
        if (this._pc !== null) {
            for (let stream of this._pc.getLocalStreams()) {
                utils.closeMediaStream(stream);
            }
            for (let stream of this._pc.getRemoteStreams()) {
                utils.closeMediaStream(stream);
            }
            this._pc.close();
            this._pc = null;
            this._setState(null);
        }
    }
}


class ConferenceCall extends EventEmitter {
    constructor(account) {
        super();
        this._account = account;
        this._id = null;
        this._pc = null;
        this._participants = new Map();
        this._terminated = false;
        this._state = null;
        this._localIdentity = new utils.Identity(account.id, account.displayName);
        this._remoteIdentity = null;
        this._activeParticpants = [];
        this._pcConfig = null;  // saved on initialize, used later for subscriptions
        this._delay_established = false;  // set to true when we need to delay posting the state change to 'established'
        this._setup_in_progress = false;  // set while we set the remote description and setup the peer copnnection
    }

    get account() {
        return this._account;
    }

    get id() {
        return this._id;
    }

    get direction() {
        // make this object API compatible with `Call`
        return 'outgoing';
    }

    get state() {
        return this._state;
    }

    get localIdentity() {
        return this._localIdentity;
    }

    get remoteIdentity() {
        return this._remoteIdentity;
    }

    get participants() {
        return Array.from(new Set(this._participants.values()));
    }

    get activeParticipants() {
        return this._activeParticpants;
    }

    getLocalStreams() {
        if (this._pc !== null) {
            return this._pc.getLocalStreams();
        } else {
            return [];
        }
    }

    getRemoteStreams() {
        let streams = [];
        for (let participant of new Set(this._participants.values())) {
            streams = streams.concat(participant.streams);
        }
        return streams;
    }

    scaleLocalTrack(oldTrack, divider) {
        DEBUG('Scaling track by %d', divider);

        let sender;

        for (sender of this._pc.getSenders()) {
            if (sender.track === oldTrack) {
                DEBUG('Found sender to modify track %o', sender);
                break;
            }
        }

        sender.setParameters({encodings: [{scaleResolutionDownBy: divider}]})
            .then(() => {
                DEBUG("Scale set to %o", divider);
                DEBUG('Active encodings %o', sender.getParameters().encodings);
            })
            .catch((error) => {
                DEBUG('Error %o', error);
            });
    }

    configureRoom(ps, cb=null) {
        if (!Array.isArray(ps)) {
            return;
        }
        this._sendConfigureRoom(ps, cb);
    }

    terminate() {
        if (this._terminated) {
            return;
        }
        DEBUG('Terminating conference');
        this._sendTerminate();
    }

    inviteParticipants(ps) {
        if (this._terminated) {
            return;
        }
        if (!Array.isArray(ps) || ps.length === 0) {
            return;
        }
        DEBUG('Inviting participants: %o', ps);
        const req = {
            sylkrtc: 'videoroom-invite',
            session: this.id,
            participants: ps
        };
        this._sendRequest(req, null);
    }

    // Private API

    _initialize(uri, options={}) {
        if (this._id !== null) {
            throw new Error('Already initialized');
        }

        if (uri.indexOf('@') === -1) {
            throw new Error('Invalid URI');
        }

        if (!options.localStream) {
            throw new Error('Missing localStream');
        }

        this._id = uuidv4();
        this._remoteIdentity = new utils.Identity(uri);

        options = Object.assign({}, options);
        const pcConfig = options.pcConfig || {iceServers:[]};
        this._pcConfig = pcConfig;
        this._initialParticipants = options.initialParticipants || [];
        const offerOptions = options.offerOptions || {};
        // only send audio / video through the publisher connection
        offerOptions.offerToReceiveAudio = false;
        offerOptions.offerToReceiveVideo = false;
        delete offerOptions.mandatory;

        // Create the RTCPeerConnection
        this._pc = new RTCPeerConnection(pcConfig);
        this._pc.addEventListener('icecandidate', (event) => {
            if (event.candidate !== null) {
                DEBUG('New ICE candidate %o', event.candidate);
            } else {
                DEBUG('ICE candidate gathering finished');
            }
            this._sendTrickle(event.candidate);
        });

        this._pc.addStream(options.localStream);
        this.emit('localStreamAdded', options.localStream);
        DEBUG('Offer options: %o', offerOptions);
        utils.createLocalSdp(this._pc, 'offer', offerOptions)
            .then((sdp) => {
                DEBUG('Local SDP: %s', sdp);
                this._sendJoin(sdp);
            })
            .catch((reason) => {
                this._localTerminate(reason);
            });
    }

    _handleEvent(message) {
        DEBUG('Conference event: %o', message);
        switch (message.event) {
            case 'session-state':
                let oldState = this._state;
                let newState = message.state;
                this._state = newState;

                if (newState === 'accepted') {
                    this.emit('stateChanged', oldState, newState, {});
                    const sdp = utils.mungeSdp(message.sdp);
                    DEBUG('Remote SDP: %s', sdp);
                    this._setup_in_progress = true;
                    this._pc.setRemoteDescription(
                        new RTCSessionDescription({type: 'answer', sdp: sdp}),
                        // success
                        () => {
                            this._setup_in_progress = false;
                            if (!this._terminated) {
                                if (this._delay_established) {
                                    oldState = this._state;
                                    this._state = 'established';
                                    DEBUG('Setting delayed established state!');
                                    this.emit('stateChanged', oldState, this._state, {});
                                    this._delay_established = false;
                                }
                                DEBUG('Conference accepted');
                                if (this._initialParticipants.length > 0 ) {
                                    setTimeout(() => {
                                            this.inviteParticipants(this._initialParticipants);
                                    }, 50);
                                }
                            }
                        },
                        // failure
                        (error) => {
                            DEBUG('Error processing conference accept: %s', error);
                            this.terminate();
                        }
                    );
                } else if (newState === 'established') {
                    if (this._setup_in_progress) {
                        this._delay_established = true;
                    } else {
                        this.emit('stateChanged', oldState, newState, {});
                    }
                } else if (newState === 'terminated') {
                    this.emit('stateChanged', oldState, newState, {reason: message.reason});
                    this._terminated = true;
                    this._close();
                } else {
                    this.emit('stateChanged', oldState, newState, {});
                }
                break;
            case 'initial-publishers':
                // this comes between 'accepted' and 'established' states
                for (let p of message.publishers) {
                    participant = new Participant(p.id, new utils.Identity(p.uri, p.display_name), this);
                    this._participants.set(participant.id, participant);
                    this._participants.set(p.id, participant);
                }
                break;
            case 'publishers-joined':
                for (let p of message.publishers) {
                    DEBUG('Participant joined: %o', p);
                    participant = new Participant(p.id, new utils.Identity(p.uri, p.display_name), this);
                    this._participants.set(participant.id, participant);
                    this._participants.set(p.id, participant);
                    this.emit('participantJoined', participant);
                }
                break;
            case 'publishers-left':
                for (let pId of message.publishers) {
                    participant = this._participants.get(pId);
                    if (participant) {
                        this._participants.delete(participant.id);
                        this._participants.delete(pId);
                        this.emit('participantLeft', participant);
                    }
                }
                break;
            case 'feed-attached':
                participant = this._participants.get(message.feed);
                if (participant) {
                    participant._handleOffer(message.sdp);
                }
                break;
            case 'feed-established':
                participant = this._participants.get(message.feed);
                if (participant) {
                    participant._setState('established');
                }
                break;
            case 'configure':
                let activeParticipants = [];
                let originator;
                const mappedOriginator = this._participants.get(message.originator);

                if (mappedOriginator) {
                    originator = mappedOriginator.identity;
                } else if (message.originator === this.id) {
                    originator = this.localIdentity;
                } else if (message.originator === 'videoroom'){
                    originator = message.originator;
                }

                for (let pId of message.active_participants) {
                    participant = this._participants.get(pId);
                    if (participant) {
                        activeParticipants.push(participant);
                    } else if (pId === this.id) {
                        activeParticipants.push({
                            id: this.id,
                            publisherId: this.id,
                            identity: this.localIdentity,
                            streams: this.getLocalStreams()
                        });
                    }
                }
                this._activeParticpants = activeParticipants;
                let roomConfig = {originator: originator, activeParticipants: this._activeParticpants};
                this.emit('roomConfigured', roomConfig);
                break;
            default:
                break;
        }
    }

    _sendConfigureRoom(ps, cb = null) {
{'fraction_non_alphanumeric': 0.11065876036227394, 'fraction_numerical': 0.000531020444287105, 'mean_word_length': 1.3706552905797609, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 22, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7103746', 'n_tokens_mistral': 8926, 'n_tokens_neox': 8426, 'n_words': 1798}