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.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  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. if (document.body !== null) {
  43. document.body.appendChild(this._canvas);
  44. }
  45. this._desktopElement = document.createElement('video');
  46. this._videoElement = document.createElement('video');
  47. videoDiv.appendChild(this._videoElement);
  48. videoDiv.appendChild(this._desktopElement);
  49. if (document.body !== null) {
  50. document.body.appendChild(videoDiv);
  51. }
  52. // Set the video element properties
  53. this._frameRate = parseInt(frameRate, 10);
  54. this._videoElement.width = parseInt(width, 10);
  55. this._videoElement.height = parseInt(height, 10);
  56. this._videoElement.autoplay = true;
  57. this._videoElement.srcObject = videoStream;
  58. // set the style attribute of the div to make it invisible
  59. videoDiv.style.display = 'none';
  60. // Bind event handler so it is only bound once for every instance.
  61. this._onVideoFrameTimer = this._onVideoFrameTimer.bind(this);
  62. this._videoFrameTimerWorker = new Worker(timerWorkerScript);
  63. this._videoFrameTimerWorker.onmessage = this._onVideoFrameTimer;
  64. }
  65. /**
  66. * EventHandler onmessage for the videoFrameTimerWorker WebWorker.
  67. *
  68. * @private
  69. * @param {EventHandler} response - The onmessage EventHandler parameter.
  70. * @returns {void}
  71. */
  72. _onVideoFrameTimer(response) {
  73. if (response.data.id === INTERVAL_TIMEOUT) {
  74. this._renderVideo();
  75. }
  76. }
  77. /**
  78. * Loop function to render the video frame input and draw presenter effect.
  79. *
  80. * @private
  81. * @returns {void}
  82. */
  83. _renderVideo() {
  84. // adjust the canvas width/height on every frame incase the window has been resized.
  85. const [ track ] = this._desktopStream.getVideoTracks();
  86. const { height, width } = track.getSettings() ?? track.getConstraints();
  87. this._canvas.width = parseInt(width, 10);
  88. this._canvas.height = parseInt(height, 10);
  89. this._ctx.drawImage(this._desktopElement, 0, 0, this._canvas.width, this._canvas.height);
  90. this._ctx.drawImage(this._videoElement, this._canvas.width - this._videoElement.width, this._canvas.height
  91. - this._videoElement.height, this._videoElement.width, this._videoElement.height);
  92. // draw a border around the video element.
  93. this._ctx.beginPath();
  94. this._ctx.lineWidth = 2;
  95. this._ctx.strokeStyle = '#A9A9A9'; // dark grey
  96. this._ctx.rect(this._canvas.width - this._videoElement.width, this._canvas.height - this._videoElement.height,
  97. this._videoElement.width, this._videoElement.height);
  98. this._ctx.stroke();
  99. }
  100. /**
  101. * Checks if the local track supports this effect.
  102. *
  103. * @param {JitsiLocalTrack} jitsiLocalTrack - Track to apply effect.
  104. * @returns {boolean} - Returns true if this effect can run on the
  105. * specified track, false otherwise.
  106. */
  107. isEnabled(jitsiLocalTrack: Object) {
  108. return jitsiLocalTrack.isVideoTrack() && jitsiLocalTrack.videoType === 'desktop';
  109. }
  110. /**
  111. * Starts loop to capture video frame and render presenter effect.
  112. *
  113. * @param {MediaStream} desktopStream - Stream to be used for processing.
  114. * @returns {MediaStream} - The stream with the applied effect.
  115. */
  116. startEffect(desktopStream: MediaStream) {
  117. const firstVideoTrack = desktopStream.getVideoTracks()[0];
  118. const { height, width } = firstVideoTrack.getSettings() ?? firstVideoTrack.getConstraints();
  119. // set the desktop element properties.
  120. this._desktopStream = desktopStream;
  121. this._desktopElement.width = parseInt(width, 10);
  122. this._desktopElement.height = parseInt(height, 10);
  123. this._desktopElement.autoplay = true;
  124. this._desktopElement.srcObject = desktopStream;
  125. this._canvas.width = parseInt(width, 10);
  126. this._canvas.height = parseInt(height, 10);
  127. this._videoFrameTimerWorker.postMessage({
  128. id: SET_INTERVAL,
  129. timeMs: 1000 / this._frameRate
  130. });
  131. return this._canvas.captureStream(this._frameRate);
  132. }
  133. /**
  134. * Stops the capture and render loop.
  135. *
  136. * @returns {void}
  137. */
  138. stopEffect() {
  139. this._videoFrameTimerWorker.postMessage({
  140. id: CLEAR_INTERVAL
  141. });
  142. }
  143. }