/* * gstwebrtc-api * * Copyright (C) 2022 Igalia S.L. * Author: Loïc Le Page * * 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.js"; import SessionState from "./session-state.js"; import RemoteController from "./remote-controller.js"; /** * Event name: "streamsChanged".
* Triggered when the underlying media streams of a {@link GstWebRTCAPI.ConsumerSession} change. * @event GstWebRTCAPI#StreamsChangedEvent * @type {external:Event} * @see GstWebRTCAPI.ConsumerSession#streams */ /** * Event name: "remoteControllerChanged".
* Triggered when the underlying remote controller of a {@link GstWebRTCAPI.ConsumerSession} changes. * @event GstWebRTCAPI#RemoteControllerChangedEvent * @type {external:Event} * @see GstWebRTCAPI.ConsumerSession#remoteController */ /** * @class GstWebRTCAPI.ConsumerSession * @hideconstructor * @classdesc Consumer session managing a peer-to-peer WebRTC channel between a remote producer and this client * instance. *

Call {@link GstWebRTCAPI#createConsumerSession} to create a ConsumerSession instance.

* @extends {GstWebRTCAPI.WebRTCSession} * @fires {@link GstWebRTCAPI#event:StreamsChangedEvent} * @fires {@link GstWebRTCAPI#event:RemoteControllerChangedEvent} */ export default class ConsumerSession extends WebRTCSession { constructor(peerId, comChannel) { super(peerId, comChannel); this._streams = []; this._remoteController = null; this.addEventListener("closed", () => { this._streams = []; if (this._remoteController) { this._remoteController.close(); } }); } /** * The array of remote media streams consumed locally through this WebRTC channel. * @member {external:MediaStream[]} GstWebRTCAPI.ConsumerSession#streams * @readonly */ get streams() { return this._streams; } /** * The remote controller associated with this WebRTC consumer session. Value may be null if consumer session * has no remote controller. * @member {GstWebRTCAPI.RemoteController} GstWebRTCAPI.ConsumerSession#remoteController * @readonly */ get remoteController() { return this._remoteController; } /** * Connects the consumer session to its remote producer.
* This method must be called after creating the consumer session in order to start receiving the remote streams. * It registers this consumer session to the signaling server and gets ready to receive audio/video streams. *

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.

* @method GstWebRTCAPI.ConsumerSession#connect * @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). */ connect() { if (!this._comChannel || (this._state === SessionState.closed)) { return false; } if (this._state !== SessionState.idle) { return true; } const msg = { type: "startSession", peerId: this._peerId }; if (!this._comChannel.send(msg)) { this.dispatchEvent(new ErrorEvent("error", { message: "cannot connect consumer session", error: new Error("cannot send startSession message to signaling server") })); this.close(); return false; } this._state = SessionState.connecting; this.dispatchEvent(new Event("stateChanged")); return true; } onSessionStarted(peerId, sessionId) { if ((this._peerId === peerId) && (this._state === SessionState.connecting) && !this._sessionId) { this._sessionId = sessionId; } } onSessionPeerMessage(msg) { if ((this._state === SessionState.closed) || !this._comChannel || !this._sessionId) { return; } if (!this._rtcPeerConnection) { const connection = new RTCPeerConnection(this._comChannel.webrtcConfig); this._rtcPeerConnection = connection; connection.ontrack = (event) => { if ((this._rtcPeerConnection === connection) && event.streams && (event.streams.length > 0)) { if (this._state === SessionState.connecting) { this._state = SessionState.streaming; this.dispatchEvent(new Event("stateChanged")); } let streamsChanged = false; for (const stream of event.streams) { if (!this._streams.includes(stream)) { this._streams.push(stream); streamsChanged = true; } } if (streamsChanged) { this.dispatchEvent(new Event("streamsChanged")); } } }; connection.ondatachannel = (event) => { const rtcDataChannel = event.channel; if (rtcDataChannel && (rtcDataChannel.label === "input")) { if (this._remoteController) { const previousController = this._remoteController; this._remoteController = null; previousController.close(); } const remoteController = new RemoteController(rtcDataChannel, this); this._remoteController = remoteController; this.dispatchEvent(new Event("remoteControllerChanged")); remoteController.addEventListener("closed", () => { if (this._remoteController === remoteController) { this._remoteController = null; this.dispatchEvent(new Event("remoteControllerChanged")); } }); } }; 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")); } if (msg.sdp) { this._rtcPeerConnection.setRemoteDescription(msg.sdp).then(() => { if (this._rtcPeerConnection) { return this._rtcPeerConnection.createAnswer(); } else { return null; } }).then((desc) => { if (this._rtcPeerConnection && desc) { return this._rtcPeerConnection.setLocalDescription(desc); } else { return null; } }).then(() => { if (this._rtcPeerConnection && 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(); } }); } 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 consumer session ${this._sessionId}`); } } }