diff options
Diffstat (limited to 'net/webrtc/gstwebrtc-api/src/producer-session.js')
-rw-r--r-- | net/webrtc/gstwebrtc-api/src/producer-session.js | 279 |
1 files changed, 279 insertions, 0 deletions
diff --git a/net/webrtc/gstwebrtc-api/src/producer-session.js b/net/webrtc/gstwebrtc-api/src/producer-session.js new file mode 100644 index 00000000..e278bdd1 --- /dev/null +++ b/net/webrtc/gstwebrtc-api/src/producer-session.js @@ -0,0 +1,279 @@ +/* + * gstwebrtc-api + * + * Copyright (C) 2022 Igalia S.L. <info@igalia.com> + * Author: Loïc Le Page <llepage@igalia.com> + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import WebRTCSession from "./webrtc-session"; +import SessionState from "./session-state"; + +/** + * @class gstWebRTCAPI.ClientSession + * @hideconstructor + * @classdesc Client session representing a link between a remote consumer and a local producer session. + * @extends {gstWebRTCAPI.WebRTCSession} + */ +class ClientSession extends WebRTCSession { + constructor(peerId, sessionId, comChannel, stream) { + super(peerId, comChannel); + this._sessionId = sessionId; + this._state = SessionState.streaming; + + const connection = new RTCPeerConnection(this._comChannel.webrtcConfig); + this._rtcPeerConnection = connection; + + for (const track of stream.getTracks()) { + connection.addTrack(track, stream); + } + + connection.onicecandidate = (event) => { + if ((this._rtcPeerConnection === connection) && event.candidate && this._comChannel) { + this._comChannel.send({ + type: "peer", + sessionId: this._sessionId, + ice: event.candidate.toJSON() + }); + } + }; + + this.dispatchEvent(new Event("rtcPeerConnectionChanged")); + + connection.setLocalDescription().then(() => { + if ((this._rtcPeerConnection === connection) && this._comChannel) { + const sdp = { + type: "peer", + sessionId: this._sessionId, + sdp: this._rtcPeerConnection.localDescription.toJSON() + }; + if (!this._comChannel.send(sdp)) { + throw new Error("cannot send local SDP configuration to WebRTC peer"); + } + } + }).catch((ex) => { + if (this._state !== SessionState.closed) { + this.dispatchEvent(new ErrorEvent("error", { + message: "an unrecoverable error occurred during SDP handshake", + error: ex + })); + + this.close(); + } + }); + } + + onSessionPeerMessage(msg) { + if ((this._state === SessionState.closed) || !this._rtcPeerConnection) { + return; + } + + if (msg.sdp) { + this._rtcPeerConnection.setRemoteDescription(msg.sdp).catch((ex) => { + if (this._state !== SessionState.closed) { + this.dispatchEvent(new ErrorEvent("error", { + message: "an unrecoverable error occurred during SDP handshake", + error: ex + })); + + this.close(); + } + }); + } else if (msg.ice) { + const candidate = new RTCIceCandidate(msg.ice); + this._rtcPeerConnection.addIceCandidate(candidate).catch((ex) => { + if (this._state !== SessionState.closed) { + this.dispatchEvent(new ErrorEvent("error", { + message: "an unrecoverable error occurred during ICE handshake", + error: ex + })); + + this.close(); + } + }); + } else { + throw new Error(`invalid empty peer message received from producer's client session ${this._peerId}`); + } + } +} + +/** + * Event name: "clientConsumerAdded".<br> + * Triggered when a remote consumer peer connects to a local {@link gstWebRTCAPI.ProducerSession}. + * @event gstWebRTCAPI#ClientConsumerAddedEvent + * @type {external:CustomEvent} + * @property {gstWebRTCAPI.ClientSession} detail - The WebRTC session associated with the added consumer peer. + * @see gstWebRTCAPI.ProducerSession + */ +/** + * Event name: "clientConsumerRemoved".<br> + * Triggered when a remote consumer peer disconnects from a local {@link gstWebRTCAPI.ProducerSession}. + * @event gstWebRTCAPI#ClientConsumerRemovedEvent + * @type {external:CustomEvent} + * @property {gstWebRTCAPI.ClientSession} detail - The WebRTC session associated with the removed consumer peer. + * @see gstWebRTCAPI.ProducerSession + */ + +/** + * @class gstWebRTCAPI.ProducerSession + * @hideconstructor + * @classdesc Producer session managing the streaming out of a local {@link external:MediaStream}.<br> + * It manages all underlying WebRTC connections to each peer client consuming the stream. + * <p>Call {@link gstWebRTCAPI#createProducerSession} to create a ProducerSession instance.</p> + * @extends {external:EventTarget} + * @fires {@link gstWebRTCAPI#event:ErrorEvent} + * @fires {@link gstWebRTCAPI#event:StateChangedEvent} + * @fires {@link gstWebRTCAPI#event:ClosedEvent} + * @fires {@link gstWebRTCAPI#event:ClientConsumerAddedEvent} + * @fires {@link gstWebRTCAPI#event:ClientConsumerRemovedEvent} + */ +export default class ProducerSession extends EventTarget { + constructor(comChannel, stream) { + super(); + + this._comChannel = comChannel; + this._stream = stream; + this._state = SessionState.idle; + this._clientSessions = {}; + } + + /** + * The local stream produced out by this session. + * @member {external:MediaStream} gstWebRTCAPI.ProducerSession#stream + * @readonly + */ + get stream() { + return this._stream; + } + + /** + * The current producer session state. + * @member {gstWebRTCAPI.SessionState} gstWebRTCAPI.ProducerSession#state + * @readonly + */ + get state() { + return this._state; + } + + /** + * Starts the producer session.<br> + * This method must be called after creating the producer session in order to start streaming. It registers this + * producer session to the signaling server and gets ready to serve peer requests from consumers. + * <p>Even on success, streaming can fail later if any error occurs during or after connection. In order to know + * the effective streaming state, you should be listening to the [error]{@link gstWebRTCAPI#event:ErrorEvent}, + * [stateChanged]{@link gstWebRTCAPI#event:StateChangedEvent} and/or [closed]{@link gstWebRTCAPI#event:ClosedEvent} + * events.</p> + * @method gstWebRTCAPI.ProducerSession#start + * @returns {boolean} true in case of success (may fail later during or after connection) or false in case of + * immediate error (wrong session state or no connection to the signaling server). + */ + start() { + if (!this._comChannel || (this._state === SessionState.closed)) { + return false; + } + + if (this._state !== SessionState.idle) { + return true; + } + + const msg = { + type: "setPeerStatus", + roles: ["listener", "producer"], + meta: this._comChannel.meta + }; + if (!this._comChannel.send(msg)) { + this.dispatchEvent(new ErrorEvent("error", { + message: "cannot start producer session", + error: new Error("cannot register producer to signaling server") + })); + + this.close(); + return false; + } + + this._state = SessionState.connecting; + this.dispatchEvent(new Event("stateChanged")); + return true; + } + + /** + * Terminates the producer session.<br> + * It immediately disconnects all peer consumers attached to this producer session and unregisters the producer + * from the signaling server. + * @method gstWebRTCAPI.ProducerSession#close + */ + close() { + if (this._state !== SessionState.closed) { + for (const track of this._stream.getTracks()) { + track.stop(); + } + + if ((this._state !== SessionState.idle) && this._comChannel) { + this._comChannel.send({ + type: "setPeerStatus", + roles: ["listener"], + meta: this._comChannel.meta + }); + } + + this._state = SessionState.closed; + this.dispatchEvent(new Event("stateChanged")); + + this._comChannel = null; + this._stream = null; + + for (const clientSession of Object.values(this._clientSessions)) { + clientSession.close(); + } + this._clientSessions = {}; + + this.dispatchEvent(new Event("closed")); + } + } + + onProducerRegistered() { + if (this._state === SessionState.connecting) { + this._state = SessionState.streaming; + this.dispatchEvent(new Event("stateChanged")); + } + } + + onStartSessionMessage(msg) { + if (this._comChannel && this._stream && !(msg.sessionId in this._clientSessions)) { + const session = new ClientSession(msg.peerId, msg.sessionId, this._comChannel, this._stream); + this._clientSessions[msg.sessionId] = session; + + session.addEventListener("closed", (event) => { + const sessionId = event.target.sessionId; + if ((sessionId in this._clientSessions) && (this._clientSessions[sessionId] === session)) { + delete this._clientSessions[sessionId]; + this.dispatchEvent(new CustomEvent("clientConsumerRemoved", { detail: session })); + } + }); + + session.addEventListener("error", (event) => { + this.dispatchEvent(new ErrorEvent("error", { + message: `error from client consumer ${event.target.peerId}: ${event.message}`, + error: event.error + })); + }); + + this.dispatchEvent(new CustomEvent("clientConsumerAdded", { detail: session })); + } + } + + onEndSessionMessage(msg) { + if (msg.sessionId in this._clientSessions) { + this._clientSessions[msg.sessionId].close(); + } + } + + onSessionPeerMessage(msg) { + if (msg.sessionId in this._clientSessions) { + this._clientSessions[msg.sessionId].onSessionPeerMessage(msg); + } + } +} |