encoder.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. /**
  2. * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import * as HME from 'h264-mp4-encoder';
  7. import { Viewport } from '../../mol-canvas3d/camera/util';
  8. import { ImagePass } from '../../mol-canvas3d/passes/image';
  9. import { PluginStateAnimation } from '../../mol-plugin-state/animation/model';
  10. import { PluginContext } from '../../mol-plugin/context';
  11. import { RuntimeContext } from '../../mol-task';
  12. import { Color } from '../../mol-util/color';
  13. export interface Mp4EncoderParams<A extends PluginStateAnimation = PluginStateAnimation> {
  14. pass: ImagePass,
  15. customBackground?: Color,
  16. animation: PluginStateAnimation.Instance<A>,
  17. width: number,
  18. height: number,
  19. viewport: Viewport,
  20. /** default is 30 */
  21. fps?: number,
  22. /** Number from 10 (best quality, slowest) to 51 (worst, fastest) */
  23. quantizationParameter?: number
  24. }
  25. export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin: PluginContext, ctx: RuntimeContext, params: Mp4EncoderParams<A>) {
  26. await ctx.update({ message: 'Initializing...', isIndeterminate: true });
  27. validateViewport(params);
  28. const durationMs = PluginStateAnimation.getDuration(plugin, params.animation);
  29. if (durationMs === void 0) {
  30. throw new Error('The animation does not have the duration specified.');
  31. }
  32. const encoder = await HME.createH264MP4Encoder();
  33. const { width, height } = params;
  34. let vw = params.viewport.width, vh = params.viewport.height;
  35. // dimensions must be a multiple of 2
  36. if (vw % 2 !== 0) vw -= 1;
  37. if (vh % 2 !== 0) vh -= 1;
  38. const normalizedViewport: Viewport = { ...params.viewport, width: vw, height: vh };
  39. encoder.width = vw;
  40. encoder.height = vh;
  41. if (params.quantizationParameter) encoder.quantizationParameter = params.quantizationParameter;
  42. if (params.fps) encoder.frameRate = params.fps;
  43. encoder.initialize();
  44. const loop = plugin.animationLoop;
  45. const originalBackground = params.customBackground ? plugin.canvas3d?.props.renderer.backgroundColor : void 0;
  46. let stoppedAnimation = true, finalized = false;
  47. try {
  48. loop.stop();
  49. loop.resetTime(0);
  50. plugin.canvas3d?.setProps({ renderer: { backgroundColor: params.customBackground } }, true);
  51. const fps = encoder.frameRate;
  52. const N = Math.ceil(durationMs / 1000 * fps);
  53. const dt = durationMs / N;
  54. await ctx.update({ message: 'Rendering...', isIndeterminate: false, current: 0, max: N + 1 });
  55. await plugin.managers.animation.play(params.animation.definition, params.animation.params);
  56. stoppedAnimation = false;
  57. for (let i = 0; i <= N; i++) {
  58. await loop.tick(i * dt, { isSynchronous: true, manualDraw: true });
  59. const image = params.pass.getImageData(width, height, normalizedViewport);
  60. encoder.addFrameRgba(image.data);
  61. if (ctx.shouldUpdate) {
  62. await ctx.update({ current: i + 1 });
  63. }
  64. }
  65. await ctx.update({ message: 'Applying finishing touches...', isIndeterminate: true });
  66. await plugin.managers.animation.stop();
  67. stoppedAnimation = true;
  68. encoder.finalize();
  69. finalized = true;
  70. return encoder.FS.readFile(encoder.outputFilename);
  71. } finally {
  72. if (finalized) encoder.delete();
  73. if (originalBackground) {
  74. plugin.canvas3d?.setProps({ renderer: { backgroundColor: originalBackground } }, true);
  75. }
  76. if (!stoppedAnimation) await plugin.managers.animation.stop();
  77. loop.start();
  78. }
  79. }
  80. function validateViewport(params: Mp4EncoderParams) {
  81. if (!params.viewport) return;
  82. if (params.viewport.x + params.viewport.width > params.width || params.viewport.x + params.viewport.width >= params.width) {
  83. throw new Error('Viewport exceeds the canvas dimensions.');
  84. }
  85. }