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 8.9KB

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