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.

strophe.jingle.sessionbase.js 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. /**
  2. * Base class for ColibriFocus and JingleSession.
  3. * @param connection Strophe connection object
  4. * @param sid my session identifier(resource)
  5. * @constructor
  6. */
  7. function SessionBase(connection, sid) {
  8. this.connection = connection;
  9. this.sid = sid;
  10. /**
  11. * The indicator which determines whether the (local) video has been muted
  12. * in response to a user command in contrast to an automatic decision made
  13. * by the application logic.
  14. */
  15. this.videoMuteByUser = false;
  16. }
  17. SessionBase.prototype.modifySources = function (successCallback) {
  18. var self = this;
  19. if(this.peerconnection)
  20. this.peerconnection.modifySources(function(){
  21. $(document).trigger('setLocalDescription.jingle', [self.sid]);
  22. if(successCallback) {
  23. successCallback();
  24. }
  25. });
  26. };
  27. SessionBase.prototype.addSource = function (elem, fromJid) {
  28. var self = this;
  29. // FIXME: dirty waiting
  30. if (!this.peerconnection.localDescription)
  31. {
  32. console.warn("addSource - localDescription not ready yet")
  33. setTimeout(function()
  34. {
  35. self.addSource(elem, fromJid);
  36. },
  37. 200
  38. );
  39. return;
  40. }
  41. this.peerconnection.addSource(elem);
  42. this.modifySources();
  43. };
  44. SessionBase.prototype.removeSource = function (elem, fromJid) {
  45. var self = this;
  46. // FIXME: dirty waiting
  47. if (!this.peerconnection.localDescription)
  48. {
  49. console.warn("removeSource - localDescription not ready yet")
  50. setTimeout(function()
  51. {
  52. self.removeSource(elem, fromJid);
  53. },
  54. 200
  55. );
  56. return;
  57. }
  58. this.peerconnection.removeSource(elem);
  59. this.modifySources();
  60. };
  61. /**
  62. * Switches video streams.
  63. * @param new_stream new stream that will be used as video of this session.
  64. * @param oldStream old video stream of this session.
  65. * @param success_callback callback executed after successful stream switch.
  66. */
  67. SessionBase.prototype.switchStreams = function (new_stream, oldStream, success_callback) {
  68. var self = this;
  69. // Stop the stream to trigger onended event for old stream
  70. oldStream.stop();
  71. // Remember SDP to figure out added/removed SSRCs
  72. var oldSdp = null;
  73. if(self.peerconnection) {
  74. if(self.peerconnection.localDescription) {
  75. oldSdp = new SDP(self.peerconnection.localDescription.sdp);
  76. }
  77. self.peerconnection.removeStream(oldStream, true);
  78. self.peerconnection.addStream(new_stream);
  79. }
  80. self.connection.jingle.localVideo = new_stream;
  81. self.connection.jingle.localStreams = [];
  82. //in firefox we have only one stream object
  83. if(self.connection.jingle.localAudio != self.connection.jingle.localVideo)
  84. self.connection.jingle.localStreams.push(self.connection.jingle.localAudio);
  85. self.connection.jingle.localStreams.push(self.connection.jingle.localVideo);
  86. // Conference is not active
  87. if(!oldSdp || !self.peerconnection) {
  88. success_callback();
  89. return;
  90. }
  91. self.peerconnection.switchstreams = true;
  92. self.modifySources(function() {
  93. console.log('modify sources done');
  94. success_callback();
  95. var newSdp = new SDP(self.peerconnection.localDescription.sdp);
  96. console.log("SDPs", oldSdp, newSdp);
  97. self.notifyMySSRCUpdate(oldSdp, newSdp);
  98. });
  99. };
  100. /**
  101. * Figures out added/removed ssrcs and send update IQs.
  102. * @param old_sdp SDP object for old description.
  103. * @param new_sdp SDP object for new description.
  104. */
  105. SessionBase.prototype.notifyMySSRCUpdate = function (old_sdp, new_sdp) {
  106. var old_media = old_sdp.getMediaSsrcMap();
  107. var new_media = new_sdp.getMediaSsrcMap();
  108. //console.log("old/new medias: ", old_media, new_media);
  109. var toAdd = old_sdp.getNewMedia(new_sdp);
  110. var toRemove = new_sdp.getNewMedia(old_sdp);
  111. //console.log("to add", toAdd);
  112. //console.log("to remove", toRemove);
  113. if(Object.keys(toRemove).length > 0){
  114. this.sendSSRCUpdate(toRemove, null, false);
  115. }
  116. if(Object.keys(toAdd).length > 0){
  117. this.sendSSRCUpdate(toAdd, null, true);
  118. }
  119. };
  120. /**
  121. * Empty method that does nothing by default. It should send SSRC update IQs to session participants.
  122. * @param sdpMediaSsrcs array of
  123. * @param fromJid
  124. * @param isAdd
  125. */
  126. SessionBase.prototype.sendSSRCUpdate = function(sdpMediaSsrcs, fromJid, isAdd) {
  127. //FIXME: put default implementation here(maybe from JingleSession?)
  128. }
  129. /**
  130. * Sends SSRC update IQ.
  131. * @param sdpMediaSsrcs SSRCs map obtained from SDP.getNewMedia. Cntains SSRCs to add/remove.
  132. * @param sid session identifier that will be put into the IQ.
  133. * @param initiator initiator identifier.
  134. * @param toJid destination Jid
  135. * @param isAdd indicates if this is remove or add operation.
  136. */
  137. SessionBase.prototype.sendSSRCUpdateIq = function(sdpMediaSsrcs, sid, initiator, toJid, isAdd) {
  138. var self = this;
  139. var modify = $iq({to: toJid, type: 'set'})
  140. .c('jingle', {
  141. xmlns: 'urn:xmpp:jingle:1',
  142. action: isAdd ? 'source-add' : 'source-remove',
  143. initiator: initiator,
  144. sid: sid
  145. }
  146. );
  147. // FIXME: only announce video ssrcs since we mix audio and dont need
  148. // the audio ssrcs therefore
  149. var modified = false;
  150. Object.keys(sdpMediaSsrcs).forEach(function(channelNum){
  151. modified = true;
  152. var channel = sdpMediaSsrcs[channelNum];
  153. modify.c('content', {name: channel.mediaType});
  154. modify.c('description', {xmlns:'urn:xmpp:jingle:apps:rtp:1', media: channel.mediaType});
  155. // FIXME: not completly sure this operates on blocks and / or handles different ssrcs correctly
  156. // generate sources from lines
  157. Object.keys(channel.ssrcs).forEach(function(ssrcNum) {
  158. var mediaSsrc = channel.ssrcs[ssrcNum];
  159. modify.c('source', { xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0' });
  160. modify.attrs({ssrc: mediaSsrc.ssrc});
  161. // iterate over ssrc lines
  162. mediaSsrc.lines.forEach(function (line) {
  163. var idx = line.indexOf(' ');
  164. var kv = line.substr(idx + 1);
  165. modify.c('parameter');
  166. if (kv.indexOf(':') == -1) {
  167. modify.attrs({ name: kv });
  168. } else {
  169. modify.attrs({ name: kv.split(':', 2)[0] });
  170. modify.attrs({ value: kv.split(':', 2)[1] });
  171. }
  172. modify.up(); // end of parameter
  173. });
  174. modify.up(); // end of source
  175. });
  176. // generate source groups from lines
  177. channel.ssrcGroups.forEach(function(ssrcGroup) {
  178. if (ssrcGroup.ssrcs.length != 0) {
  179. modify.c('ssrc-group', {
  180. semantics: ssrcGroup.semantics,
  181. xmlns: 'urn:xmpp:jingle:apps:rtp:ssma:0'
  182. });
  183. ssrcGroup.ssrcs.forEach(function (ssrc) {
  184. modify.c('source', { ssrc: ssrc })
  185. .up(); // end of source
  186. });
  187. modify.up(); // end of ssrc-group
  188. }
  189. });
  190. modify.up(); // end of description
  191. modify.up(); // end of content
  192. });
  193. if (modified) {
  194. self.connection.sendIQ(modify,
  195. function (res) {
  196. console.info('got modify result', res);
  197. },
  198. function (err) {
  199. console.error('got modify error', err);
  200. }
  201. );
  202. } else {
  203. console.log('modification not necessary');
  204. }
  205. };
  206. /**
  207. * Determines whether the (local) video is mute i.e. all video tracks are
  208. * disabled.
  209. *
  210. * @return <tt>true</tt> if the (local) video is mute i.e. all video tracks are
  211. * disabled; otherwise, <tt>false</tt>
  212. */
  213. SessionBase.prototype.isVideoMute = function () {
  214. var tracks = connection.jingle.localVideo.getVideoTracks();
  215. var mute = true;
  216. for (var i = 0; i < tracks.length; ++i) {
  217. if (tracks[i].enabled) {
  218. mute = false;
  219. break;
  220. }
  221. }
  222. return mute;
  223. };
  224. /**
  225. * Mutes/unmutes the (local) video i.e. enables/disables all video tracks.
  226. *
  227. * @param mute <tt>true</tt> to mute the (local) video i.e. to disable all video
  228. * tracks; otherwise, <tt>false</tt>
  229. * @param callback a function to be invoked with <tt>mute</tt> after all video
  230. * tracks have been enabled/disabled. The function may, optionally, return
  231. * another function which is to be invoked after the whole mute/unmute operation
  232. * has completed successfully.
  233. * @param options an object which specifies optional arguments such as the
  234. * <tt>boolean</tt> key <tt>byUser</tt> with default value <tt>true</tt> which
  235. * specifies whether the method was initiated in response to a user command (in
  236. * contrast to an automatic decision made by the application logic)
  237. */
  238. SessionBase.prototype.setVideoMute = function (mute, callback, options) {
  239. var byUser;
  240. if (options) {
  241. byUser = options.byUser;
  242. if (typeof byUser === 'undefined') {
  243. byUser = true;
  244. }
  245. } else {
  246. byUser = true;
  247. }
  248. // The user's command to mute the (local) video takes precedence over any
  249. // automatic decision made by the application logic.
  250. if (byUser) {
  251. this.videoMuteByUser = mute;
  252. } else if (this.videoMuteByUser) {
  253. return;
  254. }
  255. if (mute == this.isVideoMute())
  256. {
  257. // Even if no change occurs, the specified callback is to be executed.
  258. // The specified callback may, optionally, return a successCallback
  259. // which is to be executed as well.
  260. var successCallback = callback(mute);
  261. if (successCallback) {
  262. successCallback();
  263. }
  264. } else {
  265. var tracks = connection.jingle.localVideo.getVideoTracks();
  266. for (var i = 0; i < tracks.length; ++i) {
  267. tracks[i].enabled = !mute;
  268. }
  269. if (this.peerconnection) {
  270. this.peerconnection.hardMuteVideo(mute);
  271. }
  272. this.modifySources(callback(mute));
  273. }
  274. };
  275. // SDP-based mute by going recvonly/sendrecv
  276. // FIXME: should probably black out the screen as well
  277. SessionBase.prototype.toggleVideoMute = function (callback) {
  278. setVideoMute(isVideoMute(), callback);
  279. };