123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- import * as Reliable from "reliable";
- import { util } from "./util";
- import logger from "./logger";
- import {
- RTCPeerConnection,
- RTCSessionDescription,
- RTCIceCandidate
- } from "./adapter";
- import { MediaConnection } from "./mediaconnection";
- import { DataConnection } from "./dataconnection";
- import { ConnectionType, PeerErrorType, ConnectionEventType, ServerMessageType } from "./enums";
- import { BaseConnection } from "./baseconnection";
- /**
- * Manages all negotiations between Peers.
- */
- export class Negotiator {
- constructor(readonly connection: BaseConnection) { }
- /** Returns a PeerConnection object set up correctly (for data, media). */
- startConnection(options: any) {
- const peerConnection = this._startPeerConnection();
- // Set the connection's PC.
- this.connection.peerConnection = peerConnection;
- if (this.connection.type === ConnectionType.Media && options._stream) {
- this._addTracksToConnection(options._stream, peerConnection);
- }
- // What do we need to do now?
- if (options.originator) {
- if (this.connection.type === ConnectionType.Data) {
- const dataConnection = <DataConnection>this.connection;
- let config = {};
- if (!util.supports.sctp) {
- config = { reliable: options.reliable };
- }
- const dataChannel = peerConnection.createDataChannel(
- dataConnection.label,
- config
- );
- dataConnection.initialize(dataChannel);
- }
- this._makeOffer();
- } else {
- this.handleSDP("OFFER", options.sdp);
- }
- }
- /** Start a PC. */
- private _startPeerConnection(): RTCPeerConnection {
- logger.log("Creating RTCPeerConnection.");
- let optional = {};
- if (this.connection.type === ConnectionType.Data && !util.supports.sctp) {
- optional = { optional: [{ RtpDataChannels: true }] };
- } else if (this.connection.type === ConnectionType.Media) {
- // Interop req for chrome.
- optional = { optional: [{ DtlsSrtpKeyAgreement: true }] };
- }
- const peerConnection = new RTCPeerConnection(
- this.connection.provider.options.config,
- optional
- );
- this._setupListeners(peerConnection);
- return peerConnection;
- }
- /** Set up various WebRTC listeners. */
- private _setupListeners(
- peerConnection: RTCPeerConnection
- ) {
- const peerId = this.connection.peer;
- const connectionId = this.connection.connectionId;
- const connectionType = this.connection.type;
- const provider = this.connection.provider;
- // ICE CANDIDATES.
- logger.log("Listening for ICE candidates.");
- peerConnection.onicecandidate = (evt) => {
- if (evt.candidate) {
- logger.log("Received ICE candidates for:", peerId);
- provider.socket.send({
- type: ServerMessageType.Candidate,
- payload: {
- candidate: evt.candidate,
- type: connectionType,
- connectionId: connectionId
- },
- dst: peerId
- });
- }
- };
- peerConnection.oniceconnectionstatechange = () => {
- switch (peerConnection.iceConnectionState) {
- case "failed":
- logger.log(
- "iceConnectionState is failed, closing connections to " +
- peerId
- );
- this.connection.emit(
- ConnectionEventType.Error,
- new Error("Negotiation of connection to " + peerId + " failed.")
- );
- this.connection.close();
- break;
- case "closed":
- logger.log(
- "iceConnectionState is closed, closing connections to " +
- peerId
- );
- this.connection.emit(
- ConnectionEventType.Error,
- new Error("Negotiation of connection to " + peerId + " failed.")
- );
- this.connection.close();
- break;
- case "disconnected":
- logger.log(
- "iceConnectionState is disconnected, closing connections to " +
- peerId
- );
- break;
- case "completed":
- peerConnection.onicecandidate = util.noop;
- break;
- }
- this.connection.emit(ConnectionEventType.IceStateChanged, peerConnection.iceConnectionState);
- };
- // DATACONNECTION.
- logger.log("Listening for data channel");
- // Fired between offer and answer, so options should already be saved
- // in the options hash.
- peerConnection.ondatachannel = (evt) => {
- logger.log("Received data channel");
- const dataChannel = evt.channel;
- const connection = <DataConnection>(
- provider.getConnection(peerId, connectionId)
- );
- connection.initialize(dataChannel);
- };
- // MEDIACONNECTION.
- logger.log("Listening for remote stream");
- peerConnection.ontrack = (evt) => {
- logger.log("Received remote stream");
- const stream = evt.streams[0];
- const connection = provider.getConnection(peerId, connectionId);
- if (connection.type === ConnectionType.Media) {
- const mediaConnection = <MediaConnection>connection;
- this._addStreamToMediaConnection(stream, mediaConnection);
- }
- };
- }
- cleanup(): void {
- logger.log("Cleaning up PeerConnection to " + this.connection.peer);
- const peerConnection = this.connection.peerConnection;
- if (!peerConnection) {
- return;
- }
- this.connection.peerConnection = null;
- //unsubscribe from all PeerConnection's events
- peerConnection.onicecandidate = peerConnection.oniceconnectionstatechange = peerConnection.ondatachannel = peerConnection.ontrack = () => { };
- const peerConnectionNotClosed = peerConnection.signalingState !== "closed";
- let dataChannelNotClosed = false;
- if (this.connection.type === ConnectionType.Data) {
- const dataConnection = <DataConnection>this.connection;
- const dataChannel = dataConnection.dataChannel;
- if (dataChannel) {
- dataChannelNotClosed = !!dataChannel.readyState && dataChannel.readyState !== "closed";
- }
- }
- if (peerConnectionNotClosed || dataChannelNotClosed) {
- peerConnection.close();
- }
- }
- private async _makeOffer(): Promise<void> {
- const peerConnection = this.connection.peerConnection;
- const provider = this.connection.provider;
- try {
- const offer = await peerConnection.createOffer(
- this.connection.options.constraints
- );
- logger.log("Created offer.");
- if (!util.supports.sctp && this.connection.type === ConnectionType.Data) {
- const dataConnection = <DataConnection>this.connection;
- if (dataConnection.reliable) {
- offer.sdp = Reliable.higherBandwidthSDP(offer.sdp);
- }
- }
- if (this.connection.options.sdpTransform && typeof this.connection.options.sdpTransform === 'function') {
- offer.sdp = this.connection.options.sdpTransform(offer.sdp) || offer.sdp;
- }
- try {
- await peerConnection.setLocalDescription(offer);
- logger.log("Set localDescription:", offer, `for:${this.connection.peer}`);
- let payload: any = {
- sdp: offer,
- type: this.connection.type,
- connectionId: this.connection.connectionId,
- metadata: this.connection.metadata,
- browser: util.browser
- };
- if (this.connection.type === ConnectionType.Data) {
- const dataConnection = <DataConnection>this.connection;
- payload = {
- ...payload,
- label: dataConnection.label,
- reliable: dataConnection.reliable,
- serialization: dataConnection.serialization
- };
- }
- provider.socket.send({
- type: ServerMessageType.Offer,
- payload,
- dst: this.connection.peer
- });
- } catch (err) {
- // TODO: investigate why _makeOffer is being called from the answer
- if (
- err !=
- "OperationError: Failed to set local offer sdp: Called in wrong state: kHaveRemoteOffer"
- ) {
- provider.emitError(PeerErrorType.WebRTC, err);
- logger.log("Failed to setLocalDescription, ", err);
- }
- }
- } catch (err_1) {
- provider.emitError(PeerErrorType.WebRTC, err_1);
- logger.log("Failed to createOffer, ", err_1);
- }
- }
- private async _makeAnswer(): Promise<void> {
- const peerConnection = this.connection.peerConnection;
- const provider = this.connection.provider;
- try {
- const answer = await peerConnection.createAnswer();
- logger.log("Created answer.");
- if (!util.supports.sctp && this.connection.type === ConnectionType.Data) {
- const dataConnection = <DataConnection>this.connection;
- if (dataConnection.reliable) {
- answer.sdp = Reliable.higherBandwidthSDP(answer.sdp);
- }
- }
- if (this.connection.options.sdpTransform && typeof this.connection.options.sdpTransform === 'function') {
- answer.sdp = this.connection.options.sdpTransform(answer.sdp) || answer.sdp;
- }
- try {
- await peerConnection.setLocalDescription(answer);
- logger.log(`Set localDescription:`, answer, `for:${this.connection.peer}`);
- provider.socket.send({
- type: ServerMessageType.Answer,
- payload: {
- sdp: answer,
- type: this.connection.type,
- connectionId: this.connection.connectionId,
- browser: util.browser
- },
- dst: this.connection.peer
- });
- } catch (err) {
- provider.emitError(PeerErrorType.WebRTC, err);
- logger.log("Failed to setLocalDescription, ", err);
- }
- } catch (err_1) {
- provider.emitError(PeerErrorType.WebRTC, err_1);
- logger.log("Failed to create answer, ", err_1);
- }
- }
- /** Handle an SDP. */
- async handleSDP(
- type: string,
- sdp: any
- ): Promise<void> {
- sdp = new RTCSessionDescription(sdp);
- const peerConnection = this.connection.peerConnection;
- const provider = this.connection.provider;
- logger.log("Setting remote description", sdp);
- const self = this;
- try {
- await peerConnection.setRemoteDescription(sdp);
- logger.log(`Set remoteDescription:${type} for:${this.connection.peer}`);
- if (type === "OFFER") {
- await self._makeAnswer();
- }
- } catch (err) {
- provider.emitError(PeerErrorType.WebRTC, err);
- logger.log("Failed to setRemoteDescription, ", err);
- }
- }
- /** Handle a candidate. */
- async handleCandidate(ice: any): Promise<void> {
- const candidate = ice.candidate;
- const sdpMLineIndex = ice.sdpMLineIndex;
- const peerConnection = this.connection.peerConnection;
- const provider = this.connection.provider;
- try {
- await peerConnection.addIceCandidate(
- new RTCIceCandidate({
- sdpMLineIndex: sdpMLineIndex,
- candidate: candidate
- })
- );
- logger.log(`Added ICE candidate for:${this.connection.peer}`);
- } catch (err) {
- provider.emitError(PeerErrorType.WebRTC, err);
- logger.log("Failed to handleCandidate, ", err);
- }
- }
- private _addTracksToConnection(
- stream: MediaStream,
- peerConnection: RTCPeerConnection
- ): void {
- logger.log(`add tracks from stream ${stream.id} to peer connection`);
- if (!peerConnection.addTrack) {
- return logger.error(
- `Your browser does't support RTCPeerConnection#addTrack. Ignored.`
- );
- }
- stream.getTracks().forEach(track => {
- peerConnection.addTrack(track, stream);
- });
- }
- private _addStreamToMediaConnection(
- stream: MediaStream,
- mediaConnection: MediaConnection
- ): void {
- logger.log(
- `add stream ${stream.id} to media connection ${
- mediaConnection.connectionId
- }`
- );
- mediaConnection.addStream(stream);
- }
- }
|