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.

actions.js 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. import JitsiMeetJS, {
  2. JitsiTrackErrors,
  3. JitsiTrackEvents
  4. } from '../lib-jitsi-meet';
  5. import {
  6. CAMERA_FACING_MODE,
  7. MEDIA_TYPE,
  8. setAudioMuted,
  9. setVideoMuted
  10. } from '../media';
  11. import { getLocalParticipant } from '../participants';
  12. import {
  13. TRACK_ADDED,
  14. TRACK_REMOVED,
  15. TRACK_UPDATED
  16. } from './actionTypes';
  17. /**
  18. * Request to start capturing local audio and/or video. By default, the user
  19. * facing camera will be selected.
  20. *
  21. * @param {Object} [options] - For info @see JitsiMeetJS.createLocalTracks.
  22. * @returns {Function}
  23. */
  24. export function createLocalTracks(options = {}) {
  25. return dispatch =>
  26. JitsiMeetJS.createLocalTracks({
  27. cameraDeviceId: options.cameraDeviceId,
  28. devices: options.devices || [ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ],
  29. facingMode: options.facingMode || CAMERA_FACING_MODE.USER,
  30. micDeviceId: options.micDeviceId
  31. })
  32. .then(localTracks => dispatch(_updateLocalTracks(localTracks)))
  33. .catch(err => {
  34. console.error(
  35. `JitsiMeetJS.createLocalTracks.catch rejection reason: ${err}`);
  36. });
  37. }
  38. /**
  39. * Calls JitsiLocalTrack#dispose() on all local tracks ignoring errors when
  40. * track is already disposed. After that signals tracks to be removed.
  41. *
  42. * @returns {Function}
  43. */
  44. export function destroyLocalTracks() {
  45. return (dispatch, getState) =>
  46. dispatch(
  47. _disposeAndRemoveTracks(
  48. getState()['features/base/tracks']
  49. .filter(t => t.local)
  50. .map(t => t.jitsiTrack)));
  51. }
  52. /**
  53. * Replaces one track with another for one renegotiation instead of invoking
  54. * two renegotations with a separate removeTrack and addTrack. Disposes the
  55. * removed track as well.
  56. *
  57. * @param {JitsiLocalTrack|null} oldTrack - The track to dispose.
  58. * @param {JitsiLocalTrack|null} newTrack - The track to use instead.
  59. * @param {JitsiConference} [conference] - The conference from which to remove
  60. * and add the tracks. If one is not provied, the conference in the redux store
  61. * will be used.
  62. * @returns {Function}
  63. */
  64. export function replaceLocalTrack(oldTrack, newTrack, conference) {
  65. return (dispatch, getState) => {
  66. const currentConference = conference
  67. || getState()['features/base/conference'].conference;
  68. return currentConference.replaceTrack(oldTrack, newTrack)
  69. .then(() => {
  70. // We call dispose after doing the replace because
  71. // dispose will try and do a new o/a after the
  72. // track removes itself. Doing it after means
  73. // the JitsiLocalTrack::conference member is already
  74. // cleared, so it won't try and do the o/a
  75. const disposePromise = oldTrack
  76. ? dispatch(_disposeAndRemoveTracks([ oldTrack ]))
  77. : Promise.resolve();
  78. return disposePromise
  79. .then(() => {
  80. if (newTrack) {
  81. // The mute state of the new track should be
  82. // reflected in the app's mute state. For example,
  83. // if the app is currently muted and changing to a
  84. // new track that is not muted, the app's mute
  85. // state should be falsey. As such, emit a mute
  86. // event here to set up the app to reflect the
  87. // track's mute state. If this is not done, the
  88. // current mute state of the app will be reflected
  89. // on the track, not vice-versa.
  90. const muteAction = newTrack.isVideoTrack()
  91. ? setVideoMuted : setAudioMuted;
  92. return dispatch(muteAction(newTrack.isMuted()));
  93. }
  94. })
  95. .then(() => {
  96. if (newTrack) {
  97. return dispatch(_addTracks([ newTrack ]));
  98. }
  99. });
  100. });
  101. };
  102. }
  103. /**
  104. * Create an action for when a new track has been signaled to be added to the
  105. * conference.
  106. *
  107. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  108. * @returns {{ type: TRACK_ADDED, track: Track }}
  109. */
  110. export function trackAdded(track) {
  111. return (dispatch, getState) => {
  112. track.on(
  113. JitsiTrackEvents.TRACK_MUTE_CHANGED,
  114. () => dispatch(trackMutedChanged(track)));
  115. track.on(
  116. JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED,
  117. type => dispatch(trackVideoTypeChanged(track, type)));
  118. // participantId
  119. const local = track.isLocal();
  120. let participantId;
  121. if (local) {
  122. const participant = getLocalParticipant(getState);
  123. if (participant) {
  124. participantId = participant.id;
  125. }
  126. } else {
  127. participantId = track.getParticipantId();
  128. }
  129. return dispatch({
  130. type: TRACK_ADDED,
  131. track: {
  132. jitsiTrack: track,
  133. local,
  134. mediaType: track.getType(),
  135. mirror: _shouldMirror(track),
  136. muted: track.isMuted(),
  137. participantId,
  138. videoStarted: false,
  139. videoType: track.videoType
  140. }
  141. });
  142. };
  143. }
  144. /**
  145. * Create an action for when a track's muted state has been signaled to be
  146. * changed.
  147. *
  148. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  149. * @returns {{ type: TRACK_UPDATED, track: Track }}
  150. */
  151. export function trackMutedChanged(track) {
  152. return {
  153. type: TRACK_UPDATED,
  154. track: {
  155. jitsiTrack: track,
  156. muted: track.isMuted()
  157. }
  158. };
  159. }
  160. /**
  161. * Create an action for when a track has been signaled for removal from the
  162. * conference.
  163. *
  164. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  165. * @returns {{ type: TRACK_REMOVED, track: Track }}
  166. */
  167. export function trackRemoved(track) {
  168. return {
  169. type: TRACK_REMOVED,
  170. track: {
  171. jitsiTrack: track
  172. }
  173. };
  174. }
  175. /**
  176. * Signal that track's video started to play.
  177. *
  178. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  179. * @returns {{ type: TRACK_UPDATED, track: Track }}
  180. */
  181. export function trackVideoStarted(track) {
  182. return {
  183. type: TRACK_UPDATED,
  184. track: {
  185. jitsiTrack: track,
  186. videoStarted: true
  187. }
  188. };
  189. }
  190. /**
  191. * Create an action for when participant video type changes.
  192. *
  193. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  194. * @param {VIDEO_TYPE|undefined} videoType - Video type.
  195. * @returns {{ type: TRACK_UPDATED, track: Track }}
  196. */
  197. export function trackVideoTypeChanged(track, videoType) {
  198. return {
  199. type: TRACK_UPDATED,
  200. track: {
  201. jitsiTrack: track,
  202. videoType
  203. }
  204. };
  205. }
  206. /**
  207. * Signals passed tracks to be added.
  208. *
  209. * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} tracks - List of tracks.
  210. * @private
  211. * @returns {Function}
  212. */
  213. function _addTracks(tracks) {
  214. return dispatch =>
  215. Promise.all(tracks.map(t => dispatch(trackAdded(t))));
  216. }
  217. /**
  218. * Disposes passed tracks and signals them to be removed.
  219. *
  220. * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} tracks - List of tracks.
  221. * @protected
  222. * @returns {Function}
  223. */
  224. export function _disposeAndRemoveTracks(tracks) {
  225. return dispatch =>
  226. Promise.all(
  227. tracks.map(t =>
  228. t.dispose()
  229. .catch(err => {
  230. // Track might be already disposed so ignore such an
  231. // error. Of course, re-throw any other error(s).
  232. if (err.name !== JitsiTrackErrors.TRACK_IS_DISPOSED) {
  233. throw err;
  234. }
  235. })
  236. ))
  237. .then(Promise.all(tracks.map(t => dispatch(trackRemoved(t)))));
  238. }
  239. /**
  240. * Finds the first <tt>JitsiLocalTrack</tt> in a specific array/list of
  241. * <tt>JitsiTrack</tt>s which is of a specific <tt>MEDIA_TYPE</tt>.
  242. *
  243. * @param {JitsiTrack[]} tracks - The array/list of <tt>JitsiTrack</tt>s to look
  244. * through.
  245. * @param {MEDIA_TYPE} mediaType - The <tt>MEDIA_TYPE</tt> of the first
  246. * <tt>JitsiLocalTrack</tt> to be returned.
  247. * @private
  248. * @returns {JitsiLocalTrack} The first <tt>JitsiLocalTrack</tt>, if any, in the
  249. * specified <tt>tracks</tt> of the specified <tt>mediaType</tt>.
  250. */
  251. function _getLocalTrack(tracks, mediaType) {
  252. return tracks.find(track =>
  253. track.isLocal()
  254. // XXX JitsiTrack#getType() returns a MEDIA_TYPE value in the terms
  255. // of lib-jitsi-meet while mediaType is in the terms of jitsi-meet.
  256. && track.getType() === mediaType);
  257. }
  258. /**
  259. * Determines which local media tracks should be added and which removed.
  260. *
  261. * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} currentTracks - List of
  262. * current/existing media tracks.
  263. * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} newTracks - List of new media
  264. * tracks.
  265. * @private
  266. * @returns {{
  267. * tracksToAdd: JitsiLocalTrack[],
  268. * tracksToRemove: JitsiLocalTrack[]
  269. * }}
  270. */
  271. function _getLocalTracksToChange(currentTracks, newTracks) {
  272. const tracksToAdd = [];
  273. const tracksToRemove = [];
  274. for (const mediaType of [ MEDIA_TYPE.AUDIO, MEDIA_TYPE.VIDEO ]) {
  275. const newTrack = _getLocalTrack(newTracks, mediaType);
  276. if (newTrack) {
  277. const currentTrack = _getLocalTrack(currentTracks, mediaType);
  278. tracksToAdd.push(newTrack);
  279. currentTrack && tracksToRemove.push(currentTrack);
  280. }
  281. }
  282. return {
  283. tracksToAdd,
  284. tracksToRemove
  285. };
  286. }
  287. /**
  288. * Returns true if the provided JitsiTrack should be rendered as a mirror.
  289. *
  290. * We only want to show a video in mirrored mode when:
  291. * 1) The video source is local, and not remote.
  292. * 2) The video source is a camera, not a desktop (capture).
  293. * 3) The camera is capturing the user, not the environment.
  294. *
  295. * TODO Similar functionality is part of lib-jitsi-meet. This function should be
  296. * removed after https://github.com/jitsi/lib-jitsi-meet/pull/187 is merged.
  297. *
  298. * @param {(JitsiLocalTrack|JitsiRemoteTrack)} track - JitsiTrack instance.
  299. * @private
  300. * @returns {boolean}
  301. */
  302. function _shouldMirror(track) {
  303. return (
  304. track
  305. && track.isLocal()
  306. && track.isVideoTrack()
  307. // XXX The type of the return value of JitsiLocalTrack's
  308. // getCameraFacingMode happens to be named CAMERA_FACING_MODE as
  309. // well, it's defined by lib-jitsi-meet. Note though that the type
  310. // of the value on the right side of the equality check is defined
  311. // by jitsi-meet. The type definitions are surely compatible today
  312. // but that may not be the case tomorrow.
  313. && track.getCameraFacingMode() === CAMERA_FACING_MODE.USER
  314. );
  315. }
  316. /**
  317. * Set new local tracks replacing any existing tracks that were previously
  318. * available. Currently only one audio and one video local tracks are allowed.
  319. *
  320. * @param {(JitsiLocalTrack|JitsiRemoteTrack)[]} [newTracks=[]] - List of new
  321. * media tracks.
  322. * @private
  323. * @returns {Function}
  324. */
  325. function _updateLocalTracks(newTracks = []) {
  326. return (dispatch, getState) => {
  327. const tracks
  328. = getState()['features/base/tracks'].map(t => t.jitsiTrack);
  329. const { tracksToAdd, tracksToRemove }
  330. = _getLocalTracksToChange(tracks, newTracks);
  331. return dispatch(_disposeAndRemoveTracks(tracksToRemove))
  332. .then(() => dispatch(_addTracks(tracksToAdd)));
  333. };
  334. }