@remotion/media 4.0.351

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 (94) hide show
  1. package/LICENSE.md +49 -0
  2. package/README.md +18 -0
  3. package/dist/audio/audio-for-rendering.d.ts +3 -0
  4. package/dist/audio/audio-for-rendering.js +99 -0
  5. package/dist/audio/audio.d.ts +3 -0
  6. package/dist/audio/audio.js +60 -0
  7. package/dist/audio/props.d.ts +29 -0
  8. package/dist/audio/props.js +1 -0
  9. package/dist/audio-extraction/audio-cache.d.ts +11 -0
  10. package/dist/audio-extraction/audio-cache.js +58 -0
  11. package/dist/audio-extraction/audio-iterator.d.ts +24 -0
  12. package/dist/audio-extraction/audio-iterator.js +106 -0
  13. package/dist/audio-extraction/audio-manager.d.ts +64 -0
  14. package/dist/audio-extraction/audio-manager.js +83 -0
  15. package/dist/audio-extraction/extract-audio.d.ts +10 -0
  16. package/dist/audio-extraction/extract-audio.js +77 -0
  17. package/dist/audio-for-rendering.d.ts +3 -0
  18. package/dist/audio-for-rendering.js +94 -0
  19. package/dist/audio.d.ts +3 -0
  20. package/dist/audio.js +60 -0
  21. package/dist/audiodata-to-array.d.ts +0 -0
  22. package/dist/audiodata-to-array.js +1 -0
  23. package/dist/caches.d.ts +86 -0
  24. package/dist/caches.js +15 -0
  25. package/dist/convert-audiodata/combine-audiodata.d.ts +2 -0
  26. package/dist/convert-audiodata/combine-audiodata.js +40 -0
  27. package/dist/convert-audiodata/convert-audiodata.d.ts +16 -0
  28. package/dist/convert-audiodata/convert-audiodata.js +53 -0
  29. package/dist/convert-audiodata/data-types.d.ts +1 -0
  30. package/dist/convert-audiodata/data-types.js +22 -0
  31. package/dist/convert-audiodata/is-planar-format.d.ts +1 -0
  32. package/dist/convert-audiodata/is-planar-format.js +3 -0
  33. package/dist/convert-audiodata/log-audiodata.d.ts +1 -0
  34. package/dist/convert-audiodata/log-audiodata.js +8 -0
  35. package/dist/convert-audiodata/resample-audiodata.d.ts +10 -0
  36. package/dist/convert-audiodata/resample-audiodata.js +72 -0
  37. package/dist/convert-audiodata/trim-audiodata.d.ts +0 -0
  38. package/dist/convert-audiodata/trim-audiodata.js +1 -0
  39. package/dist/deserialized-audiodata.d.ts +15 -0
  40. package/dist/deserialized-audiodata.js +26 -0
  41. package/dist/esm/index.mjs +14487 -0
  42. package/dist/extract-audio.d.ts +7 -0
  43. package/dist/extract-audio.js +98 -0
  44. package/dist/extract-frame-and-audio.d.ts +15 -0
  45. package/dist/extract-frame-and-audio.js +28 -0
  46. package/dist/extract-frame-via-broadcast-channel.d.ts +15 -0
  47. package/dist/extract-frame-via-broadcast-channel.js +104 -0
  48. package/dist/extract-frame.d.ts +27 -0
  49. package/dist/extract-frame.js +21 -0
  50. package/dist/extrct-audio.d.ts +7 -0
  51. package/dist/extrct-audio.js +94 -0
  52. package/dist/get-frames-since-keyframe.d.ts +22 -0
  53. package/dist/get-frames-since-keyframe.js +41 -0
  54. package/dist/index.d.ts +4 -0
  55. package/dist/index.js +2 -0
  56. package/dist/keyframe-bank.d.ts +25 -0
  57. package/dist/keyframe-bank.js +120 -0
  58. package/dist/keyframe-manager.d.ts +23 -0
  59. package/dist/keyframe-manager.js +170 -0
  60. package/dist/log.d.ts +10 -0
  61. package/dist/log.js +33 -0
  62. package/dist/new-video-for-rendering.d.ts +3 -0
  63. package/dist/new-video-for-rendering.js +108 -0
  64. package/dist/new-video.d.ts +3 -0
  65. package/dist/new-video.js +37 -0
  66. package/dist/props.d.ts +29 -0
  67. package/dist/props.js +1 -0
  68. package/dist/remember-actual-matroska-timestamps.d.ts +4 -0
  69. package/dist/remember-actual-matroska-timestamps.js +19 -0
  70. package/dist/serialize-videoframe.d.ts +0 -0
  71. package/dist/serialize-videoframe.js +1 -0
  72. package/dist/video/props.d.ts +28 -0
  73. package/dist/video/props.js +1 -0
  74. package/dist/video/video-for-rendering.d.ts +3 -0
  75. package/dist/video/video-for-rendering.js +115 -0
  76. package/dist/video/video.d.ts +3 -0
  77. package/dist/video/video.js +37 -0
  78. package/dist/video-extraction/extract-frame-via-broadcast-channel.d.ts +16 -0
  79. package/dist/video-extraction/extract-frame-via-broadcast-channel.js +107 -0
  80. package/dist/video-extraction/extract-frame.d.ts +9 -0
  81. package/dist/video-extraction/extract-frame.js +21 -0
  82. package/dist/video-extraction/get-frames-since-keyframe.d.ts +23 -0
  83. package/dist/video-extraction/get-frames-since-keyframe.js +42 -0
  84. package/dist/video-extraction/keyframe-bank.d.ts +25 -0
  85. package/dist/video-extraction/keyframe-bank.js +121 -0
  86. package/dist/video-extraction/keyframe-manager.d.ts +23 -0
  87. package/dist/video-extraction/keyframe-manager.js +171 -0
  88. package/dist/video-extraction/remember-actual-matroska-timestamps.d.ts +5 -0
  89. package/dist/video-extraction/remember-actual-matroska-timestamps.js +19 -0
  90. package/dist/video-for-rendering.d.ts +3 -0
  91. package/dist/video-for-rendering.js +108 -0
  92. package/dist/video.d.ts +3 -0
  93. package/dist/video.js +37 -0
  94. package/package.json +55 -0
