Parcourir la source

SessionManager

master
Radium Zheng il y a 7 ans
Parent
révision
61652c69b3

+ 4
- 0
react/features/local-recording/controller/RecordingController.js Voir le fichier

@@ -1,11 +1,13 @@
1 1
 /* @flow */
2 2
 
3 3
 import { i18next } from '../../base/i18n';
4
+
4 5
 import {
5 6
     FlacAdapter,
6 7
     OggAdapter,
7 8
     WavAdapter
8 9
 } from '../recording';
10
+import { sessionManager } from '../session';
9 11
 
10 12
 const logger = require('jitsi-meet-logger').getLogger(__filename);
11 13
 
@@ -556,6 +558,7 @@ class RecordingController {
556 558
             delegate.start(this._micDeviceId)
557 559
             .then(() => {
558 560
                 this._changeState(ControllerState.RECORDING);
561
+                sessionManager.beginSegment(this._currentSessionToken);
559 562
                 logger.log('Local recording engaged.');
560 563
 
561 564
                 if (this._onNotify) {
@@ -591,6 +594,7 @@ class RecordingController {
591 594
                 .stop()
592 595
                 .then(() => {
593 596
                     this._changeState(ControllerState.IDLE);
597
+                    sessionManager.endSegment(this._currentSessionToken);
594 598
                     logger.log('Local recording unengaged.');
595 599
                     this.downloadRecordedData(token);
596 600
 

+ 420
- 0
react/features/local-recording/session/SessionManager.js Voir le fichier

@@ -0,0 +1,420 @@
1
+/* @flow */
2
+
3
+import jitsiLocalStorage from '../../../../modules/util/JitsiLocalStorage';
4
+
5
+const logger = require('jitsi-meet-logger').getLogger(__filename);
6
+
7
+/**
8
+ * Gets high precision system time.
9
+ *
10
+ * @returns {number}
11
+ */
12
+function highPrecisionTime(): number {
13
+    return window.performance
14
+        && window.performance.now
15
+        && window.performance.timing
16
+        && window.performance.timing.navigationStart
17
+        ? window.performance.now() + window.performance.timing.navigationStart
18
+        : Date.now();
19
+}
20
+
21
+// Have to use string literal here, instead of Symbols,
22
+// because these values need to be JSON-serializible.
23
+const SessionEventType = Object.freeze({
24
+    SESSION_STARTED: 'SESSION_STARTED',
25
+    SEGMENT_STARTED: 'SEGMENT_STARTED',
26
+    SEGMENT_ENDED: 'SEGMENT_ENDED'
27
+});
28
+
29
+/**
30
+ * Represents an event during a local recording session.
31
+ * The event can be either that the adapter started recording, or stopped
32
+ * recording.
33
+ */
34
+type SessionEvent = {
35
+
36
+    /**
37
+     * The type of the event.
38
+     * Should be one of the values in {@code SessionEventType}.
39
+     */
40
+    type: string,
41
+
42
+    /**
43
+     * The timestamp of the event.
44
+     */
45
+    timestamp: number
46
+};
47
+
48
+/**
49
+ * Representation of the metadata of a segment.
50
+ */
51
+type SegmentInfo = {
52
+
53
+    /**
54
+     * The length of gap before this segment, in milliseconds.
55
+     * mull if unknown.
56
+     */
57
+    gapBefore?: ?number,
58
+
59
+    /**
60
+     * The duration of this segment, in milliseconds.
61
+     * null if unknown or the segment is not finished.
62
+     */
63
+    duration?: ?number,
64
+
65
+    /**
66
+     * The start time, in milliseconds.
67
+     */
68
+    start?: ?number,
69
+
70
+    /**
71
+     * The end time, in milliseconds.
72
+     * null if unknown, the segment is not finished, or the recording is
73
+     * interrupted (e.g. browser reload).
74
+     */
75
+    end?: ?number
76
+};
77
+
78
+/**
79
+ * Representation of metadata of a local recording session.
80
+ */
81
+type SessionInfo = {
82
+
83
+    /**
84
+     * The session token.
85
+     */
86
+    sessionToken: string,
87
+
88
+    /**
89
+     * The start time of the session.
90
+     */
91
+    start: ?number,
92
+
93
+    /**
94
+     * The recording format.
95
+     */
96
+    format: string,
97
+
98
+    /**
99
+     * Array of segments in the session.
100
+     */
101
+    segments: SegmentInfo[]
102
+}
103
+
104
+/**
105
+ * {@code localStorage} key.
106
+ */
107
+const LOCAL_STORAGE_KEY = 'localRecordingMetadataVersion1';
108
+
109
+/**
110
+ * SessionManager manages the metadata of each segment during each local
111
+ * recording session.
112
+ *
113
+ * A segment is a continous portion of recording done using the same adapter
114
+ * on the same microphone device.
115
+ *
116
+ * Browser refreshes, switching of microphone will cause new segments to be
117
+ * created.
118
+ *
119
+ * A recording session can consist of one or more segments.
120
+ */
121
+class SessionManager {
122
+
123
+    /**
124
+     * The metadata.
125
+     */
126
+    _sessionsMetadata = {
127
+    };
128
+
129
+    /**
130
+     * Constructor.
131
+     */
132
+    constructor() {
133
+        this._loadMetadata();
134
+    }
135
+
136
+    /**
137
+     * Loads metadata from localStorage.
138
+     *
139
+     * @private
140
+     * @returns {void}
141
+     */
142
+    _loadMetadata() {
143
+        const dataStr = jitsiLocalStorage.getItem(LOCAL_STORAGE_KEY);
144
+
145
+        if (dataStr !== null) {
146
+            try {
147
+                const dataObject = JSON.parse(dataStr);
148
+
149
+                this._sessionsMetadata = dataObject;
150
+            } catch (e) {
151
+                logger.warn('Failed to parse localStorage item.');
152
+
153
+                return;
154
+            }
155
+        }
156
+    }
157
+
158
+    /**
159
+     * Persists metadata to localStorage.
160
+     *
161
+     * @private
162
+     * @returns {void}
163
+     */
164
+    _saveMetadata() {
165
+        jitsiLocalStorage.setItem(LOCAL_STORAGE_KEY,
166
+            JSON.stringify(this._sessionsMetadata));
167
+    }
168
+
169
+    /**
170
+     * Creates a session if not exists.
171
+     *
172
+     * @param {string} sessionToken - .
173
+     * @param {string} format - .
174
+     * @returns {void}
175
+     */
176
+    createSession(sessionToken: string, format: string) {
177
+        if (this._sessionsMetadata[sessionToken] === undefined) {
178
+            this._sessionsMetadata[sessionToken] = {
179
+                format,
180
+                events: []
181
+            };
182
+            this._sessionsMetadata[sessionToken].events.push({
183
+                type: SessionEventType.SESSION_STARTED,
184
+                timestamp: highPrecisionTime()
185
+            });
186
+            this._saveMetadata();
187
+        } else {
188
+            logger.warn(`Session ${sessionToken} already exists`);
189
+        }
190
+    }
191
+
192
+    /**
193
+     * Gets all the Sessions.
194
+     *
195
+     * @returns {SessionInfo[]}
196
+     */
197
+    getSessions(): SessionInfo[] {
198
+        const sessionTokens = Object.keys(this._sessionsMetadata);
199
+        const output = [];
200
+
201
+        for (let i = 0; i < sessionTokens.length; ++i) {
202
+            const thisSession = this._sessionsMetadata[sessionTokens[i]];
203
+            const newSessionInfo : SessionInfo = {
204
+                start: thisSession.events[0].timestamp,
205
+                format: thisSession.format,
206
+                sessionToken: sessionTokens[i],
207
+                segments: this.getSegments(sessionTokens[i])
208
+            };
209
+
210
+            output.push(newSessionInfo);
211
+        }
212
+
213
+        output.sort((a, b) => (a.start || 0) - (b.start || 0));
214
+
215
+        return output;
216
+    }
217
+
218
+    /**
219
+     * Removes session metadata.
220
+     *
221
+     * @param {*} sessionToken - The session token.
222
+     * @returns {void}
223
+     */
224
+    removeSession(sessionToken: string) {
225
+        delete this._sessionsMetadata[sessionToken];
226
+        this._saveMetadata();
227
+    }
228
+
229
+    /**
230
+     * Get segments of a given Session.
231
+     *
232
+     * @param {string} sessionToken - The session token.
233
+     * @returns {SegmentInfo[]}
234
+     */
235
+    getSegments(sessionToken: string): SegmentInfo[] {
236
+        const thisSession = this._sessionsMetadata[sessionToken];
237
+
238
+        if (thisSession) {
239
+            return this._constructSegments(thisSession.events);
240
+        }
241
+
242
+        return [];
243
+    }
244
+
245
+    /**
246
+     * Marks the start of a new segment.
247
+     * This should be invoked by {@code RecordingAdapter}s when they need to
248
+     * start asynchronous operations (such as switching tracks) that interrupts
249
+     * recording.
250
+     *
251
+     * @param {string} sessionToken - The token of the session to start a new
252
+     * segment in.
253
+     * @returns {number} - Current segment index.
254
+     */
255
+    beginSegment(sessionToken: string): number {
256
+        if (this._sessionsMetadata[sessionToken] === undefined) {
257
+            logger.warn('Attempting to add segments to nonexistent'
258
+                + ` session ${sessionToken}`);
259
+
260
+            return -1;
261
+        }
262
+        this._sessionsMetadata[sessionToken].events.push({
263
+            type: SessionEventType.SEGMENT_STARTED,
264
+            timestamp: highPrecisionTime()
265
+        });
266
+        this._saveMetadata();
267
+
268
+        return this.getSegments(sessionToken).length - 1;
269
+    }
270
+
271
+    /**
272
+     * Gets the current segment index. Starting from 0 for the first
273
+     * segment.
274
+     *
275
+     * @param {string} sessionToken - The session token.
276
+     * @returns {number}
277
+     */
278
+    getCurrentSegmentIndex(sessionToken: string): number {
279
+        if (this._sessionsMetadata[sessionToken] === undefined) {
280
+            return -1;
281
+        }
282
+        const segments = this.getSegments(sessionToken);
283
+
284
+        if (segments.length === 0) {
285
+            return -1;
286
+        }
287
+
288
+        const lastSegment = segments[segments.length - 1];
289
+
290
+        if (lastSegment.end) {
291
+            // last segment is already ended
292
+            return -1;
293
+        }
294
+
295
+        return segments.length - 1;
296
+    }
297
+
298
+    /**
299
+     * Marks the end of the last segment in a session.
300
+     *
301
+     * @param {string} sessionToken - The session token.
302
+     * @returns {void}
303
+     */
304
+    endSegment(sessionToken: string) {
305
+        if (this._sessionsMetadata[sessionToken] === undefined) {
306
+            logger.warn('Attempting to end a segment in nonexistent'
307
+                + ` session ${sessionToken}`);
308
+        } else {
309
+            this._sessionsMetadata[sessionToken].events.push({
310
+                type: SessionEventType.SEGMENT_ENDED,
311
+                timestamp: highPrecisionTime()
312
+            });
313
+            this._saveMetadata();
314
+        }
315
+    }
316
+
317
+    /**
318
+     * Constructs an array of {@code SegmentInfo} from an array of
319
+     * {@code SessionEvent}s.
320
+     *
321
+     * @private
322
+     * @param {*} events - The array of {@code SessionEvent}s.
323
+     * @returns {SegmentInfo[]}
324
+     */
325
+    _constructSegments(events: SessionEvent[]): SegmentInfo[] {
326
+        if (events.length === 0) {
327
+            return [];
328
+        }
329
+
330
+        const output = [];
331
+        let sessionStartTime = null;
332
+        let currentSegment : SegmentInfo = {
333
+        };
334
+
335
+        /**
336
+         * Helper function for adding a new {@code SegmentInfo} object to the
337
+         * output.
338
+         *
339
+         * @returns {void}
340
+         */
341
+        function commit() {
342
+            if (currentSegment.gapBefore === undefined
343
+                || currentSegment.gapBefore === null) {
344
+                if (output.length > 0 && output[output.length - 1].end) {
345
+                    const lastSegment = output[output.length - 1];
346
+
347
+                    if (currentSegment.start && lastSegment.end) {
348
+                        currentSegment.gapBefore = currentSegment.start
349
+                            - lastSegment.end;
350
+                    } else {
351
+                        currentSegment.gapBefore = null;
352
+                    }
353
+                } else if (sessionStartTime !== null && output.length === 0) {
354
+                    currentSegment.gapBefore = currentSegment.start
355
+                        ? currentSegment.start - sessionStartTime
356
+                        : null;
357
+                } else {
358
+                    currentSegment.gapBefore = null;
359
+                }
360
+            }
361
+            currentSegment.duration = currentSegment.end && currentSegment.start
362
+                ? currentSegment.end - currentSegment.start
363
+                : null;
364
+            output.push(currentSegment);
365
+            currentSegment = {};
366
+        }
367
+
368
+        for (let i = 0; i < events.length; ++i) {
369
+            const currentEvent = events[i];
370
+
371
+            switch (currentEvent.type) {
372
+            case SessionEventType.SESSION_STARTED:
373
+                if (sessionStartTime === null) {
374
+                    sessionStartTime = currentEvent.timestamp;
375
+                } else {
376
+                    logger.warn('Unexpected SESSION_STARTED event.'
377
+                        , currentEvent);
378
+                }
379
+                break;
380
+            case SessionEventType.SEGMENT_STARTED:
381
+                if (currentSegment.start === undefined
382
+                    || currentSegment.start === null) {
383
+                    currentSegment.start = currentEvent.timestamp;
384
+                } else {
385
+                    commit();
386
+                    currentSegment.start = currentEvent.timestamp;
387
+                }
388
+                break;
389
+
390
+            case SessionEventType.SEGMENT_ENDED:
391
+                if (currentSegment.start === undefined
392
+                    || currentSegment.start === null) {
393
+                    logger.warn('Unexpected SEGMENT_ENDED event', currentEvent);
394
+                } else {
395
+                    currentSegment.end = currentEvent.timestamp;
396
+                    commit();
397
+                }
398
+                break;
399
+
400
+            default:
401
+                logger.warn('Unexpected error during _constructSegments');
402
+                break;
403
+            }
404
+        }
405
+        if (currentSegment.start) {
406
+            commit();
407
+        }
408
+
409
+        return output;
410
+    }
411
+
412
+}
413
+
414
+/**
415
+ * Global singleton of {@code SessionManager}.
416
+ */
417
+export const sessionManager = new SessionManager();
418
+
419
+// For debug only. Remove later.
420
+window.sessionManager = sessionManager;

+ 1
- 0
react/features/local-recording/session/index.js Voir le fichier

@@ -0,0 +1 @@
1
+export * from './SessionManager';

Chargement…
Annuler
Enregistrer