Преглед на файлове

feat(recording): frontend logic can support live streaming and recording (#741)

* feat(recording): frontend logic can support live streaming and recording

Instead of either live streaming or recording, now both can live together.
The changes to facilitate such include the following:
- Remove the old recording.js module which allowed one recording at a time.
- Remove events for live stream url changes as the url is now part of a
  sesssion and not fired independently.
- Availability of sipgw and recording are gone. Instead sessions have a
  failure reason. For sipgw sessions, store that failure and emit it to
  listeners.
- Create a new recordingManager singleton that can start/stop sessions
  and handle updating known state of those sessions. Known state is
  emitted through one event.
- Create a JibriSession model to encapsulate state of a session.

* update comments, use map to store sessions

* always pass in focusmucjid

* try to fix jibrisession docs and remove default null
master
virtuacoplenny преди 7 години
родител
ревизия
fefd96e0e8

+ 17
- 41
JitsiConference.js Целия файл

@@ -1267,19 +1267,6 @@ JitsiConference.prototype.onDisplayNameChanged = function(jid, displayName) {
1267 1267
         displayName);
1268 1268
 };
1269 1269
 
1270
-/**
1271
- * Callback invoked when a known live stream URL has been updated.
1272
- *
1273
- * @params {*} ...args - Information regarding which participant has an updated
1274
- * live stream URL and what that live stream URL is.
1275
- * @returns {void}
1276
- */
1277
-JitsiConference.prototype.onLiveStreamURLChange = function(...args) {
1278
-    this.eventEmitter.emit(
1279
-        JitsiConferenceEvents.LIVE_STREAM_URL_CHANGED,
1280
-        ...args);
1281
-};
1282
-
1283 1270
 /**
1284 1271
  * Notifies this JitsiConference that a JitsiRemoteTrack was added into
1285 1272
  * the conference.
@@ -1748,44 +1735,33 @@ JitsiConference.prototype.sendTones = function(tones, duration, pause) {
1748 1735
 };
1749 1736
 
1750 1737
 /**
1751
- * Returns true if recording is supported and false if not.
1738
+ * Starts recording the current conference.
1739
+ *
1740
+ * @param {Object} options - Configuration for the recording. See
1741
+ * {@link Chatroom#startRecording} for more info.
1742
+ * @returns {Promise} See {@link Chatroom#startRecording} for more info.
1752 1743
  */
