video_processor.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. /**
  2. * 基于WebGL的视频裁切处理器
  3. * @param {Array} configs 配置数组,每个元素为一个配置对象,包含以下属性:
  4. * - type: 裁切类型,默认值为"none"
  5. * - x: 裁切区域的起始横坐标百分比(左上角),范围介于0到1之间,默认值为0
  6. * - y: 裁切区域的起始纵坐标百分比(左上角),范围介于0到1之间,默认值为0
  7. * - w: 裁切区域的宽度百分比,范围介于0到1之间,默认值为1
  8. * - h: 裁切区域的高度百分比,范围介于0到1之间,默认值为1
  9. *
  10. * 使用方式:
  11. * 1. 创建一个VideoProcessor实例,传入适当的配置数组。
  12. * 2. 调用initializeById方法,传入源canvas的ID和目标canvas ID数组。
  13. * 这会初始化源和目标canvas,并将源canvas内容处理后绘制到目标canvas。
  14. * 3. 在需要更新帧时(例如,源视频播放新的一帧时),调用drawFrame方法。
  15. * 这会将当前源canvas中的帧复制并按照给定配置处理,然后绘制到目标canvas。
  16. *
  17. * 示例代码:
  18. * ```js
  19. * const configs = [
  20. * { type: 'none', x: 0, y: 0, w: 1, h: 1 },
  21. * { type: "scaleOnly", x: 2/3, y: 0.0, w: 1/3, h: 4/9 },
  22. * { type: "threePart", x: 0.0, y: 22/27, w: 1, h: 4/27 },
  23. * ];
  24. * const videoProcessor = new VideoProcessor(configs);
  25. * videoProcessor.initializeById("video1", ["camera1", "camera2", "camera3"]);
  26. *
  27. * // 每当源视频播放新的一帧时执行以下操作
  28. * videoProcessor.drawFrame();
  29. * ```
  30. */
  31. class VideoProcessor {
  32. constructor(configs = []) {
  33. this.configs = configs;
  34. this.vertexShaderSource = this._createVertexShaderSource();
  35. }
  36. initialize(sourceCanvas, targetCanvasArray) {
  37. this.canvasSource = sourceCanvas;
  38. this.canvasTargets = targetCanvasArray;
  39. this.glSource = this.canvasSource.getContext('webgl');
  40. // 初始化帧缓冲区和纹理数组
  41. this.frameBuffers = [];
  42. this.textures = [];
  43. this.programs = [];
  44. this.canvasTargets.forEach((targetCanvas, index) => {
  45. targetCanvas.width = this.canvasSource.width;
  46. targetCanvas.height = this.canvasSource.height;
  47. const config = this.configs[index] || {
  48. type: 'none',
  49. x: 0,
  50. y: 0,
  51. w: 1,
  52. h: 1,
  53. };
  54. this.fragmentShaderSources = ['' * this.configs.length];
  55. this.fragmentShaderSources[index] =
  56. this._createFragmentShaderSource(config);
  57. const glTarget = this.canvasTargets[index].getContext('webgl');
  58. // 创建并初始化帧缓冲区和纹理
  59. this._initWebGLCanvas(glTarget, index);
  60. });
  61. }
  62. initializeById(sourceCanvasId, targetCanvasIds) {
  63. const sourceCanvas = document.getElementById(sourceCanvasId);
  64. const targetCanvasArray = targetCanvasIds.map((id) =>
  65. document.getElementById(id),
  66. );
  67. this.initialize(sourceCanvas, targetCanvasArray);
  68. }
  69. drawFrame() {
  70. this.canvasTargets.forEach((_, index) => {
  71. const glTarget = this.canvasTargets[index].getContext('webgl');
  72. this._copyOnVideoFrame(glTarget, index);
  73. });
  74. }
  75. _initWebGLCanvas(glTarget, index) {
  76. console.log(`开始初始化 webgl-${index}`);
  77. // 创建纹理
  78. const texture = glTarget.createTexture();
  79. this.textures.push(texture);
  80. glTarget.bindTexture(glTarget.TEXTURE_2D, this.textures[index]);
  81. glTarget.texParameteri(
  82. glTarget.TEXTURE_2D,
  83. glTarget.TEXTURE_MIN_FILTER,
  84. glTarget.LINEAR,
  85. );
  86. glTarget.texParameteri(
  87. glTarget.TEXTURE_2D,
  88. glTarget.TEXTURE_WRAP_S,
  89. glTarget.CLAMP_TO_EDGE,
  90. );
  91. glTarget.texParameteri(
  92. glTarget.TEXTURE_2D,
  93. glTarget.TEXTURE_WRAP_T,
  94. glTarget.CLAMP_TO_EDGE,
  95. );
  96. glTarget.texImage2D(
  97. glTarget.TEXTURE_2D,
  98. 0,
  99. glTarget.RGBA,
  100. this.canvasSource.width,
  101. this.canvasSource.height,
  102. 0,
  103. glTarget.RGBA,
  104. glTarget.UNSIGNED_BYTE,
  105. null,
  106. );
  107. // 创建一个帧缓冲区
  108. const frameBuffer = glTarget.createFramebuffer();
  109. this.frameBuffers.push(frameBuffer);
  110. glTarget.bindFramebuffer(glTarget.FRAMEBUFFER, this.frameBuffers[index]);
  111. glTarget.framebufferTexture2D(
  112. glTarget.FRAMEBUFFER,
  113. glTarget.COLOR_ATTACHMENT0,
  114. glTarget.TEXTURE_2D,
  115. this.textures[index],
  116. 0,
  117. );
  118. // 检查状态
  119. var status = glTarget.checkFramebufferStatus(glTarget.FRAMEBUFFER);
  120. if (status == glTarget.FRAMEBUFFER_COMPLETE) {
  121. console.log('Framebuffer is complete.');
  122. } else {
  123. console.error('Framebuffer is incomplete:', status);
  124. }
  125. // 创建着色器程序
  126. var vertexShader = this._createShader(
  127. glTarget,
  128. glTarget.VERTEX_SHADER,
  129. this.vertexShaderSource,
  130. );
  131. // 使用对应的片段着色器源创建并链接不同的着色器程序
  132. var fragmentShader = this._createShader(
  133. glTarget,
  134. glTarget.FRAGMENT_SHADER,
  135. this.fragmentShaderSources[index],
  136. );
  137. const program = glTarget.createProgram();
  138. this.programs.push(program);
  139. glTarget.attachShader(this.programs[index], vertexShader);
  140. glTarget.attachShader(this.programs[index], fragmentShader);
  141. glTarget.linkProgram(this.programs[index]);
  142. glTarget.useProgram(this.programs[index]);
  143. var success = glTarget.getProgramParameter(
  144. this.programs[index],
  145. glTarget.LINK_STATUS,
  146. );
  147. if (success) {
  148. console.log('Program linked successfully.');
  149. } else {
  150. console.error(glTarget.getProgramInfoLog(this.programs[index]));
  151. glTarget.deleteProgram(this.programs[index]);
  152. }
  153. // 创建顶点缓冲区,并设置 WebGL 的属性与通道(Channels)
  154. var positionLocation = glTarget.getAttribLocation(
  155. this.programs[index],
  156. 'a_position',
  157. );
  158. var texCoordLocation = glTarget.getAttribLocation(
  159. this.programs[index],
  160. 'a_texCoord',
  161. );
  162. var positionBuffer = glTarget.createBuffer();
  163. glTarget.bindBuffer(glTarget.ARRAY_BUFFER, positionBuffer);
  164. // 顶点坐标信息
  165. var positions = [-1, -1, 1, -1, -1, 1, 1, 1];
  166. glTarget.bufferData(
  167. glTarget.ARRAY_BUFFER,
  168. new Float32Array(positions),
  169. glTarget.STATIC_DRAW,
  170. );
  171. glTarget.enableVertexAttribArray(positionLocation);
  172. glTarget.vertexAttribPointer(
  173. positionLocation,
  174. 2,
  175. glTarget.FLOAT,
  176. false,
  177. 0,
  178. 0,
  179. );
  180. var texCoordBuffer = glTarget.createBuffer();
  181. glTarget.bindBuffer(glTarget.ARRAY_BUFFER, texCoordBuffer);
  182. // 纹理坐标信息
  183. var texCoords = [0, 0, 1, 0, 0, 1, 1, 1];
  184. glTarget.bufferData(
  185. glTarget.ARRAY_BUFFER,
  186. new Float32Array(texCoords),
  187. glTarget.STATIC_DRAW,
  188. );
  189. glTarget.enableVertexAttribArray(texCoordLocation);
  190. glTarget.vertexAttribPointer(
  191. texCoordLocation,
  192. 2,
  193. glTarget.FLOAT,
  194. false,
  195. 0,
  196. 0,
  197. );
  198. console.log(`初始化 webgl-${index} 完成`);
  199. console.log('--------------------');
  200. }
  201. _copyOnVideoFrame(glTarget, index) {
  202. var errorCode = glTarget.getError();
  203. if (errorCode !== glTarget.NO_ERROR) {
  204. console.error('WebGL error:', errorCode);
  205. }
  206. // 根据索引绑定相应的帧缓冲区
  207. glTarget.bindFramebuffer(glTarget.FRAMEBUFFER, this.frameBuffers[index]);
  208. glTarget.viewport(0, 0, this.canvasSource.width, this.canvasSource.height);
  209. // 从源canvas复制当前帧到纹理中
  210. glTarget.texImage2D(
  211. glTarget.TEXTURE_2D,
  212. 0,
  213. glTarget.RGBA,
  214. glTarget.RGBA,
  215. glTarget.UNSIGNED_BYTE,
  216. this.canvasSource,
  217. );
  218. // 切换到目标canvas的帧缓冲区并绘制纹理
  219. glTarget.bindFramebuffer(glTarget.FRAMEBUFFER, null);
  220. glTarget.viewport(
  221. 0,
  222. 0,
  223. this.canvasTargets[index].width,
  224. this.canvasTargets[index].height,
  225. );
  226. this._drawTextureToTargetCanvas(glTarget, index); // 此处调用内部方法
  227. errorCode = glTarget.getError();
  228. if (errorCode !== glTarget.NO_ERROR) {
  229. console.error('WebGL error:', errorCode);
  230. }
  231. }
  232. _drawTextureToTargetCanvas(glTarget, index) {
  233. glTarget.clearColor(0, 0, 0, 1);
  234. glTarget.clear(glTarget.COLOR_BUFFER_BIT);
  235. glTarget.activeTexture(glTarget.TEXTURE0);
  236. glTarget.bindTexture(glTarget.TEXTURE_2D, this.textures[index]);
  237. glTarget.uniform1i(
  238. glTarget.getUniformLocation(this.programs[index], 'u_texture'),
  239. 0,
  240. );
  241. glTarget.drawArrays(glTarget.TRIANGLE_STRIP, 0, 4);
  242. }
  243. _createShader(glTarget, type, source) {
  244. var shader = glTarget.createShader(type);
  245. glTarget.shaderSource(shader, source);
  246. glTarget.compileShader(shader);
  247. var success = glTarget.getShaderParameter(shader, glTarget.COMPILE_STATUS);
  248. if (success) {
  249. return shader;
  250. }
  251. console.error(glTarget.getShaderInfoLog(shader));
  252. glTarget.deleteShader(shader);
  253. }
  254. _createVertexShaderSource() {
  255. return `
  256. attribute vec4 a_position;
  257. attribute vec2 a_texCoord;
  258. varying vec2 v_texCoord;
  259. void main() {
  260. gl_Position = a_position;
  261. v_texCoord = a_texCoord;
  262. }
  263. `;
  264. }
  265. /**
  266. * 裁图模式
  267. * [scaleOnly: 只缩放,不裁剪]
  268. * [threePart: 三等分切割]
  269. * [camera720: 两等分切割]
  270. */
  271. _createFragmentShaderSource(config) {
  272. const { type, x, y, w, h } = config;
  273. const width = this._formatShaderNumber(w);
  274. const height = this._formatShaderNumber(h);
  275. const offsetX = this._formatShaderNumber(x);
  276. const offsetY = this._formatShaderNumber(y);
  277. switch (type) {
  278. case 'scaleOnly':
  279. return `
  280. precision mediump float;
  281. varying vec2 v_texCoord;
  282. uniform sampler2D u_texture;
  283. void main() {
  284. vec2 scaledTexCoord = vec2(v_texCoord.x * ${width}, v_texCoord.y * ${height});
  285. scaledTexCoord.x += ${offsetX};
  286. scaledTexCoord.y += ${offsetY};
  287. gl_FragColor = texture2D(u_texture, scaledTexCoord);
  288. }
  289. `;
  290. case 'threePart':
  291. return `
  292. precision mediump float;
  293. varying vec2 v_texCoord;
  294. uniform sampler2D u_texture;
  295. void main() {
  296. if (v_texCoord.y < (1.0 / 3.0)) {
  297. vec2 newTexCoord = vec2(v_texCoord.x / 3.0, v_texCoord.y * 3.0 * ${height} + ${offsetY});
  298. gl_FragColor = texture2D(u_texture, newTexCoord);
  299. } else if (v_texCoord.y < (2.0 / 3.0)) {
  300. vec2 newTexCoord = vec2(v_texCoord.x / 3.0 + 1.0 / 3.0, v_texCoord.y * 3.0 * ${height} + ${offsetY} - ${height});
  301. gl_FragColor = texture2D(u_texture, newTexCoord);
  302. } else {
  303. vec2 newTexCoord = vec2(v_texCoord.x / 3.0 + 2.0 / 3.0, v_texCoord.y * 3.0 * ${height} + ${offsetY} - 2.0 * ${height});
  304. gl_FragColor = texture2D(u_texture, newTexCoord);
  305. }
  306. }
  307. `;
  308. case "camera720":
  309. // + 1.0 / 720.0 解决拼接黑线问题
  310. return `
  311. precision mediump float;
  312. varying vec2 v_texCoord;
  313. uniform sampler2D u_texture;
  314. void main() {
  315. if (v_texCoord.y < (1.0 / 2.0)) {
  316. vec2 newTexCoord = vec2(v_texCoord.x / 2.0, v_texCoord.y * 2.0 * ${height} + ${offsetY});
  317. gl_FragColor = texture2D(u_texture, newTexCoord);
  318. } else {
  319. vec2 newTexCoord = vec2(v_texCoord.x / 2.0 + 1.0 / 2.0, v_texCoord.y * 2.0 * ${height} + ${offsetY} - 1.0 * ${height} + 1.0 / 720.0 );
  320. gl_FragColor = texture2D(u_texture, newTexCoord);
  321. }
  322. }
  323. `;
  324. case "oldTwoPart":
  325. return `
  326. precision mediump float;
  327. varying vec2 v_texCoord;
  328. uniform sampler2D u_texture;
  329. void main() {
  330. vec2 newTexCoord = vec2(v_texCoord.x / 2.0, v_texCoord.y * ${height} + ${offsetY});
  331. gl_FragColor = texture2D(u_texture, newTexCoord);
  332. }
  333. `;
  334. case "oldThreePart":
  335. return `
  336. precision mediump float;
  337. varying vec2 v_texCoord;
  338. uniform sampler2D u_texture;
  339. void main() {
  340. vec2 newTexCoord = vec2(v_texCoord.x / 3.0, v_texCoord.y * ${height} + ${offsetY});
  341. gl_FragColor = texture2D(u_texture, newTexCoord);
  342. }
  343. `;
  344. default:
  345. return `
  346. precision mediump float;
  347. varying vec2 v_texCoord;
  348. uniform sampler2D u_texture;
  349. void main() {
  350. gl_FragColor = texture2D(u_texture, v_texCoord);
  351. }
  352. `;
  353. }
  354. }
  355. _formatShaderNumber(number) {
  356. const clampedNumber = Math.max(0, Math.min(1, number));
  357. return clampedNumber.toFixed(6);
  358. }
  359. }