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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. /* global $, $iq */
  2. import { getLogger } from 'jitsi-meet-logger';
  3. const logger = getLogger(__filename);
  4. const XMPPEvents = require('../../service/xmpp/XMPPEvents');
  5. const JitsiRecorderErrors = require('../../JitsiRecorderErrors');
  6. const GlobalOnErrorHandler = require('../util/GlobalOnErrorHandler');
  7. /* eslint-disable max-params */
  8. /**
  9. *
  10. * @param type
  11. * @param eventEmitter
  12. * @param connection
  13. * @param focusMucJid
  14. * @param jirecon
  15. * @param roomjid
  16. */
  17. export default function Recording(
  18. type,
  19. eventEmitter,
  20. connection,
  21. focusMucJid,
  22. jirecon,
  23. roomjid) {
  24. this.eventEmitter = eventEmitter;
  25. this.connection = connection;
  26. this.state = null;
  27. this.focusMucJid = focusMucJid;
  28. this.jirecon = jirecon;
  29. this.url = null;
  30. this.type = type;
  31. this._isSupported
  32. = !(
  33. (type === Recording.types.JIRECON && !this.jirecon)
  34. || (type !== Recording.types.JIBRI
  35. && type !== Recording.types.COLIBRI));
  36. /**
  37. * The ID of the jirecon recording session. Jirecon generates it when we
  38. * initially start recording, and it needs to be used in subsequent requests
  39. * to jirecon.
  40. */
  41. this.jireconRid = null;
  42. this.roomjid = roomjid;
  43. }
  44. /* eslint-enable max-params */
  45. Recording.types = {
  46. COLIBRI: 'colibri',
  47. JIRECON: 'jirecon',
  48. JIBRI: 'jibri'
  49. };
  50. Recording.status = {
  51. ON: 'on',
  52. OFF: 'off',
  53. AVAILABLE: 'available',
  54. UNAVAILABLE: 'unavailable',
  55. PENDING: 'pending',
  56. RETRYING: 'retrying',
  57. ERROR: 'error',
  58. BUSY: 'busy',
  59. FAILED: 'failed'
  60. };
  61. Recording.action = {
  62. START: 'start',
  63. STOP: 'stop'
  64. };
  65. Recording.prototype.handleJibriPresence = function(jibri) {
  66. const attributes = jibri.attributes;
  67. if (!attributes) {
  68. return;
  69. }
  70. const newState = attributes.status;
  71. logger.log('Handle jibri presence : ', newState);
  72. if (newState === this.state) {
  73. return;
  74. }
  75. if (newState === 'undefined') {
  76. this.state = Recording.status.UNAVAILABLE;
  77. } else if (newState === Recording.status.OFF) {
  78. if (!this.state
  79. || this.state === 'undefined'
  80. || this.state === Recording.status.UNAVAILABLE) {
  81. this.state = Recording.status.AVAILABLE;
  82. } else {
  83. this.state = Recording.status.OFF;
  84. }
  85. } else {
  86. this.state = newState;
  87. }
  88. this.eventEmitter.emit(XMPPEvents.RECORDER_STATE_CHANGED, this.state);
  89. };
  90. /* eslint-disable max-params */
  91. Recording.prototype.setRecordingJibri = function(
  92. state,
  93. callback,
  94. errCallback,
  95. options = {}) {
  96. if (state === this.state) {
  97. errCallback(JitsiRecorderErrors.INVALID_STATE);
  98. }
  99. // FIXME jibri does not accept IQ without 'url' attribute set ?
  100. const iq
  101. = $iq({
  102. to: this.focusMucJid,
  103. type: 'set'
  104. })
  105. .c('jibri', {
  106. 'xmlns': 'http://jitsi.org/protocol/jibri',
  107. 'action': state === Recording.status.ON
  108. ? Recording.action.START
  109. : Recording.action.STOP,
  110. 'streamid': options.streamId
  111. })
  112. .up();
  113. logger.log(`Set jibri recording: ${state}`, iq.nodeTree);
  114. logger.log(iq.nodeTree);
  115. this.connection.sendIQ(
  116. iq,
  117. result => {
  118. logger.log('Result', result);
  119. const jibri = $(result).find('jibri');
  120. callback(jibri.attr('state'), jibri.attr('url'));
  121. },
  122. error => {
  123. logger.log('Failed to start recording, error: ', error);
  124. errCallback(error);
  125. });
  126. };
  127. /* eslint-enable max-params */
  128. Recording.prototype.setRecordingJirecon
  129. = function(state, callback, errCallback) {
  130. if (state === this.state) {
  131. errCallback(new Error('Invalid state!'));
  132. }
  133. const iq = $iq({ to: this.jirecon,
  134. type: 'set' })
  135. .c('recording', { xmlns: 'http://jitsi.org/protocol/jirecon',
  136. action: state === Recording.status.ON
  137. ? Recording.action.START
  138. : Recording.action.STOP,
  139. mucjid: this.roomjid });
  140. if (state === Recording.status.OFF) {
  141. iq.attrs({ rid: this.jireconRid });
  142. }
  143. logger.log('Start recording');
  144. const self = this;
  145. this.connection.sendIQ(
  146. iq,
  147. result => {
  148. // TODO wait for an IQ with the real status, since this is
  149. // provisional?
  150. // eslint-disable-next-line newline-per-chained-call
  151. self.jireconRid = $(result).find('recording').attr('rid');
  152. logger.log(
  153. `Recording ${
  154. state === Recording.status.ON ? 'started' : 'stopped'
  155. }(jirecon)${result}`);
  156. self.state = state;
  157. if (state === Recording.status.OFF) {
  158. self.jireconRid = null;
  159. }
  160. callback(state);
  161. },
  162. error => {
  163. logger.log('Failed to start recording, error: ', error);
  164. errCallback(error);
  165. });
  166. };
  167. /* eslint-disable max-params */
  168. // Sends a COLIBRI message which enables or disables (according to 'state')
  169. // the recording on the bridge. Waits for the result IQ and calls 'callback'
  170. // with the new recording state, according to the IQ.
  171. Recording.prototype.setRecordingColibri = function(
  172. state,
  173. callback,
  174. errCallback,
  175. options) {
  176. const elem = $iq({
  177. to: this.focusMucJid,
  178. type: 'set'
  179. });
  180. elem.c('conference', {
  181. xmlns: 'http://jitsi.org/protocol/colibri'
  182. });
  183. elem.c('recording', {
  184. state,
  185. token: options.token
  186. });
  187. const self = this;
  188. this.connection.sendIQ(
  189. elem,
  190. result => {
  191. logger.log('Set recording "', state, '". Result:', result);
  192. const recordingElem = $(result).find('>conference>recording');
  193. const newState = recordingElem.attr('state');
  194. self.state = newState;
  195. callback(newState);
  196. if (newState === 'pending') {
  197. self.connection.addHandler(iq => {
  198. // eslint-disable-next-line newline-per-chained-call
  199. const s = $(iq).find('recording').attr('state');
  200. if (s) {
  201. self.state = newState;
  202. callback(s);
  203. }
  204. }, 'http://jitsi.org/protocol/colibri', 'iq', null, null, null);
  205. }
  206. },
  207. error => {
  208. logger.warn(error);
  209. errCallback(error);
  210. }
  211. );
  212. };
  213. /* eslint-enable max-params */
  214. Recording.prototype.setRecording = function(...args) {
  215. switch (this.type) {
  216. case Recording.types.JIRECON:
  217. this.setRecordingJirecon(...args);
  218. break;
  219. case Recording.types.COLIBRI:
  220. this.setRecordingColibri(...args);
  221. break;
  222. case Recording.types.JIBRI:
  223. this.setRecordingJibri(...args);
  224. break;
  225. default: {
  226. const errmsg = 'Unknown recording type!';
  227. GlobalOnErrorHandler.callErrorHandler(new Error(errmsg));
  228. logger.error(errmsg);
  229. break;
  230. }
  231. }
  232. };
  233. /**
  234. * Starts/stops the recording.
  235. * @param token token for authentication
  236. * @param statusChangeHandler {function} receives the new status as argument.
  237. */
  238. Recording.prototype.toggleRecording = function(options, statusChangeHandler) {
  239. const oldState = this.state;
  240. // If the recorder is currently unavailable we throw an error.
  241. if (oldState === Recording.status.UNAVAILABLE
  242. || oldState === Recording.status.FAILED) {
  243. statusChangeHandler(Recording.status.FAILED,
  244. JitsiRecorderErrors.RECORDER_UNAVAILABLE);
  245. } else if (oldState === Recording.status.BUSY) {
  246. statusChangeHandler(Recording.status.BUSY,
  247. JitsiRecorderErrors.RECORDER_BUSY);
  248. }
  249. // If we're about to turn ON the recording we need either a streamId or
  250. // an authentication token depending on the recording type. If we don't
  251. // have any of those we throw an error.
  252. if ((oldState === Recording.status.OFF
  253. || oldState === Recording.status.AVAILABLE)
  254. && ((!options.token && this.type === Recording.types.COLIBRI)
  255. || (!options.streamId && this.type === Recording.types.JIBRI))) {
  256. statusChangeHandler(Recording.status.FAILED,
  257. JitsiRecorderErrors.NO_TOKEN);
  258. logger.error('No token passed!');
  259. return;
  260. }
  261. const newState = oldState === Recording.status.AVAILABLE
  262. || oldState === Recording.status.OFF
  263. ? Recording.status.ON
  264. : Recording.status.OFF;
  265. const self = this;
  266. logger.log('Toggle recording (old state, new state): ', oldState, newState);
  267. this.setRecording(
  268. newState,
  269. (state, url) => {
  270. // If the state is undefined we're going to wait for presence
  271. // update.
  272. if (state && state !== oldState) {
  273. self.state = state;
  274. self.url = url;
  275. statusChangeHandler(state);
  276. }
  277. },
  278. error => statusChangeHandler(Recording.status.FAILED, error),
  279. options);
  280. };
  281. /**
  282. * Returns true if the recording is supproted and false if not.
  283. */
  284. Recording.prototype.isSupported = function() {
  285. return this._isSupported;
  286. };
  287. /**
  288. * Returns null if the recording is not supported, "on" if the recording started
  289. * and "off" if the recording is not started.
  290. */
  291. Recording.prototype.getState = function() {
  292. return this.state;
  293. };
  294. /**
  295. * Returns the url of the recorded video.
  296. */
  297. Recording.prototype.getURL = function() {
  298. return this.url;
  299. };