@remotion/media 4.0.355 → 4.0.357
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.
- package/dist/audio/audio-for-preview.d.ts +30 -0
- package/dist/audio/audio-for-preview.js +213 -0
- package/dist/audio/audio-for-rendering.js +63 -12
- package/dist/audio/audio.js +8 -50
- package/dist/audio/props.d.ts +12 -3
- package/dist/audio-extraction/audio-cache.d.ts +1 -1
- package/dist/audio-extraction/audio-cache.js +5 -1
- package/dist/audio-extraction/audio-iterator.d.ts +7 -3
- package/dist/audio-extraction/audio-iterator.js +35 -12
- package/dist/audio-extraction/audio-manager.d.ts +10 -38
- package/dist/audio-extraction/audio-manager.js +40 -11
- package/dist/audio-extraction/extract-audio.d.ts +11 -3
- package/dist/audio-extraction/extract-audio.js +37 -17
- package/dist/caches.d.ts +11 -45
- package/dist/convert-audiodata/apply-tonefrequency.d.ts +2 -0
- package/dist/convert-audiodata/apply-tonefrequency.js +43 -0
- package/dist/convert-audiodata/combine-audiodata.js +2 -23
- package/dist/convert-audiodata/convert-audiodata.d.ts +1 -5
- package/dist/convert-audiodata/convert-audiodata.js +16 -24
- package/dist/convert-audiodata/wsola.d.ts +13 -0
- package/dist/convert-audiodata/wsola.js +197 -0
- package/dist/esm/index.mjs +2265 -589
- package/dist/extract-frame-and-audio.d.ts +7 -7
- package/dist/extract-frame-and-audio.js +69 -26
- package/dist/get-sink-weak.d.ts +3 -8
- package/dist/get-sink-weak.js +3 -11
- package/dist/get-sink.d.ts +13 -0
- package/dist/get-sink.js +15 -0
- package/dist/get-time-in-seconds.d.ts +10 -0
- package/dist/get-time-in-seconds.js +25 -0
- package/dist/index.d.ts +13 -3
- package/dist/index.js +12 -2
- package/dist/is-network-error.d.ts +6 -0
- package/dist/is-network-error.js +17 -0
- package/dist/render-timestamp-range.d.ts +1 -0
- package/dist/render-timestamp-range.js +9 -0
- package/dist/video/media-player.d.ts +91 -0
- package/dist/video/media-player.js +484 -0
- package/dist/video/props.d.ts +37 -18
- package/dist/video/resolve-playback-time.d.ts +8 -0
- package/dist/video/resolve-playback-time.js +22 -0
- package/dist/video/timeout-utils.d.ts +2 -0
- package/dist/video/timeout-utils.js +18 -0
- package/dist/video/video-for-preview.d.ts +25 -0
- package/dist/video/video-for-preview.js +241 -0
- package/dist/video/video-for-rendering.d.ts +26 -2
- package/dist/video/video-for-rendering.js +95 -19
- package/dist/video/video.js +13 -18
- package/dist/video-extraction/extract-frame-via-broadcast-channel.d.ts +19 -6
- package/dist/video-extraction/extract-frame-via-broadcast-channel.js +67 -4
- package/dist/video-extraction/extract-frame.d.ts +21 -2
- package/dist/video-extraction/extract-frame.js +46 -9
- package/dist/video-extraction/get-frames-since-keyframe.d.ts +17 -10
- package/dist/video-extraction/get-frames-since-keyframe.js +77 -21
- package/dist/video-extraction/keyframe-bank.d.ts +3 -2
- package/dist/video-extraction/keyframe-bank.js +32 -12
- package/dist/video-extraction/keyframe-manager.d.ts +3 -8
- package/dist/video-extraction/keyframe-manager.js +25 -10
- package/package.json +4 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { LogLevel } from 'remotion';
|
|
2
|
-
import type {
|
|
3
|
-
export declare const extractFrameAndAudio: ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, loop, }: {
|
|
2
|
+
import type { ExtractFrameViaBroadcastChannelResult } from './video-extraction/extract-frame-via-broadcast-channel';
|
|
3
|
+
export declare const extractFrameAndAudio: ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, loop, audioStreamIndex, trimAfter, trimBefore, fps, }: {
|
|
4
4
|
src: string;
|
|
5
5
|
timeInSeconds: number;
|
|
6
6
|
logLevel: LogLevel;
|
|
@@ -9,8 +9,8 @@ export declare const extractFrameAndAudio: ({ src, timeInSeconds, logLevel, dura
|
|
|
9
9
|
includeAudio: boolean;
|
|
10
10
|
includeVideo: boolean;
|
|
11
11
|
loop: boolean;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}>;
|
|
12
|
+
audioStreamIndex: number;
|
|
13
|
+
trimAfter: number | undefined;
|
|
14
|
+
trimBefore: number | undefined;
|
|
15
|
+
fps: number;
|
|
16
|
+
}) => Promise<ExtractFrameViaBroadcastChannelResult>;
|
|
@@ -1,29 +1,72 @@
|
|
|
1
1
|
import { extractAudio } from './audio-extraction/extract-audio';
|
|
2
|
+
import { isNetworkError } from './is-network-error';
|
|
2
3
|
import { extractFrame } from './video-extraction/extract-frame';
|
|
3
|
-
export const extractFrameAndAudio = async ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, loop, }) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
4
|
+
export const extractFrameAndAudio = async ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, loop, audioStreamIndex, trimAfter, trimBefore, fps, }) => {
|
|
5
|
+
try {
|
|
6
|
+
const [frame, audio] = await Promise.all([
|
|
7
|
+
includeVideo
|
|
8
|
+
? extractFrame({
|
|
9
|
+
src,
|
|
10
|
+
timeInSeconds,
|
|
11
|
+
logLevel,
|
|
12
|
+
loop,
|
|
13
|
+
trimAfter,
|
|
14
|
+
playbackRate,
|
|
15
|
+
trimBefore,
|
|
16
|
+
fps,
|
|
17
|
+
})
|
|
18
|
+
: null,
|
|
19
|
+
includeAudio
|
|
20
|
+
? extractAudio({
|
|
21
|
+
src,
|
|
22
|
+
timeInSeconds,
|
|
23
|
+
durationInSeconds,
|
|
24
|
+
logLevel,
|
|
25
|
+
loop,
|
|
26
|
+
playbackRate,
|
|
27
|
+
audioStreamIndex,
|
|
28
|
+
trimAfter,
|
|
29
|
+
fps,
|
|
30
|
+
trimBefore,
|
|
31
|
+
})
|
|
32
|
+
: null,
|
|
33
|
+
]);
|
|
34
|
+
if (frame?.type === 'cannot-decode') {
|
|
35
|
+
return {
|
|
36
|
+
type: 'cannot-decode',
|
|
37
|
+
durationInSeconds: frame.durationInSeconds,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (frame?.type === 'unknown-container-format') {
|
|
41
|
+
return { type: 'unknown-container-format' };
|
|
42
|
+
}
|
|
43
|
+
if (audio === 'unknown-container-format') {
|
|
44
|
+
if (frame !== null) {
|
|
45
|
+
frame?.frame?.close();
|
|
46
|
+
}
|
|
47
|
+
return { type: 'unknown-container-format' };
|
|
48
|
+
}
|
|
49
|
+
if (audio === 'cannot-decode') {
|
|
50
|
+
if (frame?.type === 'success' && frame.frame !== null) {
|
|
51
|
+
frame?.frame.close();
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
type: 'cannot-decode',
|
|
55
|
+
durationInSeconds: frame?.type === 'success' ? frame.durationInSeconds : null,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
type: 'success',
|
|
60
|
+
frame: frame?.frame?.toVideoFrame() ?? null,
|
|
61
|
+
audio: audio?.data ?? null,
|
|
62
|
+
durationInSeconds: audio?.durationInSeconds ?? null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const error = err;
|
|
67
|
+
if (isNetworkError(error)) {
|
|
68
|
+
return { type: 'network-error' };
|
|
69
|
+
}
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
29
72
|
};
|
package/dist/get-sink-weak.d.ts
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import type { LogLevel } from 'remotion';
|
|
2
2
|
import type { GetSink } from './video-extraction/get-frames-since-keyframe';
|
|
3
3
|
export declare const sinkPromises: Record<string, Promise<GetSink>>;
|
|
4
|
-
export declare const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
packetSink: import("mediabunny").EncodedPacketSink;
|
|
8
|
-
} | null;
|
|
9
|
-
audio: {
|
|
10
|
-
sampleSink: import("mediabunny").AudioSampleSink;
|
|
11
|
-
} | null;
|
|
4
|
+
export declare const getSink: (src: string, logLevel: LogLevel) => Promise<{
|
|
5
|
+
getVideo: () => Promise<import("./video-extraction/get-frames-since-keyframe").VideoSinkResult>;
|
|
6
|
+
getAudio: (index: number) => Promise<import("./video-extraction/get-frames-since-keyframe").AudioSinkResult>;
|
|
12
7
|
actualMatroskaTimestamps: {
|
|
13
8
|
observeTimestamp: (startTime: number) => void;
|
|
14
9
|
getRealTimestamp: (observedTimestamp: number) => number | null;
|
package/dist/get-sink-weak.js
CHANGED
|
@@ -1,23 +1,15 @@
|
|
|
1
1
|
import { Internals } from 'remotion';
|
|
2
2
|
import { getSinks } from './video-extraction/get-frames-since-keyframe';
|
|
3
3
|
export const sinkPromises = {};
|
|
4
|
-
export const
|
|
4
|
+
export const getSink = (src, logLevel) => {
|
|
5
5
|
let promise = sinkPromises[src];
|
|
6
6
|
if (!promise) {
|
|
7
|
-
promise = getSinks(src);
|
|
8
|
-
sinkPromises[src] = promise;
|
|
9
|
-
}
|
|
10
|
-
let awaited = await promise;
|
|
11
|
-
let deferredValue = awaited.deref();
|
|
12
|
-
if (!deferredValue) {
|
|
13
7
|
Internals.Log.verbose({
|
|
14
8
|
logLevel,
|
|
15
9
|
tag: '@remotion/media',
|
|
16
|
-
}, `Sink for ${src} was
|
|
10
|
+
}, `Sink for ${src} was not found, creating new sink`);
|
|
17
11
|
promise = getSinks(src);
|
|
18
12
|
sinkPromises[src] = promise;
|
|
19
|
-
awaited = await promise;
|
|
20
|
-
deferredValue = awaited.deref();
|
|
21
13
|
}
|
|
22
|
-
return
|
|
14
|
+
return promise;
|
|
23
15
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { LogLevel } from 'remotion';
|
|
2
|
+
import type { GetSink } from './video-extraction/get-frames-since-keyframe';
|
|
3
|
+
export declare const sinkPromises: Record<string, Promise<GetSink>>;
|
|
4
|
+
export declare const getSink: (src: string, logLevel: LogLevel) => Promise<{
|
|
5
|
+
getVideo: () => Promise<import("./video-extraction/get-frames-since-keyframe").VideoSinkResult>;
|
|
6
|
+
getAudio: (index: number) => Promise<import("./video-extraction/get-frames-since-keyframe").AudioSinkResult>;
|
|
7
|
+
actualMatroskaTimestamps: {
|
|
8
|
+
observeTimestamp: (startTime: number) => void;
|
|
9
|
+
getRealTimestamp: (observedTimestamp: number) => number | null;
|
|
10
|
+
};
|
|
11
|
+
isMatroska: boolean;
|
|
12
|
+
getDuration: () => Promise<number>;
|
|
13
|
+
}>;
|
package/dist/get-sink.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Internals } from 'remotion';
|
|
2
|
+
import { getSinks } from './video-extraction/get-frames-since-keyframe';
|
|
3
|
+
export const sinkPromises = {};
|
|
4
|
+
export const getSink = (src, logLevel) => {
|
|
5
|
+
let promise = sinkPromises[src];
|
|
6
|
+
if (!promise) {
|
|
7
|
+
Internals.Log.verbose({
|
|
8
|
+
logLevel,
|
|
9
|
+
tag: '@remotion/media',
|
|
10
|
+
}, `Sink for ${src} was not found, creating new sink`);
|
|
11
|
+
promise = getSinks(src);
|
|
12
|
+
sinkPromises[src] = promise;
|
|
13
|
+
}
|
|
14
|
+
return promise;
|
|
15
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const getTimeInSeconds: ({ loop, mediaDurationInSeconds, unloopedTimeInSeconds, src, trimAfter, trimBefore, fps, playbackRate, }: {
|
|
2
|
+
loop: boolean;
|
|
3
|
+
mediaDurationInSeconds: number | null;
|
|
4
|
+
unloopedTimeInSeconds: number;
|
|
5
|
+
src: string;
|
|
6
|
+
trimAfter: number | undefined;
|
|
7
|
+
trimBefore: number | undefined;
|
|
8
|
+
playbackRate: number;
|
|
9
|
+
fps: number;
|
|
10
|
+
}) => number | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Internals } from 'remotion';
|
|
2
|
+
export const getTimeInSeconds = ({ loop, mediaDurationInSeconds, unloopedTimeInSeconds, src, trimAfter, trimBefore, fps, playbackRate, }) => {
|
|
3
|
+
if (mediaDurationInSeconds === null && loop) {
|
|
4
|
+
throw new Error(`Could not determine duration of ${src}, but "loop" was set.`);
|
|
5
|
+
}
|
|
6
|
+
const loopDuration = loop
|
|
7
|
+
? Internals.calculateLoopDuration({
|
|
8
|
+
trimAfter,
|
|
9
|
+
mediaDurationInFrames: mediaDurationInSeconds * fps,
|
|
10
|
+
// Playback rate was already specified before
|
|
11
|
+
playbackRate: 1,
|
|
12
|
+
trimBefore,
|
|
13
|
+
}) / fps
|
|
14
|
+
: Infinity;
|
|
15
|
+
const timeInSeconds = (unloopedTimeInSeconds * playbackRate) % loopDuration;
|
|
16
|
+
if ((trimAfter ?? null) !== null) {
|
|
17
|
+
if (!loop) {
|
|
18
|
+
const time = (trimAfter - (trimBefore ?? 0)) / fps;
|
|
19
|
+
if (timeInSeconds >= time) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return timeInSeconds + (trimBefore ?? 0) / fps;
|
|
25
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { Audio } from './audio/audio';
|
|
2
|
+
import { Video } from './video/video';
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Now just `Audio`
|
|
5
|
+
*/
|
|
6
|
+
export declare const experimental_Audio: import("react").FC<import(".").AudioProps>;
|
|
7
|
+
/**
|
|
8
|
+
* @deprecated Now just `Video`
|
|
9
|
+
*/
|
|
10
|
+
export declare const experimental_Video: import("react").FC<import(".").VideoProps>;
|
|
11
|
+
export { AudioForPreview } from './audio/audio-for-preview';
|
|
12
|
+
export { AudioProps, FallbackHtml5AudioProps } from './audio/props';
|
|
3
13
|
export { VideoProps } from './video/props';
|
|
4
|
-
export { Video
|
|
14
|
+
export { Audio, Video };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { Audio } from './audio/audio';
|
|
2
|
+
import { Video } from './video/video';
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated Now just `Audio`
|
|
5
|
+
*/
|
|
6
|
+
export const experimental_Audio = Audio;
|
|
7
|
+
/**
|
|
8
|
+
* @deprecated Now just `Video`
|
|
9
|
+
*/
|
|
10
|
+
export const experimental_Video = Video;
|
|
11
|
+
export { AudioForPreview } from './audio/audio-for-preview';
|
|
12
|
+
export { Audio, Video };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility to check if error is network error
|
|
3
|
+
* @param error
|
|
4
|
+
* @returns
|
|
5
|
+
*/
|
|
6
|
+
export function isNetworkError(error) {
|
|
7
|
+
if (
|
|
8
|
+
// Chrome
|
|
9
|
+
error.message.includes('Failed to fetch') ||
|
|
10
|
+
// Safari
|
|
11
|
+
error.message.includes('Load failed') ||
|
|
12
|
+
// Firefox
|
|
13
|
+
error.message.includes('NetworkError when attempting to fetch resource')) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const renderTimestampRange: (timestamps: number[]) => string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const renderTimestampRange = (timestamps) => {
|
|
2
|
+
if (timestamps.length === 0) {
|
|
3
|
+
return '(none)';
|
|
4
|
+
}
|
|
5
|
+
if (timestamps.length === 1) {
|
|
6
|
+
return timestamps[0].toFixed(3);
|
|
7
|
+
}
|
|
8
|
+
return `${timestamps[0].toFixed(3)}...${timestamps[timestamps.length - 1].toFixed(3)}`;
|
|
9
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { LogLevel } from 'remotion';
|
|
2
|
+
export declare const SEEK_THRESHOLD = 0.05;
|
|
3
|
+
export type MediaPlayerInitResult = {
|
|
4
|
+
type: 'success';
|
|
5
|
+
} | {
|
|
6
|
+
type: 'unknown-container-format';
|
|
7
|
+
} | {
|
|
8
|
+
type: 'cannot-decode';
|
|
9
|
+
} | {
|
|
10
|
+
type: 'network-error';
|
|
11
|
+
} | {
|
|
12
|
+
type: 'no-tracks';
|
|
13
|
+
};
|
|
14
|
+
export declare class MediaPlayer {
|
|
15
|
+
private canvas;
|
|
16
|
+
private context;
|
|
17
|
+
private src;
|
|
18
|
+
private logLevel;
|
|
19
|
+
private playbackRate;
|
|
20
|
+
private audioStreamIndex;
|
|
21
|
+
private canvasSink;
|
|
22
|
+
private videoFrameIterator;
|
|
23
|
+
private nextFrame;
|
|
24
|
+
private audioSink;
|
|
25
|
+
private audioBufferIterator;
|
|
26
|
+
private queuedAudioNodes;
|
|
27
|
+
private gainNode;
|
|
28
|
+
private sharedAudioContext;
|
|
29
|
+
private audioSyncAnchor;
|
|
30
|
+
private playing;
|
|
31
|
+
private muted;
|
|
32
|
+
private loop;
|
|
33
|
+
private trimBeforeSeconds;
|
|
34
|
+
private trimAfterSeconds;
|
|
35
|
+
private animationFrameId;
|
|
36
|
+
private videoAsyncId;
|
|
37
|
+
private audioAsyncId;
|
|
38
|
+
private initialized;
|
|
39
|
+
private totalDuration;
|
|
40
|
+
private isBuffering;
|
|
41
|
+
private onBufferingChangeCallback?;
|
|
42
|
+
private audioBufferHealth;
|
|
43
|
+
private audioIteratorStarted;
|
|
44
|
+
private readonly HEALTHY_BUFER_THRESHOLD_SECONDS;
|
|
45
|
+
private onVideoFrameCallback?;
|
|
46
|
+
constructor({ canvas, src, logLevel, sharedAudioContext, loop, trimBeforeSeconds, trimAfterSeconds, playbackRate, audioStreamIndex, }: {
|
|
47
|
+
canvas: HTMLCanvasElement | null;
|
|
48
|
+
src: string;
|
|
49
|
+
logLevel: LogLevel;
|
|
50
|
+
sharedAudioContext: AudioContext;
|
|
51
|
+
loop: boolean;
|
|
52
|
+
trimBeforeSeconds: number | undefined;
|
|
53
|
+
trimAfterSeconds: number | undefined;
|
|
54
|
+
playbackRate: number;
|
|
55
|
+
audioStreamIndex: number;
|
|
56
|
+
});
|
|
57
|
+
private input;
|
|
58
|
+
private isReady;
|
|
59
|
+
private hasAudio;
|
|
60
|
+
private isCurrentlyBuffering;
|
|
61
|
+
initialize(startTimeUnresolved: number): Promise<MediaPlayerInitResult>;
|
|
62
|
+
private cleanupAudioQueue;
|
|
63
|
+
private cleanAudioIteratorAndNodes;
|
|
64
|
+
seekTo(time: number): Promise<void>;
|
|
65
|
+
play(): Promise<void>;
|
|
66
|
+
pause(): void;
|
|
67
|
+
setMuted(muted: boolean): void;
|
|
68
|
+
setVolume(volume: number): void;
|
|
69
|
+
setPlaybackRate(rate: number): void;
|
|
70
|
+
setLoop(loop: boolean): void;
|
|
71
|
+
dispose(): void;
|
|
72
|
+
private getPlaybackTime;
|
|
73
|
+
private scheduleAudioChunk;
|
|
74
|
+
onBufferingChange(callback: (isBuffering: boolean) => void): () => void;
|
|
75
|
+
onVideoFrame(callback: (frame: CanvasImageSource) => void): () => void;
|
|
76
|
+
private canRenderVideo;
|
|
77
|
+
private startRenderLoop;
|
|
78
|
+
private stopRenderLoop;
|
|
79
|
+
private render;
|
|
80
|
+
private shouldRenderFrame;
|
|
81
|
+
private drawCurrentFrame;
|
|
82
|
+
private startAudioIterator;
|
|
83
|
+
private startVideoIterator;
|
|
84
|
+
private updateNextFrame;
|
|
85
|
+
private bufferingStartedAtMs;
|
|
86
|
+
private minBufferingTimeoutMs;
|
|
87
|
+
private setBufferingState;
|
|
88
|
+
private maybeResumeFromBuffering;
|
|
89
|
+
private maybeForceResumeFromBuffering;
|
|
90
|
+
private runAudioIterator;
|
|
91
|
+
}
|