@@ -0,0 +1,107 @@
1
+ import { extractFrameAndAudio } from '../extract-frame-and-audio';
2
+ // Doesn't exist in studio
3
+ if (window.remotion_broadcastChannel && window.remotion_isMainTab) {
4
+ window.remotion_broadcastChannel.addEventListener('message', async (event) => {
5
+ const data = event.data;
6
+ if (data.type === 'request') {
7
+ try {
8
+ const { frame, audio } = await extractFrameAndAudio({
9
+ src: data.src,
10
+ timeInSeconds: data.timeInSeconds,
11
+ logLevel: data.logLevel,
12
+ durationInSeconds: data.durationInSeconds,
13
+ includeAudio: data.includeAudio,
14
+ includeVideo: data.includeVideo,
15
+ volume: data.volume,
16
+ loop: data.loop,
17
+ });
18
+ const videoFrame = frame;
19
+ const imageBitmap = videoFrame
20
+ ? await createImageBitmap(videoFrame)
21
+ : null;
22
+ if (videoFrame) {
23
+ videoFrame.close();
24
+ }
25
+ const response = {
26
+ type: 'response-success',
27
+ id: data.id,
28
+ frame: imageBitmap,
29
+ audio,
30
+ };
31
+ window.remotion_broadcastChannel.postMessage(response);
32
+ videoFrame?.close();
33
+ }
34
+ catch (error) {
35
+ const response = {
36
+ type: 'response-error',
37
+ id: data.id,
38
+ errorStack: error.stack ?? 'No stack trace',
39
+ };
40
+ window.remotion_broadcastChannel.postMessage(response);
41
+ }
42
+ }
43
+ else {
44
+ throw new Error('Invalid message: ' + JSON.stringify(data));
45
+ }
46
+ });
47
+ }
48
+ export const extractFrameViaBroadcastChannel = ({ src, timeInSeconds, logLevel, durationInSeconds, includeAudio, includeVideo, isClientSideRendering, volume, loop, }) => {
49
+ if (isClientSideRendering || window.remotion_isMainTab) {
50
+ return extractFrameAndAudio({
51
+ logLevel,
52
+ src,
53
+ timeInSeconds,
54
+ durationInSeconds,
55
+ includeAudio,
56
+ includeVideo,
57
+ volume,
58
+ loop,
59
+ });
60
+ }
61
+ const requestId = crypto.randomUUID();
62
+ const resolvePromise = new Promise((resolve, reject) => {
63
+ const onMessage = (event) => {
64
+ const data = event.data;
65
+ if (!data) {
66
+ return;
67
+ }
68
+ if (data.type === 'response-success' && data.id === requestId) {
69
+ resolve({
70
+ frame: data.frame ? data.frame : null,
71
+ audio: data.audio ? data.audio : null,
72
+ });
73
+ window.remotion_broadcastChannel.removeEventListener('message', onMessage);
74
+ }
75
+ else if (data.type === 'response-error' && data.id === requestId) {
76
+ reject(data.errorStack);
77
+ window.remotion_broadcastChannel.removeEventListener('message', onMessage);
78
+ }
79
+ };
80
+ window.remotion_broadcastChannel.addEventListener('message', onMessage);
81
+ });
82
+ const request = {
83
+ type: 'request',
84
+ src,
85
+ timeInSeconds,
86
+ id: requestId,
87
+ logLevel,
88
+ durationInSeconds,
89
+ includeAudio,
90
+ includeVideo,
91
+ volume,
92
+ loop,
93
+ };
94
+ window.remotion_broadcastChannel.postMessage(request);
95
+ let timeoutId;
96
+ return Promise.race([
97
+ resolvePromise.then((res) => {
98
+ clearTimeout(timeoutId);
99
+ return res;
100
+ }),
101
+ new Promise((_, reject) => {
102
+ timeoutId = setTimeout(() => {
103
+ reject(new Error(`Timeout while extracting frame at time ${timeInSeconds}sec from ${src}`));
104
+ }, Math.max(3000, window.remotion_puppeteerTimeout - 5000));
105
+ }),
106
+ ]);
107
+ };
@@ -0,0 +1,9 @@
1
+ import type { LogLevel } from '../log';
2
+ import { type GetSink } from './get-frames-since-keyframe';
3
+ export declare const sinkPromises: Record<string, Promise<GetSink>>;
4
+ export declare const extractFrame: ({ src, timeInSeconds: unloopedTimeinSeconds, logLevel, loop, }: {
5
+ src: string;
6
+ timeInSeconds: number;
7
+ logLevel: LogLevel;
8
+ loop: boolean;
9
+ }) => Promise<import("mediabunny").VideoSample | null>;
@@ -0,0 +1,21 @@
1
+ import { keyframeManager } from '../caches';
2
+ import { getSinks } from './get-frames-since-keyframe';
3
+ export const sinkPromises = {};
4
+ export const extractFrame = async ({ src, timeInSeconds: unloopedTimeinSeconds, logLevel, loop, }) => {
5
+ if (!sinkPromises[src]) {
6
+ sinkPromises[src] = getSinks(src);
7
+ }
8
+ const { video, getDuration } = await sinkPromises[src];
9
+ const timeInSeconds = loop
10
+ ? unloopedTimeinSeconds % (await getDuration())
11
+ : unloopedTimeinSeconds;
12
+ const keyframeBank = await keyframeManager.requestKeyframeBank({
13
+ packetSink: video.packetSink,
14
+ videoSampleSink: video.sampleSink,
15
+ timestamp: timeInSeconds,
16
+ src,
17
+ logLevel,
18
+ });
19
+ const frame = await keyframeBank.getFrameFromTimestamp(timeInSeconds);
20
+ return frame;
21
+ };
@@ -0,0 +1,23 @@
1
+ import type { EncodedPacket } from 'mediabunny';
2
+ import { AudioSampleSink, EncodedPacketSink, VideoSampleSink } from 'mediabunny';
3
+ export declare const getSinks: (src: string) => Promise<{
4
+ video: {
5
+ sampleSink: VideoSampleSink;
6
+ packetSink: EncodedPacketSink;
7
+ };
8
+ audio: {
9
+ sampleSink: AudioSampleSink;
10
+ } | null;
11
+ actualMatroskaTimestamps: {
12
+ observeTimestamp: (startTime: number) => void;
13
+ getRealTimestamp: (observedTimestamp: number) => number | null;
14
+ };
15
+ isMatroska: boolean;
16
+ getDuration: () => Promise<number>;
17
+ }>;
18
+ export type GetSink = Awaited<ReturnType<typeof getSinks>>;
19
+ export declare const getFramesSinceKeyframe: ({ packetSink, videoSampleSink, startPacket, }: {
20
+ packetSink: EncodedPacketSink;
21
+ videoSampleSink: VideoSampleSink;
22
+ startPacket: EncodedPacket;
23
+ }) => Promise<import("./keyframe-bank").KeyframeBank>;
@@ -0,0 +1,42 @@
1
+ import { ALL_FORMATS, AudioSampleSink, EncodedPacketSink, Input, MATROSKA, UrlSource, VideoSampleSink, } from 'mediabunny';
2
+ import { makeKeyframeBank } from './keyframe-bank';
3
+ import { rememberActualMatroskaTimestamps } from './remember-actual-matroska-timestamps';
4
+ export const getSinks = async (src) => {
5
+ const input = new Input({
6
+ formats: ALL_FORMATS,
7
+ source: new UrlSource(src),
8
+ });
9
+ const format = await input.getFormat();
10
+ const videoTrack = await input.getPrimaryVideoTrack();
11
+ if (!videoTrack) {
12
+ throw new Error(`No video track found for ${src}`);
13
+ }
14
+ const audioTrack = await input.getPrimaryAudioTrack();
15
+ const isMatroska = format === MATROSKA;
16
+ return {
17
+ video: {
18
+ sampleSink: new VideoSampleSink(videoTrack),
19
+ packetSink: new EncodedPacketSink(videoTrack),
20
+ },
21
+ audio: audioTrack
22
+ ? {
23
+ sampleSink: new AudioSampleSink(audioTrack),
24
+ }
25
+ : null,
26
+ actualMatroskaTimestamps: rememberActualMatroskaTimestamps(isMatroska),
27
+ isMatroska,
28
+ getDuration: () => input.computeDuration(),
29
+ };
30
+ };
31
+ export const getFramesSinceKeyframe = async ({ packetSink, videoSampleSink, startPacket, }) => {
32
+ const nextKeyPacket = await packetSink.getNextKeyPacket(startPacket, {
33
+ verifyKeyPackets: false,
34
+ });
35
+ const sampleIterator = videoSampleSink.samples(startPacket.timestamp, nextKeyPacket ? nextKeyPacket.timestamp : Infinity);
36
+ const keyframeBank = makeKeyframeBank({
37
+ startTimestampInSeconds: startPacket.timestamp,
38
+ endTimestampInSeconds: nextKeyPacket ? nextKeyPacket.timestamp : Infinity,
39
+ sampleIterator,
40
+ });
41
+ return keyframeBank;
42
+ };
@@ -0,0 +1,25 @@
1
+ import type { VideoSample } from 'mediabunny';
2
+ import type { LogLevel } from '../log';
3
+ export type KeyframeBank = {
4
+ startTimestampInSeconds: number;
5
+ endTimestampInSeconds: number;
6
+ getFrameFromTimestamp: (timestamp: number) => Promise<VideoSample | null>;
7
+ prepareForDeletion: () => Promise<void>;
8
+ deleteFramesBeforeTimestamp: ({ logLevel, src, timestampInSeconds, }: {
9
+ timestampInSeconds: number;
10
+ logLevel: LogLevel;
11
+ src: string;
12
+ }) => void;
13
+ hasTimestampInSecond: (timestamp: number) => Promise<boolean>;
14
+ addFrame: (frame: VideoSample) => void;
15
+ getOpenFrameCount: () => {
16
+ size: number;
17
+ timestamps: number[];
18
+ };
19
+ getLastUsed: () => number;
20
+ };
21
+ export declare const makeKeyframeBank: ({ startTimestampInSeconds, endTimestampInSeconds, sampleIterator, }: {
22
+ startTimestampInSeconds: number;
23
+ endTimestampInSeconds: number;
24
+ sampleIterator: AsyncGenerator<VideoSample, void, unknown>;
25
+ }) => KeyframeBank;
@@ -0,0 +1,121 @@
1
+ import { Log } from '../log';
2
+ // Round to only 4 digits, because WebM has a timescale of 1_000, e.g. framer.webm
3
+ const roundTo4Digits = (timestamp) => {
4
+ return Math.round(timestamp * 1000) / 1000;
5
+ };
6
+ export const makeKeyframeBank = ({ startTimestampInSeconds, endTimestampInSeconds, sampleIterator, }) => {
7
+ const frames = {};
8
+ const frameTimestamps = [];
9
+ let lastUsed = Date.now();
10
+ let alloctionSize = 0;
11
+ const hasDecodedEnoughForTimestamp = (timestamp) => {
12
+ const lastFrameTimestamp = frameTimestamps[frameTimestamps.length - 1];
13
+ if (!lastFrameTimestamp) {
14
+ return false;
15
+ }
16
+ const lastFrame = frames[lastFrameTimestamp];
17
+ // Don't decode more, will probably have to re-decode everything
18
+ if (!lastFrame) {
19
+ return true;
20
+ }
21
+ return (roundTo4Digits(lastFrame.timestamp + lastFrame.duration) >
22
+ roundTo4Digits(timestamp));
23
+ };
24
+ const addFrame = (frame) => {
25
+ frames[frame.timestamp] = frame;
26
+ frameTimestamps.push(frame.timestamp);
27
+ alloctionSize += frame.allocationSize();
28
+ lastUsed = Date.now();
29
+ };
30
+ const ensureEnoughFramesForTimestamp = async (timestamp) => {
31
+ while (!hasDecodedEnoughForTimestamp(timestamp)) {
32
+ const sample = await sampleIterator.next();
33
+ if (sample.value) {
34
+ addFrame(sample.value);
35
+ }
36
+ if (sample.done) {
37
+ break;
38
+ }
39
+ }
40
+ lastUsed = Date.now();
41
+ };
42
+ const getFrameFromTimestamp = async (timestampInSeconds) => {
43
+ lastUsed = Date.now();
44
+ if (timestampInSeconds < startTimestampInSeconds) {
45
+ return Promise.reject(new Error(`Timestamp is before start timestamp (requested: ${timestampInSeconds}sec, start: ${startTimestampInSeconds})`));
46
+ }
47
+ if (timestampInSeconds > endTimestampInSeconds) {
48
+ return Promise.reject(new Error(`Timestamp is after end timestamp (requested: ${timestampInSeconds}sec, end: ${endTimestampInSeconds})`));
49
+ }
50
+ await ensureEnoughFramesForTimestamp(timestampInSeconds);
51
+ for (let i = frameTimestamps.length - 1; i >= 0; i--) {
52
+ const sample = frames[frameTimestamps[i]];
53
+ if (!sample) {
54
+ return null;
55
+ }
56
+ if (roundTo4Digits(sample.timestamp) <= roundTo4Digits(timestampInSeconds)) {
57
+ return sample;
58
+ }
59
+ }
60
+ return null;
61
+ };
62
+ const hasTimestampInSecond = async (timestamp) => {
63
+ return (await getFrameFromTimestamp(timestamp)) !== null;
64
+ };
65
+ const prepareForDeletion = async () => {
66
+ // Cleanup frames that have been extracted that might not have been retrieved yet
67
+ const { value } = await sampleIterator.return();
68
+ if (value) {
69
+ value.close();
70
+ }
71
+ for (const frameTimestamp of frameTimestamps) {
72
+ if (!frames[frameTimestamp]) {
73
+ continue;
74
+ }
75
+ alloctionSize -= frames[frameTimestamp].allocationSize();
76
+ frames[frameTimestamp].close();
77
+ delete frames[frameTimestamp];
78
+ }
79
+ frameTimestamps.length = 0;
80
+ };
81
+ const deleteFramesBeforeTimestamp = ({ logLevel, src, timestampInSeconds, }) => {
82
+ for (const frameTimestamp of frameTimestamps) {
83
+ const isLast = frameTimestamp === frameTimestamps[frameTimestamps.length - 1];
84
+ // Don't delete the last frame, since it may be the last one in the video!
85
+ if (isLast) {
86
+ continue;
87
+ }
88
+ if (frameTimestamp < timestampInSeconds) {
89
+ if (!frames[frameTimestamp]) {
90
+ continue;
91
+ }
92
+ alloctionSize -= frames[frameTimestamp].allocationSize();
93
+ frameTimestamps.splice(frameTimestamps.indexOf(frameTimestamp), 1);
94
+ frames[frameTimestamp].close();
95
+ delete frames[frameTimestamp];
96
+ Log.verbose(logLevel, `[Video] Deleted frame ${frameTimestamp} for src ${src}`);
97
+ }
98
+ }
99
+ };
100
+ const getOpenFrameCount = () => {
101
+ return {
102
+ size: alloctionSize,
103
+ timestamps: frameTimestamps,
104
+ };
105
+ };
106
+ const getLastUsed = () => {
107
+ return lastUsed;
108
+ };
109
+ const keyframeBank = {
110
+ startTimestampInSeconds,
111
+ endTimestampInSeconds,
112
+ getFrameFromTimestamp,
113
+ prepareForDeletion,
114
+ hasTimestampInSecond,
115
+ addFrame,
116
+ deleteFramesBeforeTimestamp,
117
+ getOpenFrameCount,
118
+ getLastUsed,
119
+ };
120
+ return keyframeBank;
121
+ };
@@ -0,0 +1,23 @@
1
+ import type { EncodedPacketSink, VideoSampleSink } from 'mediabunny';
2
+ import type { LogLevel } from '../log';
3
+ import { type KeyframeBank } from './keyframe-bank';
4
+ export declare const makeKeyframeManager: () => {
5
+ requestKeyframeBank: ({ packetSink, timestamp, videoSampleSink, src, logLevel, }: {
6
+ timestamp: number;
7
+ packetSink: EncodedPacketSink;
8
+ videoSampleSink: VideoSampleSink;
9
+ src: string;
10
+ logLevel: LogLevel;
11
+ }) => Promise<KeyframeBank>;
12
+ addKeyframeBank: ({ src, bank, startTimestampInSeconds, }: {
13
+ src: string;
14
+ bank: Promise<KeyframeBank>;
15
+ startTimestampInSeconds: number;
16
+ }) => void;
17
+ getCacheStats: () => Promise<{
18
+ count: number;
19
+ totalSize: number;
20
+ }>;
21
+ clearAll: () => Promise<void>;
22
+ };
23
+ export type KeyframeManager = Awaited<ReturnType<typeof makeKeyframeManager>>;
@@ -0,0 +1,171 @@
1
+ import { getTotalCacheStats, MAX_CACHE_SIZE, SAFE_BACK_WINDOW_IN_SECONDS, } from '../caches';
2
+ import { Log } from '../log';
3
+ import { getFramesSinceKeyframe } from './get-frames-since-keyframe';
4
+ export const makeKeyframeManager = () => {
5
+ // src => {[startTimestampInSeconds]: KeyframeBank
6
+ const sources = {};
7
+ const addKeyframeBank = ({ src, bank, startTimestampInSeconds, }) => {
8
+ sources[src] = sources[src] ?? {};
9
+ sources[src][startTimestampInSeconds] = bank;
10
+ };
11
+ const logCacheStats = async (logLevel) => {
12
+ let count = 0;
13
+ let totalSize = 0;
14
+ for (const src in sources) {
15
+ for (const bank in sources[src]) {
16
+ const v = await sources[src][bank];
17
+ const { size, timestamps } = v.getOpenFrameCount();
18
+ count += timestamps.length;
19
+ totalSize += size;
20
+ if (size === 0) {
21
+ continue;
22
+ }
23
+ Log.verbose(logLevel, `[Video] Open frames for src ${src}: ${timestamps.join(', ')}`);
24
+ }
25
+ }
26
+ Log.verbose(logLevel, `[Video] Cache stats: ${count} open frames, ${totalSize} bytes`);
27
+ };
28
+ const getCacheStats = async () => {
29
+ let count = 0;
30
+ let totalSize = 0;
31
+ for (const src in sources) {
32
+ for (const bank in sources[src]) {
33
+ const v = await sources[src][bank];
34
+ const { timestamps, size } = v.getOpenFrameCount();
35
+ count += timestamps.length;
36
+ totalSize += size;
37
+ if (size === 0) {
38
+ continue;
39
+ }
40
+ }
41
+ }
42
+ return { count, totalSize };
43
+ };
44
+ const getTheKeyframeBankMostInThePast = async () => {
45
+ let mostInThePast = null;
46
+ let mostInThePastBank = null;
47
+ for (const src in sources) {
48
+ for (const b in sources[src]) {
49
+ const bank = await sources[src][b];
50
+ const lastUsed = bank.getLastUsed();
51
+ if (mostInThePast === null || lastUsed < mostInThePast) {
52
+ mostInThePast = lastUsed;
53
+ mostInThePastBank = { src, bank };
54
+ }
55
+ }
56
+ }
57
+ if (!mostInThePastBank) {
58
+ throw new Error('No keyframe bank found');
59
+ }
60
+ return mostInThePastBank;
61
+ };
62
+ const deleteOldestKeyframeBank = async (logLevel) => {
63
+ const { bank: mostInThePastBank, src: mostInThePastSrc } = await getTheKeyframeBankMostInThePast();
64
+ if (mostInThePastBank) {
65
+ await mostInThePastBank.prepareForDeletion();
66
+ delete sources[mostInThePastSrc][mostInThePastBank.startTimestampInSeconds];
67
+ Log.verbose(logLevel, `[Video] Deleted frames for src ${mostInThePastSrc} from ${mostInThePastBank.startTimestampInSeconds}sec to ${mostInThePastBank.endTimestampInSeconds}sec to free up memory.`);
68
+ }
69
+ };
70
+ const ensureToStayUnderMaxCacheSize = async (logLevel) => {
71
+ let cacheStats = await getTotalCacheStats();
72
+ while (cacheStats.totalSize > MAX_CACHE_SIZE) {
73
+ await deleteOldestKeyframeBank(logLevel);
74
+ cacheStats = await getTotalCacheStats();
75
+ }
76
+ };
77
+ const clearKeyframeBanksBeforeTime = async ({ timestampInSeconds, src, logLevel, }) => {
78
+ const threshold = timestampInSeconds - SAFE_BACK_WINDOW_IN_SECONDS;
79
+ if (!sources[src]) {
80
+ return;
81
+ }
82
+ const banks = Object.keys(sources[src]);
83
+ for (const startTimeInSeconds of banks) {
84
+ const bank = await sources[src][startTimeInSeconds];
85
+ const { endTimestampInSeconds, startTimestampInSeconds } = bank;
86
+ if (endTimestampInSeconds < threshold) {
87
+ await bank.prepareForDeletion();
88
+ Log.verbose(logLevel, `[Video] Cleared frames for src ${src} from ${startTimestampInSeconds}sec to ${endTimestampInSeconds}sec`);
89
+ delete sources[src][startTimeInSeconds];
90
+ }
91
+ else {
92
+ bank.deleteFramesBeforeTimestamp({
93
+ timestampInSeconds: threshold,
94
+ logLevel,
95
+ src,
96
+ });
97
+ }
98
+ }
99
+ await logCacheStats(logLevel);
100
+ };
101
+ const getKeyframeBankOrRefetch = async ({ packetSink, timestamp, videoSampleSink, src, logLevel, }) => {
102
+ const startPacket = await packetSink.getKeyPacket(timestamp, {
103
+ verifyKeyPackets: true,
104
+ });
105
+ if (!startPacket) {
106
+ throw new Error(`No key packet found for timestamp ${timestamp}`);
107
+ }
108
+ const startTimestampInSeconds = startPacket.timestamp;
109
+ const existingBank = sources[src]?.[startTimestampInSeconds];
110
+ // Bank does not yet exist, we need to fetch
111
+ if (!existingBank) {
112
+ const newKeyframeBank = getFramesSinceKeyframe({
113
+ packetSink,
114
+ videoSampleSink,
115
+ startPacket,
116
+ });
117
+ addKeyframeBank({ src, bank: newKeyframeBank, startTimestampInSeconds });
118
+ return newKeyframeBank;
119
+ }
120
+ // Bank exists and still has the frame we want
121
+ if (await (await existingBank).hasTimestampInSecond(timestamp)) {
122
+ return existingBank;
123
+ }
124
+ Log.verbose(logLevel, `[Video] Bank exists but frames have already been evicted!`);
125
+ // Bank exists but frames have already been evicted!
126
+ // First delete it entirely
127
+ await (await existingBank).prepareForDeletion();
128
+ delete sources[src][startTimestampInSeconds];
129
+ // Then refetch
130
+ const replacementKeybank = getFramesSinceKeyframe({
131
+ packetSink,
132
+ videoSampleSink,
133
+ startPacket,
134
+ });
135
+ addKeyframeBank({ src, bank: replacementKeybank, startTimestampInSeconds });
136
+ return replacementKeybank;
137
+ };
138
+ const requestKeyframeBank = async ({ packetSink, timestamp, videoSampleSink, src, logLevel, }) => {
139
+ await ensureToStayUnderMaxCacheSize(logLevel);
140
+ await clearKeyframeBanksBeforeTime({
141
+ timestampInSeconds: timestamp,
142
+ src,
143
+ logLevel,
144
+ });
145
+ const keyframeBank = await getKeyframeBankOrRefetch({
146
+ packetSink,
147
+ timestamp,
148
+ videoSampleSink,
149
+ src,
150
+ logLevel,
151
+ });
152
+ return keyframeBank;
153
+ };
154
+ const clearAll = async () => {
155
+ const srcs = Object.keys(sources);
156
+ for (const src of srcs) {
157
+ const banks = Object.keys(sources[src]);
158
+ for (const startTimeInSeconds of banks) {
159
+ const bank = await sources[src][startTimeInSeconds];
160
+ await bank.prepareForDeletion();
161
+ delete sources[src][startTimeInSeconds];
162
+ }
163
+ }
164
+ };
165
+ return {
166
+ requestKeyframeBank,
167
+ addKeyframeBank,
168
+ getCacheStats,
169
+ clearAll,
170
+ };
171
+ };
@@ -0,0 +1,5 @@
1
+ export declare const rememberActualMatroskaTimestamps: (isMatroska: boolean) => {
2
+ observeTimestamp: (startTime: number) => void;
3
+ getRealTimestamp: (observedTimestamp: number) => number | null;
4
+ };
5
+ export type RememberActualMatroskaTimestamps = ReturnType<typeof rememberActualMatroskaTimestamps>;
@@ -0,0 +1,19 @@
1
+ export const rememberActualMatroskaTimestamps = (isMatroska) => {
2
+ const observations = [];
3
+ const observeTimestamp = (startTime) => {
4
+ if (!isMatroska) {
5
+ return;
6
+ }
7
+ observations.push(startTime);
8
+ };
9
+ const getRealTimestamp = (observedTimestamp) => {
10
+ if (!isMatroska) {
11
+ return observedTimestamp;
12
+ }
13
+ return (observations.find((observation) => Math.abs(observedTimestamp - observation) < 0.001) ?? null);
14
+ };
15
+ return {
16
+ observeTimestamp,
17
+ getRealTimestamp,
18
+ };
19
+ };
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import type { VideoProps } from './video/props';
3
+ export declare const VideoForRendering: React.FC<VideoProps>;