SessionManager
This commit is contained in:
parent
b6e1a49d33
commit
61652c69b3
|
@ -1,11 +1,13 @@
|
||||||
/* @flow */
|
/* @flow */
|
||||||
|
|
||||||
import { i18next } from '../../base/i18n';
|
import { i18next } from '../../base/i18n';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FlacAdapter,
|
FlacAdapter,
|
||||||
OggAdapter,
|
OggAdapter,
|
||||||
WavAdapter
|
WavAdapter
|
||||||
} from '../recording';
|
} from '../recording';
|
||||||
|
import { sessionManager } from '../session';
|
||||||
|
|
||||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
|
||||||
|
@ -556,6 +558,7 @@ class RecordingController {
|
||||||
delegate.start(this._micDeviceId)
|
delegate.start(this._micDeviceId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this._changeState(ControllerState.RECORDING);
|
this._changeState(ControllerState.RECORDING);
|
||||||
|
sessionManager.beginSegment(this._currentSessionToken);
|
||||||
logger.log('Local recording engaged.');
|
logger.log('Local recording engaged.');
|
||||||
|
|
||||||
if (this._onNotify) {
|
if (this._onNotify) {
|
||||||
|
@ -591,6 +594,7 @@ class RecordingController {
|
||||||
.stop()
|
.stop()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this._changeState(ControllerState.IDLE);
|
this._changeState(ControllerState.IDLE);
|
||||||
|
sessionManager.endSegment(this._currentSessionToken);
|
||||||
logger.log('Local recording unengaged.');
|
logger.log('Local recording unengaged.');
|
||||||
this.downloadRecordedData(token);
|
this.downloadRecordedData(token);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,420 @@
|
||||||
|
/* @flow */
|
||||||
|
|
||||||
|
import jitsiLocalStorage from '../../../../modules/util/JitsiLocalStorage';
|
||||||
|
|
||||||
|
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets high precision system time.
|
||||||
|
*
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
function highPrecisionTime(): number {
|
||||||
|
return window.performance
|
||||||
|
&& window.performance.now
|
||||||
|
&& window.performance.timing
|
||||||
|
&& window.performance.timing.navigationStart
|
||||||
|
? window.performance.now() + window.performance.timing.navigationStart
|
||||||
|
: Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Have to use string literal here, instead of Symbols,
|
||||||
|
// because these values need to be JSON-serializible.
|
||||||
|
const SessionEventType = Object.freeze({
|
||||||
|
SESSION_STARTED: 'SESSION_STARTED',
|
||||||
|
SEGMENT_STARTED: 'SEGMENT_STARTED',
|
||||||
|
SEGMENT_ENDED: 'SEGMENT_ENDED'
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an event during a local recording session.
|
||||||
|
* The event can be either that the adapter started recording, or stopped
|
||||||
|
* recording.
|
||||||
|
*/
|
||||||
|
type SessionEvent = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the event.
|
||||||
|
* Should be one of the values in {@code SessionEventType}.
|
||||||
|
*/
|
||||||
|
type: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The timestamp of the event.
|
||||||
|
*/
|
||||||
|
timestamp: number
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of the metadata of a segment.
|
||||||
|
*/
|
||||||
|
type SegmentInfo = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The length of gap before this segment, in milliseconds.
|
||||||
|
* mull if unknown.
|
||||||
|
*/
|
||||||
|
gapBefore?: ?number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The duration of this segment, in milliseconds.
|
||||||
|
* null if unknown or the segment is not finished.
|
||||||
|
*/
|
||||||
|
duration?: ?number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The start time, in milliseconds.
|
||||||
|
*/
|
||||||
|
start?: ?number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The end time, in milliseconds.
|
||||||
|
* null if unknown, the segment is not finished, or the recording is
|
||||||
|
* interrupted (e.g. browser reload).
|
||||||
|
*/
|
||||||
|
end?: ?number
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of metadata of a local recording session.
|
||||||
|
*/
|
||||||
|
type SessionInfo = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The session token.
|
||||||
|
*/
|
||||||
|
sessionToken: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The start time of the session.
|
||||||
|
*/
|
||||||
|
start: ?number,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The recording format.
|
||||||
|
*/
|
||||||
|
format: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of segments in the session.
|
||||||
|
*/
|
||||||
|
segments: SegmentInfo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@code localStorage} key.
|
||||||
|
*/
|
||||||
|
const LOCAL_STORAGE_KEY = 'localRecordingMetadataVersion1';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SessionManager manages the metadata of each segment during each local
|
||||||
|
* recording session.
|
||||||
|
*
|
||||||
|
* A segment is a continous portion of recording done using the same adapter
|
||||||
|
* on the same microphone device.
|
||||||
|
*
|
||||||
|
* Browser refreshes, switching of microphone will cause new segments to be
|
||||||
|
* created.
|
||||||
|
*
|
||||||
|
* A recording session can consist of one or more segments.
|
||||||
|
*/
|
||||||
|
class SessionManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata.
|
||||||
|
*/
|
||||||
|
_sessionsMetadata = {
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this._loadMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads metadata from localStorage.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_loadMetadata() {
|
||||||
|
const dataStr = jitsiLocalStorage.getItem(LOCAL_STORAGE_KEY);
|
||||||
|
|
||||||
|
if (dataStr !== null) {
|
||||||
|
try {
|
||||||
|
const dataObject = JSON.parse(dataStr);
|
||||||
|
|
||||||
|
this._sessionsMetadata = dataObject;
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('Failed to parse localStorage item.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists metadata to localStorage.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_saveMetadata() {
|
||||||
|
jitsiLocalStorage.setItem(LOCAL_STORAGE_KEY,
|
||||||
|
JSON.stringify(this._sessionsMetadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a session if not exists.
|
||||||
|
*
|
||||||
|
* @param {string} sessionToken - .
|
||||||
|
* @param {string} format - .
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
createSession(sessionToken: string, format: string) {
|
||||||
|
if (this._sessionsMetadata[sessionToken] === undefined) {
|
||||||
|
this._sessionsMetadata[sessionToken] = {
|
||||||
|
format,
|
||||||
|
events: []
|
||||||
|
};
|
||||||
|
this._sessionsMetadata[sessionToken].events.push({
|
||||||
|
type: SessionEventType.SESSION_STARTED,
|
||||||
|
timestamp: highPrecisionTime()
|
||||||
|
});
|
||||||
|
this._saveMetadata();
|
||||||
|
} else {
|
||||||
|
logger.warn(`Session ${sessionToken} already exists`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all the Sessions.
|
||||||
|
*
|
||||||
|
* @returns {SessionInfo[]}
|
||||||
|
*/
|
||||||
|
getSessions(): SessionInfo[] {
|
||||||
|
const sessionTokens = Object.keys(this._sessionsMetadata);
|
||||||
|
const output = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < sessionTokens.length; ++i) {
|
||||||
|
const thisSession = this._sessionsMetadata[sessionTokens[i]];
|
||||||
|
const newSessionInfo : SessionInfo = {
|
||||||
|
start: thisSession.events[0].timestamp,
|
||||||
|
format: thisSession.format,
|
||||||
|
sessionToken: sessionTokens[i],
|
||||||
|
segments: this.getSegments(sessionTokens[i])
|
||||||
|
};
|
||||||
|
|
||||||
|
output.push(newSessionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.sort((a, b) => (a.start || 0) - (b.start || 0));
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes session metadata.
|
||||||
|
*
|
||||||
|
* @param {*} sessionToken - The session token.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
removeSession(sessionToken: string) {
|
||||||
|
delete this._sessionsMetadata[sessionToken];
|
||||||
|
this._saveMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get segments of a given Session.
|
||||||
|
*
|
||||||
|
* @param {string} sessionToken - The session token.
|
||||||
|
* @returns {SegmentInfo[]}
|
||||||
|
*/
|
||||||
|
getSegments(sessionToken: string): SegmentInfo[] {
|
||||||
|
const thisSession = this._sessionsMetadata[sessionToken];
|
||||||
|
|
||||||
|
if (thisSession) {
|
||||||
|
return this._constructSegments(thisSession.events);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the start of a new segment.
|
||||||
|
* This should be invoked by {@code RecordingAdapter}s when they need to
|
||||||
|
* start asynchronous operations (such as switching tracks) that interrupts
|
||||||
|
* recording.
|
||||||
|
*
|
||||||
|
* @param {string} sessionToken - The token of the session to start a new
|
||||||
|
* segment in.
|
||||||
|
* @returns {number} - Current segment index.
|
||||||
|
*/
|
||||||
|
beginSegment(sessionToken: string): number {
|
||||||
|
if (this._sessionsMetadata[sessionToken] === undefined) {
|
||||||
|
logger.warn('Attempting to add segments to nonexistent'
|
||||||
|
+ ` session ${sessionToken}`);
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
this._sessionsMetadata[sessionToken].events.push({
|
||||||
|
type: SessionEventType.SEGMENT_STARTED,
|
||||||
|
timestamp: highPrecisionTime()
|
||||||
|
});
|
||||||
|
this._saveMetadata();
|
||||||
|
|
||||||
|
return this.getSegments(sessionToken).length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current segment index. Starting from 0 for the first
|
||||||
|
* segment.
|
||||||
|
*
|
||||||
|
* @param {string} sessionToken - The session token.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
getCurrentSegmentIndex(sessionToken: string): number {
|
||||||
|
if (this._sessionsMetadata[sessionToken] === undefined) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const segments = this.getSegments(sessionToken);
|
||||||
|
|
||||||
|
if (segments.length === 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSegment = segments[segments.length - 1];
|
||||||
|
|
||||||
|
if (lastSegment.end) {
|
||||||
|
// last segment is already ended
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the end of the last segment in a session.
|
||||||
|
*
|
||||||
|
* @param {string} sessionToken - The session token.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
endSegment(sessionToken: string) {
|
||||||
|
if (this._sessionsMetadata[sessionToken] === undefined) {
|
||||||
|
logger.warn('Attempting to end a segment in nonexistent'
|
||||||
|
+ ` session ${sessionToken}`);
|
||||||
|
} else {
|
||||||
|
this._sessionsMetadata[sessionToken].events.push({
|
||||||
|
type: SessionEventType.SEGMENT_ENDED,
|
||||||
|
timestamp: highPrecisionTime()
|
||||||
|
});
|
||||||
|
this._saveMetadata();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an array of {@code SegmentInfo} from an array of
|
||||||
|
* {@code SessionEvent}s.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {*} events - The array of {@code SessionEvent}s.
|
||||||
|
* @returns {SegmentInfo[]}
|
||||||
|
*/
|
||||||
|
_constructSegments(events: SessionEvent[]): SegmentInfo[] {
|
||||||
|
if (events.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = [];
|
||||||
|
let sessionStartTime = null;
|
||||||
|
let currentSegment : SegmentInfo = {
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for adding a new {@code SegmentInfo} object to the
|
||||||
|
* output.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function commit() {
|
||||||
|
if (currentSegment.gapBefore === undefined
|
||||||
|
|| currentSegment.gapBefore === null) {
|
||||||
|
if (output.length > 0 && output[output.length - 1].end) {
|
||||||
|
const lastSegment = output[output.length - 1];
|
||||||
|
|
||||||
|
if (currentSegment.start && lastSegment.end) {
|
||||||
|
currentSegment.gapBefore = currentSegment.start
|
||||||
|
- lastSegment.end;
|
||||||
|
} else {
|
||||||
|
currentSegment.gapBefore = null;
|
||||||
|
}
|
||||||
|
} else if (sessionStartTime !== null && output.length === 0) {
|
||||||
|
currentSegment.gapBefore = currentSegment.start
|
||||||
|
? currentSegment.start - sessionStartTime
|
||||||
|
: null;
|
||||||
|
} else {
|
||||||
|
currentSegment.gapBefore = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentSegment.duration = currentSegment.end && currentSegment.start
|
||||||
|
? currentSegment.end - currentSegment.start
|
||||||
|
: null;
|
||||||
|
output.push(currentSegment);
|
||||||
|
currentSegment = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < events.length; ++i) {
|
||||||
|
const currentEvent = events[i];
|
||||||
|
|
||||||
|
switch (currentEvent.type) {
|
||||||
|
case SessionEventType.SESSION_STARTED:
|
||||||
|
if (sessionStartTime === null) {
|
||||||
|
sessionStartTime = currentEvent.timestamp;
|
||||||
|
} else {
|
||||||
|
logger.warn('Unexpected SESSION_STARTED event.'
|
||||||
|
, currentEvent);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SessionEventType.SEGMENT_STARTED:
|
||||||
|
if (currentSegment.start === undefined
|
||||||
|
|| currentSegment.start === null) {
|
||||||
|
currentSegment.start = currentEvent.timestamp;
|
||||||
|
} else {
|
||||||
|
commit();
|
||||||
|
currentSegment.start = currentEvent.timestamp;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SessionEventType.SEGMENT_ENDED:
|
||||||
|
if (currentSegment.start === undefined
|
||||||
|
|| currentSegment.start === null) {
|
||||||
|
logger.warn('Unexpected SEGMENT_ENDED event', currentEvent);
|
||||||
|
} else {
|
||||||
|
currentSegment.end = currentEvent.timestamp;
|
||||||
|
commit();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
logger.warn('Unexpected error during _constructSegments');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentSegment.start) {
|
||||||
|
commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global singleton of {@code SessionManager}.
|
||||||
|
*/
|
||||||
|
export const sessionManager = new SessionManager();
|
||||||
|
|
||||||
|
// For debug only. Remove later.
|
||||||
|
window.sessionManager = sessionManager;
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './SessionManager';
|
Loading…
Reference in New Issue