Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

JitsiStreamBackgroundEffect.ts 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { VIRTUAL_BACKGROUND_TYPE } from '../../virtual-background/constants';
  2. import {
  3. CLEAR_TIMEOUT,
  4. SET_TIMEOUT,
  5. TIMEOUT_TICK,
  6. timerWorkerScript
  7. } from './TimerWorker';
  8. export interface IBackgroundEffectOptions {
  9. height: number;
  10. virtualBackground: {
  11. backgroundType?: string;
  12. blurValue?: number;
  13. virtualSource?: string;
  14. };
  15. width: number;
  16. }
  17. /**
  18. * Represents a modified MediaStream that adds effects to video background.
  19. * <tt>JitsiStreamBackgroundEffect</tt> does the processing of the original
  20. * video stream.
  21. */
  22. export default class JitsiStreamBackgroundEffect {
  23. _model: any;
  24. _options: IBackgroundEffectOptions;
  25. _stream: any;
  26. _segmentationPixelCount: number;
  27. _inputVideoElement: HTMLVideoElement;
  28. _maskFrameTimerWorker: Worker;
  29. _outputCanvasElement: HTMLCanvasElement;
  30. _outputCanvasCtx: CanvasRenderingContext2D | null;
  31. _segmentationMaskCtx: CanvasRenderingContext2D | null;
  32. _segmentationMask: ImageData;
  33. _segmentationMaskCanvas: HTMLCanvasElement;
  34. _virtualImage: HTMLImageElement;
  35. _virtualVideo: HTMLVideoElement;
  36. /**
  37. * Represents a modified video MediaStream track.
  38. *
  39. * @class
  40. * @param {Object} model - Meet model.
  41. * @param {Object} options - Segmentation dimensions.
  42. */
  43. constructor(model: Object, options: IBackgroundEffectOptions) {
  44. this._options = options;
  45. if (this._options.virtualBackground.backgroundType === VIRTUAL_BACKGROUND_TYPE.IMAGE) {
  46. this._virtualImage = document.createElement('img');
  47. this._virtualImage.crossOrigin = 'anonymous';
  48. this._virtualImage.src = this._options.virtualBackground.virtualSource ?? '';
  49. }
  50. this._model = model;
  51. this._segmentationPixelCount = this._options.width * this._options.height;
  52. // Bind event handler so it is only bound once for every instance.
  53. this._onMaskFrameTimer = this._onMaskFrameTimer.bind(this);
  54. // Workaround for FF issue https://bugzilla.mozilla.org/show_bug.cgi?id=1388974
  55. this._outputCanvasElement = document.createElement('canvas');
  56. this._outputCanvasElement.getContext('2d');
  57. this._inputVideoElement = document.createElement('video');
  58. }
  59. /**
  60. * EventHandler onmessage for the maskFrameTimerWorker WebWorker.
  61. *
  62. * @private
  63. * @param {EventHandler} response - The onmessage EventHandler parameter.
  64. * @returns {void}
  65. */
  66. _onMaskFrameTimer(response: { data: { id: number; }; }) {
  67. if (response.data.id === TIMEOUT_TICK) {
  68. this._renderMask();
  69. }
  70. }
  71. /**
  72. * Represents the run post processing.
  73. *
  74. * @returns {void}
  75. */
  76. runPostProcessing() {
  77. const track = this._stream.getVideoTracks()[0];
  78. const { height, width } = track.getSettings() ?? track.getConstraints();
  79. const { backgroundType } = this._options.virtualBackground;
  80. if (!this._outputCanvasCtx) {
  81. return;
  82. }
  83. this._outputCanvasElement.height = height;
  84. this._outputCanvasElement.width = width;
  85. this._outputCanvasCtx.globalCompositeOperation = 'copy';
  86. // Draw segmentation mask.
  87. // Smooth out the edges.
  88. this._outputCanvasCtx.filter = backgroundType === VIRTUAL_BACKGROUND_TYPE.IMAGE ? 'blur(4px)' : 'blur(8px)';
  89. this._outputCanvasCtx?.drawImage( // @ts-ignore
  90. this._segmentationMaskCanvas,
  91. 0,
  92. 0,
  93. this._options.width,
  94. this._options.height,
  95. 0,
  96. 0,
  97. this._inputVideoElement.width,
  98. this._inputVideoElement.height
  99. );
  100. this._outputCanvasCtx.globalCompositeOperation = 'source-in';
  101. this._outputCanvasCtx.filter = 'none';
  102. // Draw the foreground video.
  103. // @ts-ignore
  104. this._outputCanvasCtx?.drawImage(this._inputVideoElement, 0, 0);
  105. // Draw the background.
  106. this._outputCanvasCtx.globalCompositeOperation = 'destination-over';
  107. if (backgroundType === VIRTUAL_BACKGROUND_TYPE.IMAGE) {
  108. this._outputCanvasCtx?.drawImage( // @ts-ignore
  109. backgroundType === VIRTUAL_BACKGROUND_TYPE.IMAGE
  110. ? this._virtualImage : this._virtualVideo,
  111. 0,
  112. 0,
  113. this._outputCanvasElement.width,
  114. this._outputCanvasElement.height
  115. );
  116. } else {
  117. this._outputCanvasCtx.filter = `blur(${this._options.virtualBackground.blurValue}px)`;
  118. // @ts-ignore
  119. this._outputCanvasCtx?.drawImage(this._inputVideoElement, 0, 0);
  120. }
  121. }
  122. /**
  123. * Represents the run Tensorflow Interference.
  124. *
  125. * @returns {void}
  126. */
  127. runInference() {
  128. this._model._runInference();
  129. const outputMemoryOffset = this._model._getOutputMemoryOffset() / 4;
  130. for (let i = 0; i < this._segmentationPixelCount; i++) {
  131. const person = this._model.HEAPF32[outputMemoryOffset + i];
  132. // Sets only the alpha component of each pixel.
  133. this._segmentationMask.data[(i * 4) + 3] = 255 * person;
  134. }
  135. this._segmentationMaskCtx?.putImageData(this._segmentationMask, 0, 0);
  136. }
  137. /**
  138. * Loop function to render the background mask.
  139. *
  140. * @private
  141. * @returns {void}
  142. */
  143. _renderMask() {
  144. this.resizeSource();
  145. this.runInference();
  146. this.runPostProcessing();
  147. this._maskFrameTimerWorker.postMessage({
  148. id: SET_TIMEOUT,
  149. timeMs: 1000 / 30
  150. });
  151. }
  152. /**
  153. * Represents the resize source process.
  154. *
  155. * @returns {void}
  156. */
  157. resizeSource() {
  158. this._segmentationMaskCtx?.drawImage( // @ts-ignore
  159. this._inputVideoElement,
  160. 0,
  161. 0,
  162. this._inputVideoElement.width,
  163. this._inputVideoElement.height,
  164. 0,
  165. 0,
  166. this._options.width,
  167. this._options.height
  168. );
  169. const imageData = this._segmentationMaskCtx?.getImageData(
  170. 0,
  171. 0,
  172. this._options.width,
  173. this._options.height
  174. );
  175. const inputMemoryOffset = this._model._getInputMemoryOffset() / 4;
  176. for (let i = 0; i < this._segmentationPixelCount; i++) {
  177. this._model.HEAPF32[inputMemoryOffset + (i * 3)] = Number(imageData?.data[i * 4]) / 255;
  178. this._model.HEAPF32[inputMemoryOffset + (i * 3) + 1] = Number(imageData?.data[(i * 4) + 1]) / 255;
  179. this._model.HEAPF32[inputMemoryOffset + (i * 3) + 2] = Number(imageData?.data[(i * 4) + 2]) / 255;
  180. }
  181. }
  182. /**
  183. * Checks if the local track supports this effect.
  184. *
  185. * @param {JitsiLocalTrack} jitsiLocalTrack - Track to apply effect.
  186. * @returns {boolean} - Returns true if this effect can run on the specified track
  187. * false otherwise.
  188. */
  189. isEnabled(jitsiLocalTrack: any) {
  190. return jitsiLocalTrack.isVideoTrack() && jitsiLocalTrack.videoType === 'camera';
  191. }
  192. /**
  193. * Starts loop to capture video frame and render the segmentation mask.
  194. *
  195. * @param {MediaStream} stream - Stream to be used for processing.
  196. * @returns {MediaStream} - The stream with the applied effect.
  197. */
  198. startEffect(stream: MediaStream) {
  199. this._stream = stream;
  200. this._maskFrameTimerWorker = new Worker(timerWorkerScript, { name: 'Blur effect worker' });
  201. this._maskFrameTimerWorker.onmessage = this._onMaskFrameTimer;
  202. const firstVideoTrack = this._stream.getVideoTracks()[0];
  203. const { height, frameRate, width }
  204. = firstVideoTrack.getSettings ? firstVideoTrack.getSettings() : firstVideoTrack.getConstraints();
  205. this._segmentationMask = new ImageData(this._options.width, this._options.height);
  206. this._segmentationMaskCanvas = document.createElement('canvas');
  207. this._segmentationMaskCanvas.width = this._options.width;
  208. this._segmentationMaskCanvas.height = this._options.height;
  209. this._segmentationMaskCtx = this._segmentationMaskCanvas.getContext('2d');
  210. this._outputCanvasElement.width = parseInt(width, 10);
  211. this._outputCanvasElement.height = parseInt(height, 10);
  212. this._outputCanvasCtx = this._outputCanvasElement.getContext('2d');
  213. this._inputVideoElement.width = parseInt(width, 10);
  214. this._inputVideoElement.height = parseInt(height, 10);
  215. this._inputVideoElement.autoplay = true;
  216. this._inputVideoElement.srcObject = this._stream;
  217. this._inputVideoElement.onloadeddata = () => {
  218. this._maskFrameTimerWorker.postMessage({
  219. id: SET_TIMEOUT,
  220. timeMs: 1000 / 30
  221. });
  222. };
  223. return this._outputCanvasElement.captureStream(parseInt(frameRate, 10));
  224. }
  225. /**
  226. * Stops the capture and render loop.
  227. *
  228. * @returns {void}
  229. */
  230. stopEffect() {
  231. this._maskFrameTimerWorker.postMessage({
  232. id: CLEAR_TIMEOUT
  233. });
  234. this._maskFrameTimerWorker.terminate();
  235. }
  236. }