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.

recording.js 9.1KB

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