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.

AbstractVideoManager.js 9.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. /* @flow */
  2. /* eslint-disable no-invalid-this */
  3. import Logger from 'jitsi-meet-logger';
  4. import throttle from 'lodash/throttle';
  5. import { Component } from 'react';
  6. import { sendAnalytics, createSharedVideoEvent as createEvent } from '../../../analytics';
  7. import { getCurrentConference } from '../../../base/conference';
  8. import { MEDIA_TYPE } from '../../../base/media';
  9. import { getLocalParticipant } from '../../../base/participants';
  10. import { isLocalTrackMuted } from '../../../base/tracks';
  11. import { showWarningNotification } from '../../../notifications/actions';
  12. import { dockToolbox } from '../../../toolbox/actions.web';
  13. import { muteLocal } from '../../../video-menu/actions.any';
  14. import { setSharedVideoStatus, stopSharedVideo } from '../../actions.any';
  15. export const PLAYBACK_STATES = {
  16. PLAYING: 'playing',
  17. PAUSED: 'pause',
  18. STOPPED: 'stop'
  19. };
  20. const logger = Logger.getLogger(__filename);
  21. /**
  22. * Return true if the diffenrece between the two timees is larger than 5.
  23. *
  24. * @param {number} newTime - The current time.
  25. * @param {number} previousTime - The previous time.
  26. * @private
  27. * @returns {boolean}
  28. */
  29. function shouldSeekToPosition(newTime, previousTime) {
  30. return Math.abs(newTime - previousTime) > 5;
  31. }
  32. /**
  33. * The type of the React {@link Component} props of {@link YoutubeLargeVideo}.
  34. */
  35. export type Props = {
  36. /**
  37. * The current coference
  38. */
  39. _conference: Object,
  40. /**
  41. * Warning that indicates an incorect video url
  42. */
  43. _displayWarning: Function,
  44. /**
  45. * Docks the toolbox
  46. */
  47. _dockToolbox: Function,
  48. /**
  49. * Action to stop video sharing
  50. */
  51. _stopSharedVideo: Function,
  52. /**
  53. * Indicates whether the local audio is muted
  54. */
  55. _isLocalAudioMuted: boolean,
  56. /**
  57. * Is the video shared by the local user.
  58. *
  59. * @private
  60. */
  61. _isOwner: boolean,
  62. /**
  63. * Store flag for muted state
  64. */
  65. _muted: boolean,
  66. /**
  67. * Mutes local audio track
  68. */
  69. _muteLocal: Function,
  70. /**
  71. * The shared video owner id
  72. */
  73. _ownerId: string,
  74. /**
  75. * Updates the shared video status
  76. */
  77. _setSharedVideoStatus: Function,
  78. /**
  79. * The shared video status
  80. */
  81. _status: string,
  82. /**
  83. * Seek time in seconds.
  84. *
  85. */
  86. _time: number,
  87. /**
  88. * The video url
  89. */
  90. _videoUrl: string,
  91. /**
  92. * The video id
  93. */
  94. videoId: string
  95. }
  96. /**
  97. * Manager of shared video.
  98. */
  99. class AbstractVideoManager extends Component<Props> {
  100. throttledFireUpdateSharedVideoEvent: Function;
  101. /**
  102. * Initializes a new instance of AbstractVideoManager.
  103. *
  104. * @returns {void}
  105. */
  106. constructor() {
  107. super();
  108. this.throttledFireUpdateSharedVideoEvent = throttle(this.fireUpdateSharedVideoEvent.bind(this), 5000);
  109. // selenium tests handler
  110. window._sharedVideoPlayer = this;
  111. }
  112. /**
  113. * Implements React Component's componentDidMount.
  114. *
  115. * @inheritdoc
  116. */
  117. componentDidMount() {
  118. this.props._dockToolbox(true);
  119. this.processUpdatedProps();
  120. }
  121. /**
  122. * Implements React Component's componentDidUpdate.
  123. *
  124. * @inheritdoc
  125. */
  126. componentDidUpdate(prevProps: Props) {
  127. const { _videoUrl } = this.props;
  128. if (prevProps._videoUrl !== _videoUrl) {
  129. sendAnalytics(createEvent('started'));
  130. }
  131. this.processUpdatedProps();
  132. }
  133. /**
  134. * Implements React Component's componentWillUnmount.
  135. *
  136. * @inheritdoc
  137. */
  138. componentWillUnmount() {
  139. sendAnalytics(createEvent('stopped'));
  140. if (this.dispose) {
  141. this.dispose();
  142. }
  143. this.props._dockToolbox(false);
  144. }
  145. /**
  146. * Processes new properties.
  147. *
  148. * @returns {void}
  149. */
  150. processUpdatedProps() {
  151. const { _status, _time, _isOwner, _muted } = this.props;
  152. if (_isOwner) {
  153. return;
  154. }
  155. const playerTime = this.getTime();
  156. if (shouldSeekToPosition(_time, playerTime)) {
  157. this.seek(_time);
  158. }
  159. if (this.getPlaybackState() !== _status) {
  160. if (_status === PLAYBACK_STATES.PLAYING) {
  161. this.play();
  162. }
  163. if (_status === PLAYBACK_STATES.PAUSED) {
  164. this.pause();
  165. }
  166. }
  167. if (this.isMuted() !== _muted) {
  168. if (_muted) {
  169. this.mute();
  170. } else {
  171. this.unMute();
  172. }
  173. }
  174. }
  175. /**
  176. * Handle video error.
  177. *
  178. * @returns {void}
  179. */
  180. onError() {
  181. logger.error('Error in the video player');
  182. this.props._stopSharedVideo();
  183. this.props._displayWarning();
  184. }
  185. /**
  186. * Handle video playing.
  187. *
  188. * @returns {void}
  189. */
  190. onPlay() {
  191. this.smartAudioMute();
  192. sendAnalytics(createEvent('play'));
  193. this.fireUpdateSharedVideoEvent();
  194. }
  195. /**
  196. * Handle video paused.
  197. *
  198. * @returns {void}
  199. */
  200. onPause() {
  201. sendAnalytics(createEvent('paused'));
  202. this.fireUpdateSharedVideoEvent();
  203. }
  204. /**
  205. * Handle volume changed.
  206. *
  207. * @returns {void}
  208. */
  209. onVolumeChange() {
  210. const volume = this.getVolume();
  211. const muted = this.isMuted();
  212. if (volume > 0 && !muted) {
  213. this.smartAudioMute();
  214. }
  215. sendAnalytics(createEvent(
  216. 'volume.changed',
  217. {
  218. volume,
  219. muted
  220. }));
  221. this.fireUpdatePlayingVideoEvent();
  222. }
  223. /**
  224. * Handle changes to the shared playing video.
  225. *
  226. * @returns {void}
  227. */
  228. fireUpdatePlayingVideoEvent() {
  229. if (this.getPlaybackState() === PLAYBACK_STATES.PLAYING) {
  230. this.fireUpdateSharedVideoEvent();
  231. }
  232. }
  233. /**
  234. * Dispatches an update action for the shared video.
  235. *
  236. * @returns {void}
  237. */
  238. fireUpdateSharedVideoEvent() {
  239. const { _isOwner } = this.props;
  240. if (!_isOwner) {
  241. return;
  242. }
  243. const status = this.getPlaybackState();
  244. if (!Object.values(PLAYBACK_STATES).includes(status)) {
  245. return;
  246. }
  247. const {
  248. _ownerId,
  249. _setSharedVideoStatus,
  250. _videoUrl
  251. } = this.props;
  252. _setSharedVideoStatus({
  253. videoUrl: _videoUrl,
  254. status,
  255. time: this.getTime(),
  256. ownerId: _ownerId,
  257. muted: this.isMuted()
  258. });
  259. }
  260. /**
  261. * Indicates if the player volume is currently on. This will return true if
  262. * we have an available player, which is currently in a PLAYING state,
  263. * which isn't muted and has it's volume greater than 0.
  264. *
  265. * @returns {boolean} Indicating if the volume of the shared video is
  266. * currently on.
  267. */
  268. isSharedVideoVolumeOn() {
  269. return this.getPlaybackState() === PLAYBACK_STATES.PLAYING
  270. && !this.isMuted()
  271. && this.getVolume() > 0;
  272. }
  273. /**
  274. * Smart mike mute. If the mike isn't currently muted and the shared video
  275. * volume is on we mute the mike.
  276. *
  277. * @returns {void}
  278. */
  279. smartAudioMute() {
  280. const { _isLocalAudioMuted, _muteLocal } = this.props;
  281. if (!_isLocalAudioMuted
  282. && this.isSharedVideoVolumeOn()) {
  283. sendAnalytics(createEvent('audio.muted'));
  284. _muteLocal(true);
  285. }
  286. }
  287. /**
  288. * Seeks video to provided time
  289. * @param {number} time
  290. */
  291. seek: (time: number) => void;
  292. /**
  293. * Indicates the playback state of the video
  294. */
  295. getPlaybackState: () => boolean;
  296. /**
  297. * Indicates whether the video is muted
  298. */
  299. isMuted: () => boolean;
  300. /**
  301. * Retrieves current volume
  302. */
  303. getVolume: () => number;
  304. /**
  305. * Sets current volume
  306. */
  307. setVolume: (value: number) => void;
  308. /**
  309. * Plays video
  310. */
  311. play: () => void;
  312. /**
  313. * Pauses video
  314. */
  315. pause: () => void;
  316. /**
  317. * Mutes video
  318. */
  319. mute: () => void;
  320. /**
  321. * Unmutes video
  322. */
  323. unMute: () => void;
  324. /**
  325. * Retrieves current time
  326. */
  327. getTime: () => number;
  328. /**
  329. * Disposes current video player
  330. */
  331. dispose: () => void;
  332. }
  333. export default AbstractVideoManager;
  334. /**
  335. * Maps part of the Redux store to the props of this component.
  336. *
  337. * @param {Object} state - The Redux state.
  338. * @returns {Props}
  339. */
  340. export function _mapStateToProps(state: Object): $Shape<Props> {
  341. const { ownerId, status, time, videoUrl, muted } = state['features/shared-video'];
  342. const localParticipant = getLocalParticipant(state);
  343. const _isLocalAudioMuted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);
  344. return {
  345. _conference: getCurrentConference(state),
  346. _isLocalAudioMuted,
  347. _isOwner: ownerId === localParticipant.id,
  348. _muted: muted,
  349. _ownerId: ownerId,
  350. _status: status,
  351. _time: time,
  352. _videoUrl: videoUrl
  353. };
  354. }
  355. /**
  356. * Maps part of the props of this component to Redux actions.
  357. *
  358. * @param {Function} dispatch - The Redux dispatch function.
  359. * @returns {Props}
  360. */
  361. export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
  362. return {
  363. _displayWarning: () => {
  364. dispatch(showWarningNotification({
  365. titleKey: 'dialog.shareVideoLinkError'
  366. }));
  367. },
  368. _dockToolbox: value => {
  369. dispatch(dockToolbox(value));
  370. },
  371. _stopSharedVideo: () => {
  372. dispatch(stopSharedVideo());
  373. },
  374. _muteLocal: value => {
  375. dispatch(muteLocal(value, MEDIA_TYPE.AUDIO));
  376. },
  377. _setSharedVideoStatus: ({ videoUrl, status, time, ownerId, muted }) => {
  378. dispatch(setSharedVideoStatus({
  379. videoUrl,
  380. status,
  381. time,
  382. ownerId,
  383. muted
  384. }));
  385. }
  386. };
  387. }