1753
-JitsiConference.prototype.isRecordingSupported = function() {
1744
+JitsiConference.prototype.startRecording = function(options) {
1754 1745
     if (this.room) {
1755
-        return this.room.isRecordingSupported();
1746
+        return this.room.startRecording(options);
1756 1747
     }
1757 1748
 
1758
-    return false;
1749
+    return Promise.reject(new Error('The conference is not created yet!'));
1759 1750
 };
1760 1751
 
1761 1752
 /**
1762
- * Returns null if the recording is not supported, "on" if the recording started
1763
- * and "off" if the recording is not started.
1764
- */
1765
-JitsiConference.prototype.getRecordingState = function() {
1766
-    return this.room ? this.room.getRecordingState() : undefined;
1767
-};
1768
-
1769
-/**
1770
- * Returns the url of the recorded video.
1771
- */
1772
-JitsiConference.prototype.getRecordingURL = function() {
1773
-    return this.room ? this.room.getRecordingURL() : null;
1774
-};
1775
-
1776
-/**
1777
- * Starts/stops the recording
1753
+ * Stop a recording session.
1754
+ *
1755
+ * @param {string} sessionID - The ID of the recording session that
1756
+ * should be stopped.
1757
+ * @returns {Promise} See {@link Chatroom#stopRecording} for more info.
1778 1758
  */
1779
-JitsiConference.prototype.toggleRecording = function(options) {
1759
+JitsiConference.prototype.stopRecording = function(sessionID) {
1780 1760
     if (this.room) {
1781
-        return this.room.toggleRecording(options, (status, error) => {
1782
-            this.eventEmitter.emit(
1783
-                JitsiConferenceEvents.RECORDER_STATE_CHANGED, status, error);
1784
-        });
1761
+        return this.room.stopRecording(sessionID);
1785 1762
     }
1786
-    this.eventEmitter.emit(
1787
-        JitsiConferenceEvents.RECORDER_STATE_CHANGED, 'error',
1788
-        new Error('The conference is not created yet!'));
1763
+
1764
+    return Promise.reject(new Error('The conference is not created yet!'));
1789 1765
 };
1790 1766
 
1791 1767
 /**

+ 3
- 8
JitsiConferenceEventManager.js Целия файл

@@ -266,24 +266,19 @@ JitsiConferenceEventManager.prototype.setupChatRoomListeners = function() {
266 266
     chatRoom.addListener(XMPPEvents.DISPLAY_NAME_CHANGED,
267 267
         conference.onDisplayNameChanged.bind(conference));
268 268
 
269
-    chatRoom.addListener(XMPPEvents.LIVE_STREAM_URL_CHANGE,
270
-        conference.onLiveStreamURLChange.bind(conference));
271
-
272 269
     chatRoom.addListener(XMPPEvents.LOCAL_ROLE_CHANGED, role => {
273 270
         conference.onLocalRoleChanged(role);
274 271
 
275 272
         // log all events for the recorder operated by the moderator
276 273
         if (conference.statistics && conference.isModerator()) {
277 274
             conference.on(JitsiConferenceEvents.RECORDER_STATE_CHANGED,
278
-                (status, error) => {
275
+                recorderSession => {
279 276
                     const logObject = {
277
+                        error: recorderSession.getError(),
280 278
                         id: 'recorder_status',
281
-                        status
279
+                        status: recorderSession.getStatus()
282 280
                     };
283 281
 
284
-                    if (error) {
285
-                        logObject.error = error;
286
-                    }
287 282
                     Statistics.sendLog(JSON.stringify(logObject));
288 283
                 });
289 284
         }

+ 0
- 5
JitsiConferenceEvents.js Целия файл

@@ -122,11 +122,6 @@ export const KICKED = 'conference.kicked';
122 122
  */
123 123
 export const LAST_N_ENDPOINTS_CHANGED = 'conference.lastNEndpointsChanged';
124 124
 
125
-/**
126
- * A known participant's live stream URL has changed.
127
- */
128
-export const LIVE_STREAM_URL_CHANGED = 'conference.liveStreamURLChanged';
129
-
130 125
 /**
131 126
  * Indicates that the room has been locked or unlocked.
132 127
  */

+ 2
- 5
JitsiMeetJS.js Целия файл

@@ -12,13 +12,11 @@ import * as JitsiConnectionErrors from './JitsiConnectionErrors';
12 12
 import * as JitsiConnectionEvents from './JitsiConnectionEvents';
13 13
 import JitsiMediaDevices from './JitsiMediaDevices';
14 14
 import * as JitsiMediaDevicesEvents from './JitsiMediaDevicesEvents';
15
-import JitsiRecorderErrors from './JitsiRecorderErrors';
16 15
 import JitsiTrackError from './JitsiTrackError';
17 16
 import * as JitsiTrackErrors from './JitsiTrackErrors';
18 17
 import * as JitsiTrackEvents from './JitsiTrackEvents';
19 18
 import * as JitsiTranscriptionStatus from './JitsiTranscriptionStatus';
20 19
 import LocalStatsCollector from './modules/statistics/LocalStatsCollector';
21
-import Recording from './modules/xmpp/recording';
22 20
 import Logger from 'jitsi-meet-logger';
23 21
 import * as MediaType from './service/RTC/MediaType';
24 22
 import Resolutions from './service/RTC/Resolutions';
@@ -28,6 +26,7 @@ import RTC from './modules/RTC/RTC';
28 26
 import browser from './modules/browser';
29 27
 import RTCUIHelper from './modules/RTC/RTCUIHelper';
30 28
 import ScriptUtil from './modules/util/ScriptUtil';
29
+import recordingConstants from './modules/recording/recordingConstants';
31 30
 import Statistics from './modules/statistics/statistics';
32 31
 import * as VideoSIPGWConstants from './modules/videosipgw/VideoSIPGWConstants';
33 32
 
@@ -132,8 +131,7 @@ export default _mergeNamespaceAndModule({
132 131
     JitsiConnection,
133 132
     constants: {
134 133
         participantConnectionStatus: ParticipantConnectionStatus,
135
-        recordingStatus: Recording.status,
136
-        recordingTypes: Recording.types,
134
+        recording: recordingConstants,
137 135
         sipVideoGW: VideoSIPGWConstants,
138 136
         transcriptionStatus: JitsiTranscriptionStatus
139 137
     },
@@ -147,7 +145,6 @@ export default _mergeNamespaceAndModule({
147 145
     errors: {
148 146
         conference: JitsiConferenceErrors,
149 147
         connection: JitsiConnectionErrors,
150
-        recorder: JitsiRecorderErrors,
151 148
         track: JitsiTrackErrors
152 149
     },
153 150
     errorTypes: {

+ 0
- 32
JitsiRecorderErrors.js Целия файл

@@ -1,32 +0,0 @@
1
-/**
2
- * Enumeration with the errors for the conference.
3
- * @type {{string: string}}
4
- */
5
-const JitsiRecorderErrors = {
6
-    /**
7
-     * Indicates that the recorder is currently unavailable.
8
-     */
9
-    RECORDER_UNAVAILABLE: 'recorder.unavailable',
10
-
11
-    /**
12
-     * Indicates that all available recorders are currently busy.
13
-     */
14
-    RECORDER_BUSY: 'recorder.busy',
15
-
16
-    /**
17
-     * Indicates that the authentication token is missing.
18
-     */
19
-    NO_TOKEN: 'recorder.noToken',
20
-
21
-    /**
22
-     * Indicates that a state change failed.
23
-     */
24
-    STATE_CHANGE_FAILED: 'recorder.stateChangeFailed',
25
-
26
-    /**
27
-     * Indicates an invalid state.
28
-     */
29
-    INVALID_STATE: 'recorder.invalidState'
30
-};
31
-
32
-module.exports = JitsiRecorderErrors;

+ 0
- 7
doc/example/example.js Целия файл

@@ -150,13 +150,6 @@ function onConnectionSuccess() {
150 150
     room.on(
151 151
         JitsiMeetJS.events.conference.TRACK_AUDIO_LEVEL_CHANGED,
152 152
         (userID, audioLevel) => console.log(`${userID} - ${audioLevel}`));
153
-    room.on(
154
-        JitsiMeetJS.events.conference.RECORDER_STATE_CHANGED,
155
-        () =>
156
-            console.log(
157
-                `${room.isRecordingSupported()} - ${
158
-                    room.getRecordingState()} - ${
159
-                    room.getRecordingURL()}`));
160 153
     room.on(
161 154
         JitsiMeetJS.events.conference.PHONE_NUMBER_CHANGED,
162 155
         () => console.log(`${room.getPhoneNumber()} - ${room.getPhonePin()}`));

+ 216
- 0
modules/recording/JibriSession.js Целия файл

@@ -0,0 +1,216 @@
1
+import { $iq } from 'strophe.js';
2
+
3
+import recordingXMLUtils from './recordingXMLUtils';
4
+
5
+/**
6
+ * Represents a recording session.
7
+ */
8
+export default class JibriSession {
9
+    /**
10
+     * Initializes a new JibriSession instance.
11
+     *
12
+     * @constructor
13
+     */
14
+    constructor(options = {}) {
15
+        this._connection = options.connection;
16
+        this._mode = options.mode;
17
+
18
+        this._setSessionID(options.sessionID);
19
+        this.setStatus(options.status);
20
+    }
21
+
22
+    /**
23
+     * Returns the error related to the session instance, if any.
24
+     *
25
+     * @returns {string|undefined}
26
+     */
27
+    getError() {
28
+        return this._error;
29
+    }
30
+
31
+    /**
32
+     * Returns the session ID of the session instance.
33
+     *
34
+     * @returns {string|undefined}
35
+     */
36
+    getID() {
37
+        return this._sessionID;
38
+    }
39
+
40
+    /**
41
+     * Returns the streaming URL of the session.
42
+     *
43
+     * @returns {string|undefined}
44
+     */
45
+    getLiveStreamViewURL() {
46
+        return this._liveStreamViewURL;
47
+    }
48
+
49
+    /**
50
+     * Returns the current status of the session.
51
+     *
52
+     * @returns {string|undefined}
53
+     */
54
+    getStatus() {
55
+        return this._status;
56
+    }
57
+
58
+    /**
59
+     * Returns the current recording mode of the session, such as "file".
60
+     *
61
+     * @returns {string}
62
+     */
63
+    getMode() {
64
+        return this._mode;
65
+    }
66
+
67
+    /**
68
+     * Sets the last known error message related to the session.
69
+     *
70
+     * @param {string} error - The error string explaining why the session
71
+     * entered an error state.
72
+     * @returns {void}
73
+     */
74
+    setError(error) {
75
+        this._error = error;
76
+    }
77
+
78
+    /**
79
+     * Sets the last live stream URL for the session instance. Usually this is
80
+     * a YouTube URL and usually this is only set for "stream" sessions.
81
+     *
82
+     * @param {string} url - The live stream URL associated with the session.
83
+     * @returns {void}
84
+     */
85
+    setLiveStreamViewURL(url) {
86
+        this._liveStreamViewURL = url;
87
+    }
88
+
89
+    /**
90
+     * Sets the last known status for this recording session.
91
+     *
92
+     * @param {string} status - The new status to set.
93
+     * @returns {void}
94
+     */
95
+    setStatus(status) {
96
+        this._status = status;
97
+    }
98
+
99
+    /**
100
+     * Sends a message to start the actual recording.
101
+     *
102
+     * @param {Object} options - Additional arguments for starting the
103
+     * recording.
104
+     * @param {string} [optional] options.broadcastId - The broadcast ID of an
105
+     * associated YouTube stream, used for knowing the URL from which the stream
106
+     * can be viewed.
107
+     * @param {string} options.focusMucJid - The JID of the focus participant
108
+     * that controls recording.
109
+     * @param {streamId} options.streamId - Necessary for live streaming, this
110
+     * is the the stream key needed to start a live streaming session with the
111
+     * streaming service provider.
112
+     * @returns Promise
113
+     */
114
+    start({ broadcastId, focusMucJid, streamId }) {
115
+        return new Promise((resolve, reject) => {
116
+            this._connection.sendIQ(
117
+                this._createIQ({
118
+                    action: 'start',
119
+                    focusMucJid,
120
+                    broadcastId,
121
+                    streamId
122
+                }),
123
+                result => {
124
+                    // All users will eventually receive the 'pending' status
125
+                    // from the backend, but for the user initiating the session
126
+                    // it's better to give some instant feedback that recording
127
+                    // is starting so fire 'pending' here manually.
128
+                    this.setStatus('pending');
129
+                    this._setSessionID(
130
+                        recordingXMLUtils.getSessionIdFromIq(result));
131
+
132
+                    resolve();
133
+                },
134
+                error => {
135
+                    this._setErrorFromIq(error);
136
+
137
+                    reject(error);
138
+                });
139
+        });
140
+    }
141
+
142
+    /**
143
+     * Sends a message to actually stop the recording session.
144
+     *
145
+     * @param {Object} options - Additional arguments for stopping the
146
+     * recording.
147
+     * @param {Object} options.focusMucJid - The JID of the focus participant
148
+     * that controls recording.
149
+     * @returns Promise
150
+     */
151
+    stop({ focusMucJid }) {
152
+        return new Promise((resolve, reject) => {
153
+            this._connection.sendIQ(
154
+                this._createIQ({
155
+                    action: 'stop',
156
+                    focusMucJid
157
+                }),
158
+                resolve,
159
+                reject);
160
+        });
161
+    }
162
+
163
+    /**
164
+     * Generates the message to change the status of the recording session.
165
+     *
166
+     * @param {string} status - The new status to which the recording session
167
+     * should transition.
168
+     * @param {string} [optional] options.broadcastId - The broadcast ID of an
169
+     * associated YouTube stream, used for knowing the URL from which the stream
170
+     * can be viewed.
171
+     * @param {string} options.focusMucJid - The JID of the focus participant
172
+     * that controls recording.
173
+     * @param {streamId} options.streamId - Necessary for live streaming, this
174
+     * is the the stream key needed to start a live streaming session with the
175
+     * streaming service provider.
176
+     * @returns Object - The XMPP IQ message.
177
+     */
178
+    _createIQ({ action, broadcastId, focusMucJid, streamId }) {
179
+        return $iq({
180
+            to: focusMucJid,
181
+            type: 'set'
182
+        })
183
+        .c('jibri', {
184
+            'xmlns': 'http://jitsi.org/protocol/jibri',
185
+            'action': action,
186
+            'recording_mode': this._mode,
187
+            'streamid': streamId,
188
+            'you_tube_broadcast_id': broadcastId
189
+        })
190
+        .up();
191
+    }
192
+
193
+    /**
194
+     * Handles the error from an iq and stores the error.
195
+     *
196
+     * @param {Node} errorIq - The error response from an Iq.
197
+     * @private
198
+     * @returns {void}
199
+     */
200
+    _setErrorFromIq(errorIq) {
201
+        const error = errorIq.getElementsByTagName('error')[0];
202
+
203
+        this.setError(error.children[0].tagName);
204
+    }
205
+
206
+    /**
207
+     * Sets the known session ID for this recording session.
208
+     *
209
+     * @param {string} sessionID
210
+     * @private
211
+     * @returns {void}
212
+     */
213
+    _setSessionID(sessionID) {
214
+        this._sessionID = sessionID;
215
+    }
216
+}

+ 17
- 0
modules/recording/recordingConstants.js Целия файл

@@ -0,0 +1,17 @@
1
+export default {
2
+    error: {
3
+        BUSY: 'busy',
4
+        ERROR: 'error',
5
+        RESOURCE_CONSTRAINT: 'resource-constraint',
6
+        SERVICE_UNAVAILABLE: 'service-unavailable'
7
+    },
8
+    mode: {
9
+        FILE: 'file',
10
+        STREAM: 'stream'
11
+    },
12
+    status: {
13
+        OFF: 'off',
14
+        ON: 'on',
15
+        PENDING: 'pending'
16
+    }
17
+};

+ 257
- 0
modules/recording/recordingManager.js Целия файл

@@ -0,0 +1,257 @@
1
+import { getLogger } from 'jitsi-meet-logger';
2
+
3
+import XMPPEvents from '../../service/xmpp/XMPPEvents';
4
+
5
+import JibriSession from './JibriSession';
6
+import recordingXMLUtils from './recordingXMLUtils';
7
+
8
+const logger = getLogger(__filename);
9
+
10
+/**
11
+ * A singleton responsible for starting and stopping recording sessions and
12
+ * emitting state updates for them.
13
+ */
14
+const recordingManager = {
15
+    /**
16
+     * All known recording sessions from the current conference.
17
+     */
18
+    _sessions: {},
19
+
20
+    /**
21
+     * Initialize recordingManager with other objects that are necessary for
22
+     * starting a recording.
23
+     *
24
+     * @param {Object} eventEmitter - The eventEmitter to be used for
25
+     * broadcasting recording state changes.
26
+     * @param {Object} connection - The MUC connection used for sending out
27
+     * messages regarding recording.
28
+     * @param {string} focusMucJid - The ID of the conference (MUC) the focus
29
+     * is in.
30
+     * @returns {void}
31
+     */
32
+    init(eventEmitter, connection, focusMucJid) {
33
+        this._eventEmitter = eventEmitter;
34
+        this._connection = connection;
35
+        this._focusMucJid = focusMucJid;
36
+    },
37
+
38
+    /**
39
+     * Finds an existing recording session by session ID.
40
+     *
41
+     * @param {string} sessionID - The session ID associated with the recording.
42
+     * @returns {JibriSession|undefined}
43
+     */
44
+    getSession(sessionID) {
45
+        return this._sessions[sessionID];
46
+    },
47
+
48
+    /**
49
+     * Callback to invoke to parse through a presence update to find recording
50
+     * related updates (from Jibri participant doing the recording and the
51
+     * focus which controls recording).
52
+     *
53
+     * @param {Node} presence - An XMPP presence update.
54
+     * @param {boolean} isHiddenDomain - Whether or not the presence update
55
+     * comes from a participant that is trusted but not visible, as would be the
56
+     * case with the Jibri recorder participant.
57
+     * @returns {void}
58
+     */
59
+    onPresence(presence, isHiddenDomain) {
60
+        if (recordingXMLUtils.isFromFocus(presence)) {
61
+            this._handleFocusPresence(presence);
62
+        } else if (isHiddenDomain) {
63
+            this._handleJibriPresence(presence);
64
+        }
65
+    },
66
+
67
+    /**
68
+     * Sets the currently known ID of the conference (MUC). This method exists
69
+     * in case the ID is not known at init time.
70
+     *
71
+     * @param {string} focusMucJid - The ID of the conference (MUC) the focus
72
+     * is in.
73
+     * @returns {void}
74
+     */
75
+    setFocusMucJid(focusMucJid) {
76
+        this._focusMucJid = focusMucJid;
77
+    },
78
+
79
+    /**
80
+     * Start a recording session.
81
+     *
82
+     * @param {Object} options - Configuration for the recording.
83
+     * @param {string} [optional] options.broadcastId - The channel on which a
84
+     * live stream will occur.
85
+     * @param {string} options.mode - The mode in which recording should be
86
+     * started. Recognized values are "file" and "stream".
87
+     * @param {string} [optional] options.streamId - The stream key to be used
88
+     * for live stream broadcasting. Required for live streaming.
89
+     * @returns {Promise} A promise for starting a recording, which will pass
90
+     * back the session on success. The promise resolves after receiving an
91
+     * acknowledgment of the start request success or fail.
92
+     */
93
+    startRecording(options) {
94
+        const session = new JibriSession({
95
+            ...options,
96
+            connection: this._connection
97
+        });
98
+
99
+        return session.start({
100
+            broadcastId: options.broadcastId,
101
+            focusMucJid: this._focusMucJid,
102
+            streamId: options.streamId
103
+        })
104
+            .then(() => {
105
+                // Only store the session and emit if the session has not been
106
+                // added already. This is a workaround for the session getting
107
+                // created due to a presence update to announce a "pending"
108
+                // recording being received before JibriSession#start finishes.
109
+                if (!this.getSession(session.getID())) {
110
+                    this._addSession(session);
111
+                    this._emitSessionUpdate(session);
112
+                }
113
+
114
+                return session;
115
+            })
116
+            .catch(error => {
117
+                this._emitSessionUpdate(session);
118
+
119
+                return Promise.reject(error);
120
+            });
121
+    },
122
+
123
+    /**
124
+     * Stop a recording session.
125
+     *
126
+     * @param {string} sessionID - The ID associated with the recording session
127
+     * to be stopped.
128
+     * @returns {Promise} The promise resolves after receiving an
129
+     * acknowledgment of the stop request success or fail.
130
+     */
131
+    stopRecording(sessionID) {
132
+        const session = this.getSession(sessionID);
133
+
134
+        if (session) {
135
+            return session.stop({ focusMucJid: this._focusMucJid });
136
+        }
137
+
138
+        return Promise.reject(new Error('Could not find session'));
139
+    },
140
+
141
+    /**
142
+     * Stores a reference to the passed in JibriSession.
143
+     *
144
+     * @param {string} session - The JibriSession instance to store.
145
+     * @returns {void}
146
+     */
147
+    _addSession(session) {
148
+        this._sessions[session.getID()] = session;
149
+    },
150
+
151
+    /**
152
+     * Create a new instance of a recording session and stores a reference to
153
+     * it.
154
+     *
155
+     * @param {string} sessionID - The session ID of the recording in progress.
156
+     * @param {string} status - The current status of the recording session.
157
+     * @param {string} mode - The recording mode of the session.
158
+     * @returns {JibriSession}
159
+     */
160
+    _createSession(sessionID, status, mode) {
161
+        const session = new JibriSession({
162
+            connection: this._connection,
163
+            focusMucJid: this._focusMucJid,
164
+            mode,
165
+            sessionID,
166
+            status
167
+        });
168
+
169
+        this._addSession(session);
170
+
171
+        return session;
172
+    },
173
+
174
+    /**
175
+     * Notifies listeners of an update to a recording session.
176
+     *
177
+     * @param {JibriSession} session - The session that has been updated.
178
+     */
179
+    _emitSessionUpdate(session) {
180
+        this._eventEmitter.emit(XMPPEvents.RECORDER_STATE_CHANGED, session);
181
+    },
182
+
183
+    /**
184
+     * Parses presence to update an existing JibriSession or to create a new
185
+     * JibriSession.
186
+     *
187
+     * @param {Node} presence - An XMPP presence update.
188
+     * @returns {void}
189
+     */
190
+    _handleFocusPresence(presence) {
191
+        const jibriStatus = recordingXMLUtils.getFocusRecordingUpdate(presence);
192
+
193
+        if (!jibriStatus) {
194
+            return;
195
+        }
196
+
197
+        const { sessionID, status, error, recordingMode } = jibriStatus;
198
+
199
+        // We'll look for an existing session or create one (in case we're a
200
+        // participant joining a call with an existing recording going on).
201
+        let session = this.getSession(sessionID);
202
+
203
+        // Handle the case where a status update is received in presence but
204
+        // the local participant has joined while the JibriSession has already
205
+        // ended.
206
+        if (!session && status === 'off') {
207
+            logger.warn(
208
+                'Ignoring recording presence update',
209
+                'Received a new session with status off.');
210
+
211
+            return;
212
+        }
213
+
214
+        if (!session) {
215
+            session = this._createSession(sessionID, status, recordingMode);
216
+        }
217
+
218
+        session.setStatus(status);
219
+
220
+        if (error) {
221
+            session.setError(error);
222
+        }
223
+
224
+        this._emitSessionUpdate(session);
225
+    },
226
+
227
+    /**
228
+     * Handles updates from the Jibri which can broadcast a YouTube URL that
229
+     * needs to be updated in a JibriSession.
230
+     *
231
+     * @param {Node} presence - An XMPP presence update.
232
+     * @returns {void}
233
+     */
234
+    _handleJibriPresence(presence) {
235
+        const { liveStreamViewURL, mode, sessionID }
236
+            = recordingXMLUtils.getHiddenDomainUpdate(presence);
237
+
238
+        if (!sessionID) {
239
+            logger.warn(
240
+                'Ignoring potential jibri presence due to no session id.');
241
+
242
+            return;
243
+        }
244
+
245
+        let session = this.getSession(sessionID);
246
+
247
+        if (!session) {
248
+            session = this._createSession(sessionID, '', mode);
249
+        }
250
+
251
+        session.setLiveStreamViewURL(liveStreamViewURL);
252
+
253
+        this._emitSessionUpdate(session);
254
+    }
255
+};
256
+
257
+export default recordingManager;

+ 93
- 0
modules/recording/recordingXMLUtils.js Целия файл

@@ -0,0 +1,93 @@
1
+/**
2
+ * A collection of utility functions for taking in XML and parsing it to return
3
+ * certain values.
4
+ */
5
+export default {
6
+    /**
7
+     * Parses the presence update of the focus and returns an object with the
8
+     * statuses related to recording.
9
+     *
10
+     * @param {Node} presence - An XMPP presence update.
11
+     * @returns {Object} The current presence values related to recording.
12
+     */
13
+    getFocusRecordingUpdate(presence) {
14
+        const jibriStatus = presence
15
+            && presence.getElementsByTagName('jibri-recording-status')[0];
16
+
17
+        if (!jibriStatus) {
18
+            return;
19
+        }
20
+
21
+        return {
22
+            error: jibriStatus.getAttribute('failure_reason'),
23
+            recordingMode: jibriStatus.getAttribute('recording_mode'),
24
+            sessionID: jibriStatus.getAttribute('session_id'),
25
+            status: jibriStatus.getAttribute('status')
26
+        };
27
+    },
28
+
29
+    /**
30
+     * Parses the presence update from a hidden domain participant and returns
31
+     * an object with the statuses related to recording.
32
+     *
33
+     * @param {Node} presence - An XMPP presence update.
34
+     * @returns {Object} The current presence values related to recording.
35
+     */
36
+    getHiddenDomainUpdate(presence) {
37
+        const liveStreamViewURLContainer
38
+            = presence.getElementsByTagName('live-stream-view-url')[0];
39
+        const liveStreamViewURL = liveStreamViewURLContainer
40
+            && liveStreamViewURLContainer.textContent;
41
+        const modeContainer
42
+            = presence.getElementsByTagName('mode')[0];
43
+        const mode = modeContainer
44
+            && modeContainer.textContent
45
+            && modeContainer.textContent.toLowerCase();
46
+        const sessionIDContainer
47
+            = presence.getElementsByTagName('session_id')[0];
48
+        const sessionID
49
+            = sessionIDContainer && sessionIDContainer.textContent;
50
+
51
+        return {
52
+            liveStreamViewURL,
53
+            mode,
54
+            sessionID
55
+        };
56
+    },
57
+
58
+    /**
59
+     * Returns the recording session ID from a successful IQ.
60
+     *
61
+     * @param {Node} response - The response from the IQ.
62
+     * @returns {string} The session ID of the recording session.
63
+     */
64
+    getSessionIdFromIq(response) {
65
+        const jibri = response && response.getElementsByTagName('jibri')[0];
66
+
67
+        return jibri && jibri.getAttribute('session_id');
68
+    },
69
+
70
+    /**
71
+     * Returns the recording session ID from a presence, if it exists.
72
+     *
73
+     * @param {Node} presence - An XMPP presence update.
74
+     * @returns {string|undefined} The session ID of the recording session.
75
+     */
76
+    getSessionId(presence) {
77
+        const sessionIdContainer
78
+            = presence.getElementsByTagName('session_id')[0];
79
+        const sessionId = sessionIdContainer && sessionIdContainer.textContent;
80
+
81
+        return sessionId;
82
+    },
83
+
84
+    /**
85
+     * Returns whether or not a presence is from the focus.
86
+     *
87
+     * @param {Node} presence - An XMPP presence update.
88
+     * @returns {boolean} True if the presence is from the focus.
89
+     */
90
+    isFromFocus(presence) {
91
+        return presence.getAttribute('from').includes('focus');
92
+    }
93
+};

+ 5
- 1
modules/videosipgw/JitsiVideoSIPGWSession.js Целия файл

@@ -81,8 +81,11 @@ export default class JitsiVideoSIPGWSession extends Listenable {
81 81
      * Changes the state of this session.
82 82
      *
83 83
      * @param {string} newState - The new {VideoSIPGWConstants} state to set.
84
+     * @param {string} [optional] failureReason - The reason why a failure state
85
+     * was entered.
86
+     * @returns {void}
84 87
      */
85
-    setState(newState) {
88
+    setState(newState, failureReason) {
86 89
         if (newState === this.state) {
87 90
             return;
88 91
         }
@@ -93,6 +96,7 @@ export default class JitsiVideoSIPGWSession extends Listenable {
93 96
         this.eventEmitter.emit(STATE_CHANGED,
94 97
             {
95 98
                 address: this.sipAddress,
99
+                failureReason,
96 100
                 oldState,
97 101
                 newState: this.state,
98 102
                 displayName: this.displayName

+ 1
- 30
modules/videosipgw/VideoSIPGW.js Целия файл

@@ -26,39 +26,10 @@ export default class VideoSIPGW {
26 26
         // VideoSIPGW, JitsiConference and ChatRoom are not reusable and no
27 27
         // more than one VideoSIPGW can be created per JitsiConference,
28 28
         // so we don't bother to cleanup
29
-        chatRoom.addPresenceListener('jibri-sip-status',
30
-            this.handleJibriSIPStatus.bind(this));
31 29
         chatRoom.addPresenceListener('jibri-sip-call-state',
32 30
             this.handleJibriSIPState.bind(this));
33 31
     }
34 32
 
35
-    /**
36
-     * Handles presence nodes with name: jibri-sip-status.
37
-     *
38
-     * @param {Object} node the presence node Object to handle.
39
-     * Object representing part of the presence received over xmpp.
40
-     */
41
-    handleJibriSIPStatus(node) {
42
-        const attributes = node.attributes;
43
-
44
-        if (!attributes) {
45
-            return;
46
-        }
47
-
48
-        logger.debug('Handle video sip gw status : ', attributes);
49
-        const newStatus = attributes.status;
50
-
51
-        // check for global availability of the service
52
-        if (newStatus !== this.status
53
-            && (newStatus === Constants.STATUS_UNDEFINED
54
-                || newStatus === Constants.STATUS_AVAILABLE
55
-                || newStatus === Constants.STATUS_BUSY)) {
56
-            this.status = newStatus;
57
-            this.eventEmitter.emit(
58
-                XMPPEvents.VIDEO_SIP_GW_AVAILABILITY_CHANGED, this.status);
59
-        }
60
-    }
61
-
62 33
     /**
63 34
      * Handles presence nodes with name: jibri-sip-call-state.
64 35
      *
@@ -96,7 +67,7 @@ export default class VideoSIPGW {
96 67
             const session = this.sessions[address];
97 68
 
98 69
             if (session) {
99
-                session.setState(newState);
70
+                session.setState(newState, attributes.failure_reason);
100 71
             } else {
101 72
                 logger.warn('Video SIP GW session not found:', address);
102 73
             }

+ 26
- 87
modules/xmpp/ChatRoom.js Целия файл

@@ -6,12 +6,12 @@ import { $iq, $msg, $pres, Strophe } from 'strophe.js';
6 6
 import GlobalOnErrorHandler from '../util/GlobalOnErrorHandler';
7 7
 import * as JitsiTranscriptionStatus from '../../JitsiTranscriptionStatus';
8 8
 import Listenable from '../util/Listenable';
9
+import recordingManager from '../recording/recordingManager';
9 10
 import Settings from '../settings/Settings';
10 11
 import * as MediaType from '../../service/RTC/MediaType';
11 12
 import XMPPEvents from '../../service/xmpp/XMPPEvents';
12 13
 
13 14
 import Moderator from './moderator';
14
-import Recorder from './recording';
15 15
 
16 16
 const logger = getLogger(__filename);
17 17
 
@@ -172,6 +172,11 @@ export default class ChatRoom extends Listenable {
172 172
 
173 173
         this.locked = false;
174 174
         this.transcriptionStatus = JitsiTranscriptionStatus.OFF;
175
+
176
+        recordingManager.init(
177
+            this.eventEmitter,
178
+            this.connection,
179
+            this.focusMucJid);
175 180
     }
176 181
 
177 182
     /* eslint-enable max-params */
@@ -419,17 +424,7 @@ export default class ChatRoom extends Listenable {
419 424
                 && this.options.hiddenDomain
420 425
                     === jid.substring(jid.indexOf('@') + 1, jid.indexOf('/'));
421 426
 
422
-        // Check isHiddenDomain as a way to verify a live stream URL is from a
423
-        // trusted source. This prevents users from trying to display arbitrary
424
-        // live stream URLs.
425
-        if (member.isHiddenDomain) {
426
-            const liveStreamViewURLItem
427
-                = pres.getElementsByTagName('live-stream-view-url')[0];
428
-
429
-            if (liveStreamViewURLItem) {
430
-                member.liveStreamViewURL = liveStreamViewURLItem.textContent;
431
-            }
432
-        }
427
+        recordingManager.onPresence(pres, member.isHiddenDomain);
433 428
 
434 429
         const xEl = pres.querySelector('x');
435 430
 
@@ -441,7 +436,6 @@ export default class ChatRoom extends Listenable {
441 436
 
442 437
         parser.packet2JSON(pres, nodes);
443 438
         this.lastPresences[from] = nodes;
444
-        let jibri = null;
445 439
 
446 440
         // process nodes to extract data needed for MUC_JOINED and
447 441
         // MUC_MEMBER_JOINED events
@@ -530,13 +524,6 @@ export default class ChatRoom extends Listenable {
530 524
                     member.status,
531 525
                     member.identity);
532 526
 
533
-                if (member.liveStreamViewURL) {
534
-                    this.eventEmitter.emit(
535
-                        XMPPEvents.LIVE_STREAM_URL_CHANGE,
536
-                        from,
537
-                        member.liveStreamViewURL);
538
-                }
539
-
540 527
                 // we are reporting the status with the join
541 528
                 // so we do not want a second event about status update
542 529
                 hasStatusUpdate = false;
@@ -576,15 +563,6 @@ export default class ChatRoom extends Listenable {
576 563
                 hasStatusUpdate = true;
577 564
                 memberOfThis.status = member.status;
578 565
             }
579
-
580
-            if (memberOfThis.liveStreamViewURL !== member.liveStreamViewURL) {
581
-                memberOfThis.liveStreamViewURL = member.liveStreamViewURL;
582
-                this.eventEmitter.emit(
583
-                    XMPPEvents.LIVE_STREAM_URL_CHANGE,
584
-                    from,
585
-                    member.liveStreamViewURL);
586
-            }
587
-
588 566
         }
589 567
 
590 568
         // after we had fired member or room joined events, lets fire events
@@ -614,9 +592,6 @@ export default class ChatRoom extends Listenable {
614 592
                     this.eventEmitter.emit(XMPPEvents.BRIDGE_DOWN);
615 593
                 }
616 594
                 break;
617
-            case 'jibri-recording-status':
618
-                jibri = node;
619
-                break;
620 595
             case 'transcription-status': {
621 596
                 const { attributes } = node;
622 597
 
@@ -660,13 +635,6 @@ export default class ChatRoom extends Listenable {
660 635
                 from,
661 636
                 member.status);
662 637
         }
663
-
664
-        if (jibri) {
665
-            this.lastJibri = jibri;
666
-            if (this.recording) {
667
-                this.recording.handleJibriPresence(jibri);
668
-            }
669
-        }
670 638
     }
671 639
 
672 640
     /**
@@ -676,14 +644,9 @@ export default class ChatRoom extends Listenable {
676 644
      */
677 645
     _initFocus(from, mucJid) {
678 646
         this.focusMucJid = from;
679
-        if (!this.recording) {
680
-            this.recording = new Recorder(this.options.recordingType,
681
-                this.eventEmitter, this.connection, this.focusMucJid,
682
-                this.options.jirecon, this.roomjid);
683
-            if (this.lastJibri) {
684
-                this.recording.handleJibriPresence(this.lastJibri);
685
-            }
686
-        }
647
+
648
+        recordingManager.setFocusMucJid(this.focusMucJid);
649
+
687 650
         logger.info(`Ignore focus: ${from}, real JID: ${mucJid}`);
688 651
     }
689 652
 
@@ -840,13 +803,6 @@ export default class ChatRoom extends Listenable {
840 803
         const membersKeys = Object.keys(this.members);
841 804
 
842 805
         if (!isSelfPresence) {
843
-            if (this.members[from].liveStreamViewURL) {
844
-                this.eventEmitter.emit(
845
-                    XMPPEvents.LIVE_STREAM_URL_CHANGE,
846
-                    from,
847
-                    undefined);
848
-            }
849
-
850 806
             delete this.members[from];
851 807
             this.onParticipantLeft(from, false);
852 808
         } else if (membersKeys.length > 0) {
@@ -1300,44 +1256,27 @@ export default class ChatRoom extends Listenable {
1300 1256
     }
1301 1257
 
1302 1258
     /**
1303
-     * Returns true if the recording is supproted and false if not.
1304
-     */
1305
-    isRecordingSupported() {
1306
-        if (this.recording) {
1307
-            return this.recording.isSupported();
1308
-        }
1309
-
1310
-        return false;
1311
-    }
1312
-
1313
-    /**
1314
-     * Returns null if the recording is not supported, "on" if the recording
1315
-     * started and "off" if the recording is not started.
1316
-     */
1317
-    getRecordingState() {
1318
-        return this.recording ? this.recording.getState() : undefined;
1319
-    }
1320
-
1321
-    /**
1322
-     * Returns the url of the recorded video.
1259
+     * Starts a recording session.
1260
+     *
1261
+     * @param {Object} options - Configuration for the recording. See
1262
+     * {@link recordingManager#startRecording} for more info.
1263
+     * @returns {Promise} See {@link recordingManager#startRecording} for more
1264
+     * info.
1323 1265
      */
1324
-    getRecordingURL() {
1325
-        return this.recording ? this.recording.getURL() : null;
1266
+    startRecording(options) {
1267
+        return recordingManager.startRecording(options);
1326 1268
     }
1327 1269
 
1328 1270
     /**
1329
-     * Starts/stops the recording
1330
-     * @param token token for authentication
1331
-     * @param statusChangeHandler {function} receives the new status as
1332
-     * argument.
1271
+     * Stops a recording session.
1272
+     *
1273
+     * @param {string} sessionID - The ID of the recording session that should
1274
+     * be stopped.
1275
+     * @returns {Promise} See {@link recordingManager#stopRecording} for more
1276
+     * info.
1333 1277
      */
1334
-    toggleRecording(options, statusChangeHandler) {
1335
-        if (this.recording) {
1336
-            return this.recording.toggleRecording(options, statusChangeHandler);
1337
-        }
1338
-
1339
-        return statusChangeHandler('error',
1340
-            new Error('The conference is not created yet!'));
1278
+    stopRecording(sessionID) {
1279
+        return recordingManager.stopRecording(sessionID);
1341 1280
     }
1342 1281
 
1343 1282
     /**

+ 0
- 433
modules/xmpp/recording.js Целия файл

@@ -1,433 +0,0 @@
1
-/* global $ */
2
-
3
-import { getLogger } from 'jitsi-meet-logger';
4
-import { $iq } from 'strophe.js';
5
-
6
-const XMPPEvents = require('../../service/xmpp/XMPPEvents');
7
-const JitsiRecorderErrors = require('../../JitsiRecorderErrors');
8
-const GlobalOnErrorHandler = require('../util/GlobalOnErrorHandler');
9
-
10
-const logger = getLogger(__filename);
11
-
12
-/**
13
- * Extracts the error details from given error element/node.
14
- *
15
- * @param {Element|Object} errorIqNode - Either DOM element or the structure
16
- * from ChatRoom packet2JSON.
17
- * @return {{
18
- *     code: string,
19
- *     type: string,
20
- *     message: string
21
- * }}
22
- */
23
-function getJibriErrorDetails(errorIqNode) {
24
-    if (typeof errorIqNode.querySelector === 'function') {
25
-        const error = errorIqNode.querySelector('error');
26
-        const errorText = error && error.querySelector('text');
27
-
28
-        return error && {
29
-            code: error.attributes.code && error.attributes.code.value,
30
-            type: error.attributes.type && error.attributes.type.value,
31
-            message: errorText && errorText.textContent
32
-        };
33
-    }
34
-
35
-    let error = null;
36
-
37
-    for (const child of errorIqNode.children) {
38
-        if (child.tagName === 'error') {
39
-            error = child;
40
-            break;
41
-        }
42
-    }
43
-
44
-    if (!error) {
45
-        return null;
46
-    }
47
-
48
-    let errorText = null;
49
-
50
-    for (const errorChild of error.children) {
51
-        if (errorChild.tagName === 'text') {
52
-            errorText = errorChild.value;
53
-            break;
54
-        }
55
-    }
56
-
57
-    return {
58
-        code: error.attributes.code,
59
-        type: error.attributes.type,
60
-        message: errorText
61
-    };
62
-}
63
-
64
-/* eslint-disable max-params */
65
-
66
-/**
67
- *
68
- * @param type
69
- * @param eventEmitter
70
- * @param connection
71
- * @param focusMucJid
72
- * @param jirecon
73
- * @param roomjid
74
- */
75
-export default function Recording(
76
-        type,
77
-        eventEmitter,
78
-        connection,
79
-        focusMucJid,
80
-        jirecon,
81
-        roomjid) {
82
-    this.eventEmitter = eventEmitter;
83
-    this.connection = connection;
84
-    this.state = null;
85
-    this.focusMucJid = focusMucJid;
86
-    this.jirecon = jirecon;
87
-    this.url = null;
88
-    this.type = type;
89
-    this._isSupported
90
-        = !(
91
-            (type === Recording.types.JIRECON && !this.jirecon)
92
-                || (type !== Recording.types.JIBRI
93
-                    && type !== Recording.types.JIBRI_FILE
94
-                    && type !== Recording.types.COLIBRI));
95
-
96
-    /**
97
-     * The ID of the jirecon recording session. Jirecon generates it when we
98
-     * initially start recording, and it needs to be used in subsequent requests
99
-     * to jirecon.
100
-     */
101
-    this.jireconRid = null;
102
-    this.roomjid = roomjid;
103
-}
104
-
105
-/* eslint-enable max-params */
106
-
107
-Recording.types = {
108
-    COLIBRI: 'colibri',
109
-    JIRECON: 'jirecon',
110
-    JIBRI: 'jibri',
111
-    JIBRI_FILE: 'jibri_file'
112
-};
113
-
114
-Recording.status = {
115
-    ON: 'on',
116
-    OFF: 'off',
117
-    AVAILABLE: 'available',
118
-    UNAVAILABLE: 'unavailable',
119
-    PENDING: 'pending',
120
-    RETRYING: 'retrying',
121
-    ERROR: 'error',
122
-    BUSY: 'busy',
123
-    FAILED: 'failed'
124
-};
125
-
126
-Recording.action = {
127
-    START: 'start',
128
-    STOP: 'stop'
129
-};
130
-
131
-Recording.prototype.handleJibriPresence = function(jibri) {
132
-    const attributes = jibri.attributes;
133
-
134
-    if (!attributes) {
135
-        return;
136
-    }
137
-
138
-    const newState = attributes.status;
139
-    const errorDetails = getJibriErrorDetails(jibri);
140
-
141
-    logger.log(`Handle Jibri presence : ${newState}`, errorDetails);
142
-
143
-    if (newState === this.state) {
144
-        return;
145
-    }
146
-
147
-    if (newState === 'undefined') {
148
-        this.state = Recording.status.UNAVAILABLE;
149
-    } else if (newState === Recording.status.OFF) {
150
-        if (!this.state
151
-            || this.state === 'undefined'
152
-            || this.state === Recording.status.UNAVAILABLE) {
153
-            this.state = Recording.status.AVAILABLE;
154
-        } else {
155
-            this.state = Recording.status.OFF;
156
-        }
157
-    } else {
158
-        this.state = newState;
159
-    }
160
-
161
-    this.eventEmitter.emit(XMPPEvents.RECORDER_STATE_CHANGED, this.state);
162
-};
163
-
164
-/* eslint-disable max-params */
165
-
166
-Recording.prototype.setRecordingJibri = function(
167
-        state,
168
-        callback,
169
-        errCallback,
170
-        options = {}) {
171
-    if (state === this.state) {
172
-        errCallback(JitsiRecorderErrors.INVALID_STATE);
173
-    }
174
-
175
-    // FIXME jibri does not accept IQ without 'url' attribute set ?
176
-    const iq
177
-        = $iq({
178
-            to: this.focusMucJid,
179
-            type: 'set'
180
-        })
181
-            .c('jibri', {
182
-                'xmlns': 'http://jitsi.org/protocol/jibri',
183
-                'action':
184
-                    state === Recording.status.ON
185
-                        ? Recording.action.START
186
-                        : Recording.action.STOP,
187
-                'recording_mode':
188
-                    this.type === Recording.types.JIBRI_FILE
189
-                        ? 'file'
190
-                        : 'stream',
191
-                'streamid':
192
-                    this.type === Recording.types.JIBRI
193
-                        ? options.streamId
194
-                        : undefined,
195
-                'you_tube_broadcast_id': options.broadcastId
196
-            })
197
-            .up();
198
-
199
-    logger.log(`Set jibri recording: ${state}`, iq.nodeTree);
200
-    logger.log(iq.nodeTree);
201
-    this.connection.sendIQ(
202
-        iq,
203
-        result => {
204
-            logger.log('Result', result);
205
-
206
-            const jibri = $(result).find('jibri');
207
-
208
-            callback(jibri.attr('state'), jibri.attr('url'));
209
-        },
210
-        error => {
211
-            logger.log(
212
-                'Failed to start recording, error: ',
213
-                getJibriErrorDetails(error));
214
-            errCallback(error);
215
-        });
216
-};
217
-
218
-/* eslint-enable max-params */
219
-
220
-Recording.prototype.setRecordingJirecon
221
-    = function(state, callback, errCallback) {
222
-        if (state === this.state) {
223
-            errCallback(new Error('Invalid state!'));
224
-        }
225
-
226
-        const iq
227
-            = $iq({
228
-                to: this.jirecon,
229
-                type: 'set'
230
-            })
231
-                .c('recording', { xmlns: 'http://jitsi.org/protocol/jirecon',
232
-                    action: state === Recording.status.ON
233
-                        ? Recording.action.START
234
-                        : Recording.action.STOP,
235
-                    mucjid: this.roomjid });
236
-
237
-        if (state === Recording.status.OFF) {
238
-            iq.attrs({ rid: this.jireconRid });
239
-        }
240
-
241
-        logger.log('Start recording');
242
-        const self = this;
243
-
244
-        this.connection.sendIQ(
245
-            iq,
246
-            result => {
247
-                // TODO wait for an IQ with the real status, since this is
248
-                // provisional?
249
-                // eslint-disable-next-line newline-per-chained-call
250
-                self.jireconRid = $(result).find('recording').attr('rid');
251
-
252
-                const stateStr
253
-                    = state === Recording.status.ON ? 'started' : 'stopped';
254
-
255
-                logger.log(`Recording ${stateStr}(jirecon)${result}`);
256
-
257
-                self.state = state;
258
-                if (state === Recording.status.OFF) {
259
-                    self.jireconRid = null;
260
-                }
261
-
262
-                callback(state);
263
-            },
264
-            error => {
265
-                logger.log('Failed to start recording, error: ', error);
266
-                errCallback(error);
267
-            });
268
-    };
269
-
270
-/* eslint-disable max-params */
271
-
272
-// Sends a COLIBRI message which enables or disables (according to 'state')
273
-// the recording on the bridge. Waits for the result IQ and calls 'callback'
274
-// with the new recording state, according to the IQ.
275
-Recording.prototype.setRecordingColibri = function(
276
-        state,
277
-        callback,
278
-        errCallback,
279
-        options) {
280
-    const elem = $iq({
281
-        to: this.focusMucJid,
282
-        type: 'set'
283
-    });
284
-
285
-    elem.c('conference', {
286
-        xmlns: 'http://jitsi.org/protocol/colibri'
287
-    });
288
-    elem.c('recording', {
289
-        state,
290
-        token: options.token
291
-    });
292
-
293
-    const self = this;
294
-
295
-    this.connection.sendIQ(
296
-        elem,
297
-        result => {
298
-            logger.log('Set recording "', state, '". Result:', result);
299
-            const recordingElem = $(result).find('>conference>recording');
300
-            const newState = recordingElem.attr('state');
301
-
302
-            self.state = newState;
303
-            callback(newState);
304
-
305
-            if (newState === 'pending') {
306
-                self.connection.addHandler(iq => {
307
-                    // eslint-disable-next-line newline-per-chained-call
308
-                    const s = $(iq).find('recording').attr('state');
309
-
310
-                    if (s) {
311
-                        self.state = newState;
312
-                        callback(s);
313
-                    }
314
-                }, 'http://jitsi.org/protocol/colibri', 'iq', null, null, null);
315
-            }
316
-        },
317
-        error => {
318
-            logger.warn(error);
319
-            errCallback(error);
320
-        }
321
-    );
322
-};
323
-
324
-/* eslint-enable max-params */
325
-
326
-Recording.prototype.setRecording = function(...args) {
327
-    switch (this.type) {
328
-    case Recording.types.JIRECON:
329
-        this.setRecordingJirecon(...args);
330
-        break;
331
-    case Recording.types.COLIBRI:
332
-        this.setRecordingColibri(...args);
333
-        break;
334
-    case Recording.types.JIBRI:
335
-    case Recording.types.JIBRI_FILE:
336
-        this.setRecordingJibri(...args);
337
-        break;
338
-    default: {
339
-        const errmsg = 'Unknown recording type!';
340
-
341
-        GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
342
-        logger.error(errmsg);
343
-        break;
344
-    }
345
-    }
346
-};
347
-
348
-/**
349
- * Starts/stops the recording.
350
- * @param {Object} options
351
- * @param {string} options.token token for authentication
352
- * @param {string} options.streamId the stream ID to be used with Jibri in
353
- * the streaming mode.
354
- * @param statusChangeHandler {function} receives the new status as argument.
355
- */
356
-Recording.prototype.toggleRecording = function(
357
-        options = { },
358
-        statusChangeHandler) {
359
-    const oldState = this.state;
360
-
361
-    // If the recorder is currently unavailable we throw an error.
362
-    if (oldState === Recording.status.UNAVAILABLE
363
-        || oldState === Recording.status.FAILED) {
364
-        statusChangeHandler(
365
-            Recording.status.FAILED,
366
-            JitsiRecorderErrors.RECORDER_UNAVAILABLE);
367
-    } else if (oldState === Recording.status.BUSY) {
368
-        statusChangeHandler(
369
-            Recording.status.BUSY,
370
-            JitsiRecorderErrors.RECORDER_BUSY);
371
-    }
372
-
373
-    // If we're about to turn ON the recording we need either a streamId or
374
-    // an authentication token depending on the recording type. If we don't
375
-    // have any of those we throw an error.
376
-    if ((oldState === Recording.status.OFF
377
-                || oldState === Recording.status.AVAILABLE)
378
-            && ((!options.token && this.type === Recording.types.COLIBRI)
379
-                || (!options.streamId
380
-                    && this.type === Recording.types.JIBRI))) {
381
-        statusChangeHandler(
382
-            Recording.status.FAILED,
383
-            JitsiRecorderErrors.NO_TOKEN);
384
-        logger.error('No token passed!');
385
-
386
-        return;
387
-    }
388
-
389
-    const newState
390
-        = oldState === Recording.status.AVAILABLE
391
-                || oldState === Recording.status.OFF
392
-            ? Recording.status.ON
393
-            : Recording.status.OFF;
394
-
395
-    const self = this;
396
-
397
-    logger.log('Toggle recording (old state, new state): ', oldState, newState);
398
-    this.setRecording(
399
-        newState,
400
-        (state, url) => {
401
-            // If the state is undefined we're going to wait for presence
402
-            // update.
403
-            if (state && state !== oldState) {
404
-                self.state = state;
405
-                self.url = url;
406
-                statusChangeHandler(state);
407
-            }
408
-        },
409
-        error => statusChangeHandler(Recording.status.FAILED, error),
410
-        options);
411
-};
412
-
413
-/**
414
- * Returns true if the recording is supproted and false if not.
415
- */
416
-Recording.prototype.isSupported = function() {
417
-    return this._isSupported;
418
-};
419
-
420
-/**
421
- * Returns null if the recording is not supported, "on" if the recording started
422
- * and "off" if the recording is not started.
423
- */
424
-Recording.prototype.getState = function() {
425
-    return this.state;
426
-};
427
-
428
-/**
429
- * Returns the url of the recorded video.
430
- */
431
-Recording.prototype.getURL = function() {
432
-    return this.url;
433
-};

+ 0
- 4
service/xmpp/XMPPEvents.js Целия файл

@@ -90,10 +90,6 @@ const XMPPEvents = {
90 90
     // Designates an event indicating that we were kicked from the XMPP MUC.
91 91
     KICKED: 'xmpp.kicked',
92 92
 
93
-    // Designates an event indicating that a participant's live stream URL has
94
-    // been updated.
95
-    LIVE_STREAM_URL_CHANGE: 'xmpp.live_stream_url_changed',
96
-
97 93
     // Designates an event indicating that our role in the XMPP MUC has changed.
98 94
     LOCAL_ROLE_CHANGED: 'xmpp.localrole_changed',
99 95
 

Loading…
Отказ
Запис