選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

SessionManager.js 12KB

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