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.

JitsiStreamPresenterEffect.js 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. // @flow
  2. import {
  3. CLEAR_INTERVAL,
  4. INTERVAL_TIMEOUT,
  5. SET_INTERVAL,
  6. timerWorkerScript
  7. } from './TimeWorker';
  8. /**
  9. * Represents a modified MediaStream that adds video as pip on a desktop stream.
  10. * <tt>JitsiStreamPresenterEffect</tt> does the processing of the original
  11. * desktop stream.
  12. */
  13. export default class JitsiStreamPresenterEffect {
  14. _canvas: HTMLCanvasElement;
  15. _ctx: CanvasRenderingContext2D;
  16. _desktopElement: HTMLVideoElement;
  17. _desktopStream: MediaStream;
  18. _frameRate: number;
  19. _onVideoFrameTimer: Function;
  20. _onVideoFrameTimerWorker: Function;
  21. _renderVideo: Function;
  22. _videoFrameTimerWorker: Worker;
  23. _videoElement: HTMLVideoElement;
  24. isEnabled: Function;
  25. startEffect: Function;
  26. stopEffect: Function;
  27. /**
  28. * Represents a modified MediaStream that adds a camera track at the
  29. * bottom right corner of the desktop track using a HTML canvas.
  30. * <tt>JitsiStreamPresenterEffect</tt> does the processing of the original
  31. * video stream.
  32. *
  33. * @param {MediaStream} videoStream - The video stream which is user for
  34. * creating the canvas.
  35. */
  36. constructor(videoStream: MediaStream) {
  37. const videoDiv = document.createElement('div');
  38. const firstVideoTrack = videoStream.getVideoTracks()[0];
  39. const { height, width, frameRate } = firstVideoTrack.getSettings() ?? firstVideoTrack.getConstraints();
  40. this._canvas = document.createElement('canvas');
  41. this._ctx = this._canvas.getContext('2d');
  42. this._desktopElement = document.createElement('video');
  43. this._videoElement = document.createElement('video');
  44. videoDiv.appendChild(this._videoElement);
  45. videoDiv.appendChild(this._desktopElement);
  46. if (document.body !== null) {
  47. document.body.appendChild(videoDiv);
  48. }
  49. // Set the video element properties
  50. this._frameRate = parseInt(frameRate, 10);
  51. this._videoElement.width = parseInt(width, 10);
  52. this._videoElement.height = parseInt(height, 10);
  53. this._videoElement.autoplay = true;
  54. this._videoElement.srcObject = videoStream;
  55. // set the style attribute of the div to make it invisible
  56. videoDiv.style.display = 'none';
  57. // Bind event handler so it is only bound once for every instance.
  58. this._onVideoFrameTimer = this._onVideoFrameTimer.bind(this);
  59. this._videoFrameTimerWorker = new Worker(timerWorkerScript);
  60. this._videoFrameTimerWorker.onmessage = this._onVideoFrameTimer;
  61. }
  62. /**
  63. * EventHandler onmessage for the videoFrameTimerWorker WebWorker.
  64. *
  65. * @private
  66. * @param {EventHandler} response - The onmessage EventHandler parameter.
  67. * @returns {void}
  68. */
  69. _onVideoFrameTimer(response) {
  70. if (response.data.id === INTERVAL_TIMEOUT) {
  71. this._renderVideo();
  72. }
  73. }
  74. /**
  75. * Loop function to render the video frame input and draw presenter effect.
  76. *
  77. * @private
  78. * @returns {void}
  79. */
  80. _renderVideo() {
  81. // adjust the canvas width/height on every frame incase the window has been resized.
  82. const [ track ] = this._desktopStream.getVideoTracks();
  83. const { height, width } = track.getSettings() ?? track.getConstraints();
  84. this._canvas.width = parseInt(width, 10);
  85. this._canvas.height = parseInt(height, 10);
  86. this._ctx.drawImage(this._desktopElement, 0, 0, this._canvas.width, this._canvas.height);
  87. this._ctx.drawImage(this._videoElement, this._canvas.width - this._videoElement.width, this._canvas.height
  88. - this._videoElement.height, this._videoElement.width, this._videoElement.height);
  89. // draw a border around the video element.
  90. this._ctx.beginPath();
  91. this._ctx.lineWidth = 2;
  92. this._ctx.strokeStyle = '#A9A9A9'; // dark grey
  93. this._ctx.rect(this._canvas.width - this._videoElement.width, this._canvas.height - this._videoElement.height,
  94. this._videoElement.width, this._videoElement.height);
  95. this._ctx.stroke();
  96. }
  97. /**
  98. * Checks if the local track supports this effect.
  99. *
  100. * @param {JitsiLocalTrack} jitsiLocalTrack - Track to apply effect.
  101. * @returns {boolean} - Returns true if this effect can run on the
  102. * specified track, false otherwise.
  103. */
  104. isEnabled(jitsiLocalTrack: Object) {
  105. return jitsiLocalTrack.isVideoTrack() && jitsiLocalTrack.videoType === 'desktop';
  106. }
  107. /**
  108. * Starts loop to capture video frame and render presenter effect.
  109. *
  110. * @param {MediaStream} desktopStream - Stream to be used for processing.
  111. * @returns {MediaStream} - The stream with the applied effect.
  112. */
  113. startEffect(desktopStream: MediaStream) {
  114. const firstVideoTrack = desktopStream.getVideoTracks()[0];
  115. const { height, width } = firstVideoTrack.getSettings() ?? firstVideoTrack.getConstraints();
  116. // set the desktop element properties.
  117. this._desktopStream = desktopStream;
  118. this._desktopElement.width = parseInt(width, 10);
  119. this._desktopElement.height = parseInt(height, 10);
  120. this._desktopElement.autoplay = true;
  121. this._desktopElement.srcObject = desktopStream;
  122. this._canvas.width = parseInt(width, 10);
  123. this._canvas.height = parseInt(height, 10);
  124. this._videoFrameTimerWorker.postMessage({
  125. id: SET_INTERVAL,
  126. timeMs: 1000 / this._frameRate
  127. });
  128. return this._canvas.captureStream(this._frameRate);
  129. }
  130. /**
  131. * Stops the capture and render loop.
  132. *
  133. * @returns {void}
  134. */
  135. stopEffect() {
  136. this._videoFrameTimerWorker.postMessage({
  137. id: CLEAR_INTERVAL
  138. });
  139. }
  140. }