You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SessionManager.js 12KB

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