@remotion/webcodecs 4.0.229 → 4.0.231

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +12 -1
  2. package/dist/arraybuffer-to-uint8-array.d.ts +1 -0
  3. package/dist/arraybuffer-to-uint8-array.js +7 -0
  4. package/dist/audio-decoder.d.ts +2 -2
  5. package/dist/audio-encoder-config.js +15 -2
  6. package/dist/audio-encoder.d.ts +2 -1
  7. package/dist/audio-encoder.js +17 -5
  8. package/dist/browser-quirks.d.ts +2 -0
  9. package/dist/browser-quirks.js +11 -0
  10. package/dist/can-copy-audio-track.d.ts +2 -4
  11. package/dist/can-copy-audio-track.js +7 -4
  12. package/dist/can-copy-video-track.d.ts +2 -4
  13. package/dist/can-copy-video-track.js +6 -6
  14. package/dist/can-reencode-audio-track.js +1 -6
  15. package/dist/can-reencode-video-track.js +1 -0
  16. package/dist/choose-correct-avc1-profile.d.ts +5 -0
  17. package/dist/choose-correct-avc1-profile.js +54 -0
  18. package/dist/codec-id.d.ts +7 -4
  19. package/dist/codec-id.js +28 -5
  20. package/dist/convert-encoded-chunk.d.ts +2 -1
  21. package/dist/convert-encoded-chunk.js +25 -2
  22. package/dist/convert-media.d.ts +13 -12
  23. package/dist/convert-media.js +62 -46
  24. package/dist/convert-to-correct-videoframe.d.ts +9 -0
  25. package/dist/convert-to-correct-videoframe.js +28 -0
  26. package/dist/default-on-audio-track-handler.d.ts +2 -0
  27. package/dist/default-on-audio-track-handler.js +36 -0
  28. package/dist/default-on-video-track-handler.d.ts +2 -0
  29. package/dist/default-on-video-track-handler.js +29 -0
  30. package/dist/esm/index.mjs +493 -174
  31. package/dist/generate-output-filename.d.ts +2 -0
  32. package/dist/generate-output-filename.js +14 -0
  33. package/dist/get-default-audio-codec.d.ts +4 -0
  34. package/dist/get-default-audio-codec.js +13 -0
  35. package/dist/get-default-video-codec.d.ts +4 -0
  36. package/dist/get-default-video-codec.js +10 -0
  37. package/dist/index.d.ts +12 -8
  38. package/dist/index.js +12 -1
  39. package/dist/io-manager/io-synchronizer.js +2 -2
  40. package/dist/{resolve-audio-action.d.ts → on-audio-track-handler.d.ts} +5 -5
  41. package/dist/on-audio-track-handler.js +2 -0
  42. package/dist/on-audio-track.d.ts +7 -8
  43. package/dist/on-audio-track.js +55 -16
  44. package/dist/on-frame.d.ts +4 -4
  45. package/dist/on-frame.js +15 -9
  46. package/dist/{resolve-video-action.d.ts → on-video-track-handler.d.ts} +5 -5
  47. package/dist/on-video-track-handler.js +2 -0
  48. package/dist/on-video-track.d.ts +8 -8
  49. package/dist/on-video-track.js +49 -15
  50. package/dist/set-remotion-imported.d.ts +6 -0
  51. package/dist/set-remotion-imported.js +25 -0
  52. package/dist/throttled-state-update.d.ts +13 -0
  53. package/dist/throttled-state-update.js +49 -0
  54. package/dist/video-decoder.d.ts +2 -2
  55. package/dist/video-decoder.js +5 -0
  56. package/dist/video-encoder-config.d.ts +2 -1
  57. package/dist/video-encoder-config.js +9 -2
  58. package/dist/video-encoder.d.ts +4 -2
  59. package/dist/video-encoder.js +12 -10
  60. package/package.json +4 -3
  61. package/dist/can-reencode-audio.d.ts +0 -7
  62. package/dist/can-reencode-audio.js +0 -21
  63. package/dist/can-reencode-video.d.ts +0 -6
  64. package/dist/can-reencode-video.js +0 -15
  65. package/dist/event-emitter.d.ts +0 -25
  66. package/dist/event-emitter.js +0 -23
  67. package/dist/polyfill-encoded-audio-chunk.d.ts +0 -3
  68. package/dist/polyfill-encoded-audio-chunk.js +0 -5
  69. package/dist/resolve-audio-action.js +0 -32
  70. package/dist/resolve-video-action.js +0 -26
  71. package/dist/wait-until-return.d.ts +0 -4
  72. package/dist/wait-until-return.js +0 -14
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.throttledStateUpdate = void 0;
4
+ const throttledStateUpdate = ({ updateFn, everyMilliseconds, signal, }) => {
5
+ let currentState = {
6
+ decodedAudioFrames: 0,
7
+ decodedVideoFrames: 0,
8
+ encodedVideoFrames: 0,
9
+ encodedAudioFrames: 0,
10
+ bytesWritten: 0,
11
+ millisecondsWritten: 0,
12
+ expectedOutputDurationInMs: null,
13
+ overallProgress: 0,
14
+ };
15
+ if (!updateFn) {
16
+ return {
17
+ get: () => currentState,
18
+ update: null,
19
+ stopAndGetLastProgress: () => { },
20
+ };
21
+ }
22
+ let lastUpdated = null;
23
+ const callUpdateIfChanged = () => {
24
+ if (currentState === lastUpdated) {
25
+ return;
26
+ }
27
+ updateFn(currentState);
28
+ lastUpdated = currentState;
29
+ };
30
+ const interval = setInterval(() => {
31
+ callUpdateIfChanged();
32
+ }, everyMilliseconds);
33
+ const onAbort = () => {
34
+ clearInterval(interval);
35
+ };
36
+ signal.addEventListener('abort', onAbort, { once: true });
37
+ return {
38
+ get: () => currentState,
39
+ update: (fn) => {
40
+ currentState = fn(currentState);
41
+ },
42
+ stopAndGetLastProgress: () => {
43
+ clearInterval(interval);
44
+ signal.removeEventListener('abort', onAbort);
45
+ return currentState;
46
+ },
47
+ };
48
+ };
49
+ exports.throttledStateUpdate = throttledStateUpdate;
@@ -1,6 +1,6 @@
1
- import type { LogLevel, VideoSample } from '@remotion/media-parser';
1
+ import type { AudioOrVideoSample, LogLevel } from '@remotion/media-parser';
2
2
  export type WebCodecsVideoDecoder = {
3
- processSample: (videoSample: VideoSample) => Promise<void>;
3
+ processSample: (videoSample: AudioOrVideoSample) => Promise<void>;
4
4
  waitForFinish: () => Promise<void>;
5
5
  close: () => void;
6
6
  flush: () => Promise<void>;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createVideoDecoder = void 0;
4
4
  const io_synchronizer_1 = require("./io-manager/io-synchronizer");
5
+ const log_1 = require("./log");
5
6
  const createVideoDecoder = ({ onFrame, onError, signal, config, logLevel, }) => {
6
7
  const ioSynchronizer = (0, io_synchronizer_1.makeIoSynchronizer)(logLevel, 'Video decoder');
7
8
  let outputQueue = Promise.resolve();
@@ -69,9 +70,13 @@ const createVideoDecoder = ({ onFrame, onError, signal, config, logLevel, }) =>
69
70
  },
70
71
  waitForFinish: async () => {
71
72
  await videoDecoder.flush();
73
+ log_1.Log.verbose(logLevel, 'Flushed video decoder');
72
74
  await ioSynchronizer.waitForFinish();
75
+ log_1.Log.verbose(logLevel, 'IO synchro finished');
73
76
  await outputQueue;
77
+ log_1.Log.verbose(logLevel, 'Output queue finished');
74
78
  await inputQueue;
79
+ log_1.Log.verbose(logLevel, 'Input queue finished');
75
80
  },
76
81
  close,
77
82
  flush: async () => {
@@ -1,6 +1,7 @@
1
1
  import type { ConvertMediaVideoCodec } from './codec-id';
2
- export declare const getVideoEncoderConfig: ({ width, height, codec, }: {
2
+ export declare const getVideoEncoderConfig: ({ codec, height, width, fps, }: {
3
3
  width: number;
4
4
  height: number;
5
5
  codec: ConvertMediaVideoCodec;
6
+ fps: number | null;
6
7
  }) => Promise<VideoEncoderConfig | null>;
@@ -1,14 +1,21 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getVideoEncoderConfig = void 0;
4
- const getVideoEncoderConfig = async ({ width, height, codec, }) => {
4
+ const browser_quirks_1 = require("./browser-quirks");
5
+ const choose_correct_avc1_profile_1 = require("./choose-correct-avc1-profile");
6
+ const getVideoEncoderConfig = async ({ codec, height, width, fps, }) => {
5
7
  if (typeof VideoEncoder === 'undefined') {
6
8
  return null;
7
9
  }
8
10
  const config = {
9
- codec: codec === 'vp9' ? 'vp09.00.10.08' : codec,
11
+ codec: codec === 'h264'
12
+ ? (0, choose_correct_avc1_profile_1.chooseCorrectAvc1Profile)({ fps, height, width })
13
+ : codec === 'vp9'
14
+ ? 'vp09.00.10.08'
15
+ : codec,
10
16
  height,
11
17
  width,
18
+ bitrate: (0, browser_quirks_1.isSafari)() ? 3000000 : undefined,
12
19
  };
13
20
  const hardware = {
14
21
  ...config,
@@ -1,14 +1,16 @@
1
1
  import type { LogLevel } from '@remotion/media-parser';
2
+ import type { ConvertMediaVideoCodec } from './codec-id';
2
3
  export type WebCodecsVideoEncoder = {
3
4
  encodeFrame: (videoFrame: VideoFrame, timestamp: number) => Promise<void>;
4
5
  waitForFinish: () => Promise<void>;
5
6
  close: () => void;
6
7
  flush: () => Promise<void>;
7
8
  };
8
- export declare const createVideoEncoder: ({ onChunk, onError, signal, config, logLevel, }: {
9
- onChunk: (chunk: EncodedVideoChunk) => Promise<void>;
9
+ export declare const createVideoEncoder: ({ onChunk, onError, signal, config, logLevel, outputCodec, }: {
10
+ onChunk: (chunk: EncodedVideoChunk, metadata: EncodedVideoChunkMetadata | null) => Promise<void>;
10
11
  onError: (error: DOMException) => void;
11
12
  signal: AbortSignal;
12
13
  config: VideoEncoderConfig;
13
14
  logLevel: LogLevel;
15
+ outputCodec: ConvertMediaVideoCodec;
14
16
  }) => WebCodecsVideoEncoder;
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createVideoEncoder = void 0;
4
+ const convert_to_correct_videoframe_1 = require("./convert-to-correct-videoframe");
4
5
  const io_synchronizer_1 = require("./io-manager/io-synchronizer");
5
- const createVideoEncoder = ({ onChunk, onError, signal, config, logLevel, }) => {
6
+ const log_1 = require("./log");
7
+ const createVideoEncoder = ({ onChunk, onError, signal, config, logLevel, outputCodec, }) => {
6
8
  if (signal.aborted) {
7
9
  throw new Error('Not creating video encoder, already aborted');
8
10
  }
@@ -12,18 +14,16 @@ const createVideoEncoder = ({ onChunk, onError, signal, config, logLevel, }) =>
12
14
  error(error) {
13
15
  onError(error);
14
16
  },
15
- output(chunk) {
16
- if (chunk.duration === null) {
17
- throw new Error('Duration is null');
18
- }
19
- const timestamp = chunk.timestamp + chunk.duration;
17
+ output(chunk, metadata) {
18
+ var _a;
19
+ const timestamp = chunk.timestamp + ((_a = chunk.duration) !== null && _a !== void 0 ? _a : 0);
20
20
  ioSynchronizer.onOutput(timestamp);
21
21
  outputQueue = outputQueue
22
22
  .then(() => {
23
23
  if (signal.aborted) {
24
24
  return;
25
25
  }
26
- return onChunk(chunk);
26
+ return onChunk(chunk, metadata !== null && metadata !== void 0 ? metadata : null);
27
27
  })
28
28
  .then(() => {
29
29
  ioSynchronizer.onProcessed();
@@ -46,6 +46,7 @@ const createVideoEncoder = ({ onChunk, onError, signal, config, logLevel, }) =>
46
46
  close();
47
47
  };
48
48
  signal.addEventListener('abort', onAbort);
49
+ log_1.Log.verbose(logLevel, 'Configuring video encoder', config);
49
50
  encoder.configure(config);
50
51
  let framesProcessed = 0;
51
52
  const encodeFrame = async (frame) => {
@@ -53,15 +54,16 @@ const createVideoEncoder = ({ onChunk, onError, signal, config, logLevel, }) =>
53
54
  return;
54
55
  }
55
56
  await ioSynchronizer.waitFor({
56
- unemitted: 2,
57
- _unprocessed: 2,
57
+ // Firefox stalls if too few frames are passed
58
+ unemitted: 10,
59
+ _unprocessed: 10,
58
60
  });
59
61
  // @ts-expect-error - can have changed in the meanwhile
60
62
  if (encoder.state === 'closed') {
61
63
  return;
62
64
  }
63
65
  const keyFrame = framesProcessed % 40 === 0;
64
- encoder.encode(frame, {
66
+ encoder.encode((0, convert_to_correct_videoframe_1.convertToCorrectVideoFrame)({ videoFrame: frame, outputCodec }), {
65
67
  keyFrame,
66
68
  });
67
69
  ioSynchronizer.inputItem(frame.timestamp, keyFrame);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotion/webcodecs",
3
- "version": "4.0.229",
3
+ "version": "4.0.231",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/esm/index.mjs",
@@ -17,19 +17,20 @@
17
17
  "author": "Jonny Burger <jonny@remotion.dev>",
18
18
  "license": "Remotion License (See https://remotion.dev/docs/webcodecs#license)",
19
19
  "dependencies": {
20
- "@remotion/media-parser": "4.0.229"
20
+ "@remotion/media-parser": "4.0.231"
21
21
  },
22
22
  "peerDependencies": {},
23
23
  "devDependencies": {
24
24
  "@types/dom-webcodecs": "0.1.11",
25
25
  "eslint": "9.14.0",
26
- "@remotion/eslint-config-internal": "4.0.229"
26
+ "@remotion/eslint-config-internal": "4.0.231"
27
27
  },
28
28
  "keywords": [],
29
29
  "publishConfig": {
30
30
  "access": "public"
31
31
  },
32
32
  "description": "Media conversion in the browser",
33
+ "homepage": "https://remotion.dev/webcodecs",
33
34
  "scripts": {
34
35
  "formatting": "prettier src --check",
35
36
  "lint": "eslint src",
@@ -1,7 +0,0 @@
1
- import type { AudioTrack } from '@remotion/media-parser';
2
- import type { ConvertMediaAudioCodec } from './codec-id';
3
- export declare const canReencodeAudioTrack: ({ track, audioCodec, bitrate, }: {
4
- track: AudioTrack;
5
- audioCodec: ConvertMediaAudioCodec;
6
- bitrate: number;
7
- }) => Promise<boolean>;
@@ -1,21 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.canReencodeAudioTrack = void 0;
4
- const audio_decoder_config_1 = require("./audio-decoder-config");
5
- const audio_encoder_config_1 = require("./audio-encoder-config");
6
- const canReencodeAudioTrack = async ({ track, audioCodec, bitrate, }) => {
7
- const audioDecoderConfig = await (0, audio_decoder_config_1.getAudioDecoderConfig)({
8
- codec: track.codec,
9
- numberOfChannels: track.numberOfChannels,
10
- sampleRate: track.sampleRate,
11
- description: track.description,
12
- });
13
- const audioEncoderConfig = await (0, audio_encoder_config_1.getAudioEncoderConfig)({
14
- codec: audioCodec,
15
- numberOfChannels: track.numberOfChannels,
16
- sampleRate: track.sampleRate,
17
- bitrate,
18
- });
19
- return Boolean(audioDecoderConfig && audioEncoderConfig);
20
- };
21
- exports.canReencodeAudioTrack = canReencodeAudioTrack;
@@ -1,6 +0,0 @@
1
- import type { VideoTrack } from '@remotion/media-parser';
2
- import type { ConvertMediaVideoCodec } from './codec-id';
3
- export declare const canReencodeVideoTrack: ({ videoCodec, track, }: {
4
- videoCodec: ConvertMediaVideoCodec;
5
- track: VideoTrack;
6
- }) => Promise<boolean>;
@@ -1,15 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.canReencodeVideoTrack = void 0;
4
- const video_decoder_config_1 = require("./video-decoder-config");
5
- const video_encoder_config_1 = require("./video-encoder-config");
6
- const canReencodeVideoTrack = async ({ videoCodec, track, }) => {
7
- const videoEncoderConfig = await (0, video_encoder_config_1.getVideoEncoderConfig)({
8
- codec: videoCodec,
9
- height: track.displayAspectHeight,
10
- width: track.displayAspectWidth,
11
- });
12
- const videoDecoderConfig = await (0, video_decoder_config_1.getVideoDecoderConfigWithHardwareAcceleration)(track);
13
- return Boolean(videoDecoderConfig && videoEncoderConfig);
14
- };
15
- exports.canReencodeVideoTrack = canReencodeVideoTrack;
@@ -1,25 +0,0 @@
1
- type Input = {
2
- timestamp: number;
3
- keyframe: boolean;
4
- };
5
- type Output = {
6
- timestamp: number;
7
- };
8
- type IoEventMap = {
9
- input: Input;
10
- output: Output;
11
- };
12
- export type IoEventTypes = keyof IoEventMap;
13
- export type CallbackListener<T extends IoEventTypes> = (data: {
14
- detail: IoEventMap[T];
15
- }) => void;
16
- type IoListeners = {
17
- [EventType in IoEventTypes]: CallbackListener<EventType>[];
18
- };
19
- export declare class IoEventEmitter {
20
- listeners: IoListeners;
21
- addEventListener<Q extends IoEventTypes>(name: Q, callback: CallbackListener<Q>): void;
22
- removeEventListener<Q extends IoEventTypes>(name: Q, callback: CallbackListener<Q>): void;
23
- dispatchEvent<T extends IoEventTypes>(dispatchName: T, context: IoEventMap[T]): void;
24
- }
25
- export {};
@@ -1,23 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.IoEventEmitter = void 0;
4
- class IoEventEmitter {
5
- constructor() {
6
- this.listeners = {
7
- input: [],
8
- output: [],
9
- };
10
- }
11
- addEventListener(name, callback) {
12
- this.listeners[name].push(callback);
13
- }
14
- removeEventListener(name, callback) {
15
- this.listeners[name] = this.listeners[name].filter((l) => l !== callback);
16
- }
17
- dispatchEvent(dispatchName, context) {
18
- this.listeners[dispatchName].forEach((callback) => {
19
- callback({ detail: context });
20
- });
21
- }
22
- }
23
- exports.IoEventEmitter = IoEventEmitter;
@@ -1,3 +0,0 @@
1
- import type { AudioSample } from '@remotion/media-parser';
2
- import type { AudioOrVideoSample } from '@remotion/media-parser/src/create/cluster';
3
- export declare const polyfillAudioChunk: (audioSample: AudioSample) => AudioOrVideoSample;
@@ -1,5 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.polyfillAudioChunk = void 0;
4
- const polyfillAudioChunk = (audioSample) => { };
5
- exports.polyfillAudioChunk = polyfillAudioChunk;
@@ -1,32 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.defaultResolveAudioAction = void 0;
4
- const can_copy_audio_track_1 = require("./can-copy-audio-track");
5
- const can_reencode_audio_track_1 = require("./can-reencode-audio-track");
6
- const log_1 = require("./log");
7
- const DEFAULT_BITRATE = 128000;
8
- const defaultResolveAudioAction = async ({ track, audioCodec, logLevel, container, }) => {
9
- const bitrate = DEFAULT_BITRATE;
10
- const canCopy = (0, can_copy_audio_track_1.canCopyAudioTrack)({
11
- inputCodec: track.codecWithoutConfig,
12
- outputCodec: audioCodec,
13
- container,
14
- });
15
- if (canCopy) {
16
- log_1.Log.verbose(logLevel, `Track ${track.trackId} (audio): Can copy = ${canCopy}, action = copy`);
17
- return Promise.resolve({ type: 'copy' });
18
- }
19
- const canReencode = await (0, can_reencode_audio_track_1.canReencodeAudioTrack)({
20
- audioCodec,
21
- track,
22
- bitrate,
23
- });
24
- if (canReencode) {
25
- log_1.Log.verbose(logLevel, `Track ${track.trackId} (audio): Can re-encode = ${canReencode}, can copy = ${canCopy}, action = reencode`);
26
- return Promise.resolve({ type: 'reencode', bitrate, audioCodec });
27
- }
28
- log_1.Log.verbose(logLevel, `Track ${track.trackId} (audio): Can re-encode = ${canReencode}, can copy = ${canCopy}, action = drop`);
29
- // TODO: Make a fail option?
30
- return Promise.resolve({ type: 'drop' });
31
- };
32
- exports.defaultResolveAudioAction = defaultResolveAudioAction;
@@ -1,26 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.defaultResolveVideoAction = void 0;
4
- const can_copy_video_track_1 = require("./can-copy-video-track");
5
- const can_reencode_video_track_1 = require("./can-reencode-video-track");
6
- const log_1 = require("./log");
7
- const defaultResolveVideoAction = async ({ track, videoCodec, logLevel, container, }) => {
8
- const canCopy = (0, can_copy_video_track_1.canCopyVideoTrack)({
9
- inputCodec: track.codecWithoutConfig,
10
- outputCodec: videoCodec,
11
- container,
12
- });
13
- if (canCopy) {
14
- log_1.Log.verbose(logLevel, `Track ${track.trackId} (video): Can copy, therefore copying`);
15
- return Promise.resolve({ type: 'copy' });
16
- }
17
- const canReencode = await (0, can_reencode_video_track_1.canReencodeVideoTrack)({ videoCodec, track });
18
- if (canReencode) {
19
- log_1.Log.verbose(logLevel, `Track ${track.trackId} (video): Cannot copy, but re-enconde, therefore re-encoding`);
20
- return Promise.resolve({ type: 'reencode', videoCodec });
21
- }
22
- log_1.Log.verbose(logLevel, `Track ${track.trackId} (video): Can neither copy nor re-encode, therefore dropping`);
23
- // TODO: Make a fail option?
24
- return Promise.resolve({ type: 'drop' });
25
- };
26
- exports.defaultResolveVideoAction = defaultResolveVideoAction;
@@ -1,4 +0,0 @@
1
- export declare const waitUntilReturn: () => {
2
- waitForReturn: () => any;
3
- isReturning: () => void;
4
- };
@@ -1,14 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.waitUntilReturn = void 0;
4
- const with_resolvers_1 = require("./with-resolvers");
5
- const waitUntilReturn = () => {
6
- const { promise, resolve } = (0, with_resolvers_1.withResolvers)();
7
- return {
8
- waitForReturn: () => promise,
9
- isReturning: () => {
10
- resolve(undefined);
11
- },
12
- };
13
- };
14
- exports.waitUntilReturn = waitUntilReturn;