@editframe/elements 0.16.8-beta.0 → 0.18.3-beta.0
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/README.md +30 -0
- package/dist/DecoderResetFrequency.test.d.ts +1 -0
- package/dist/DecoderResetRecovery.test.d.ts +1 -0
- package/dist/DelayedLoadingState.d.ts +48 -0
- package/dist/DelayedLoadingState.integration.test.d.ts +1 -0
- package/dist/DelayedLoadingState.js +113 -0
- package/dist/DelayedLoadingState.test.d.ts +1 -0
- package/dist/EF_FRAMEGEN.d.ts +10 -1
- package/dist/EF_FRAMEGEN.js +199 -179
- package/dist/EF_INTERACTIVE.js +2 -6
- package/dist/EF_RENDERING.js +1 -3
- package/dist/LoadingDebounce.test.d.ts +1 -0
- package/dist/LoadingIndicator.browsertest.d.ts +0 -0
- package/dist/ManualScrubTest.test.d.ts +1 -0
- package/dist/ScrubResolvedFlashing.test.d.ts +1 -0
- package/dist/ScrubTrackManager.d.ts +96 -0
- package/dist/ScrubTrackManager.test.d.ts +1 -0
- package/dist/VideoSeekFlashing.browsertest.d.ts +0 -0
- package/dist/VideoStuckDiagnostic.test.d.ts +1 -0
- package/dist/elements/CrossUpdateController.js +13 -15
- package/dist/elements/EFAudio.browsertest.d.ts +0 -0
- package/dist/elements/EFAudio.d.ts +22 -3
- package/dist/elements/EFAudio.js +60 -43
- package/dist/elements/EFCaptions.js +337 -373
- package/dist/elements/EFImage.d.ts +1 -0
- package/dist/elements/EFImage.js +73 -91
- package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +18 -0
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +41 -0
- package/dist/elements/EFMedia/AssetIdMediaEngine.test.d.ts +1 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +47 -0
- package/dist/elements/EFMedia/AssetMediaEngine.js +116 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +55 -0
- package/dist/elements/EFMedia/BaseMediaEngine.js +96 -0
- package/dist/elements/EFMedia/BaseMediaEngine.test.d.ts +1 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +43 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.js +159 -0
- package/dist/elements/EFMedia/JitMediaEngine.browsertest.d.ts +0 -0
- package/dist/elements/EFMedia/JitMediaEngine.d.ts +31 -0
- package/dist/elements/EFMedia/JitMediaEngine.js +62 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.d.ts +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +48 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +138 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +22 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.d.ts +7 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +24 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +18 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +16 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +104 -0
- package/dist/elements/EFMedia/services/AudioElementFactory.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/services/AudioElementFactory.d.ts +22 -0
- package/dist/elements/EFMedia/services/AudioElementFactory.js +72 -0
- package/dist/elements/EFMedia/services/MediaSourceService.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/services/MediaSourceService.d.ts +47 -0
- package/dist/elements/EFMedia/services/MediaSourceService.js +73 -0
- package/dist/elements/EFMedia/shared/AudioSpanUtils.d.ts +7 -0
- package/dist/elements/EFMedia/shared/AudioSpanUtils.js +54 -0
- package/dist/elements/EFMedia/shared/BufferUtils.d.ts +70 -0
- package/dist/elements/EFMedia/shared/BufferUtils.js +89 -0
- package/dist/elements/EFMedia/shared/MediaTaskUtils.d.ts +23 -0
- package/dist/elements/EFMedia/shared/RenditionHelpers.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +19 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +18 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +60 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts +1 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.d.ts +16 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +46 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.js +16 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.d.ts +3 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoInputTask.js +27 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.d.ts +7 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSeekTask.js +25 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.js +18 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.d.ts +9 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.d.ts +4 -0
- package/dist/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.js +16 -0
- package/dist/elements/EFMedia.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia.d.ts +95 -66
- package/dist/elements/EFMedia.js +204 -683
- package/dist/elements/EFSourceMixin.js +31 -48
- package/dist/elements/EFTemporal.d.ts +2 -1
- package/dist/elements/EFTemporal.js +266 -360
- package/dist/elements/EFTimegroup.d.ts +14 -1
- package/dist/elements/EFTimegroup.js +337 -323
- package/dist/elements/EFVideo.browsertest.d.ts +0 -0
- package/dist/elements/EFVideo.d.ts +123 -4
- package/dist/elements/EFVideo.js +308 -111
- package/dist/elements/EFWaveform.js +375 -411
- package/dist/elements/FetchMixin.js +14 -24
- package/dist/elements/MediaController.d.ts +30 -0
- package/dist/elements/SampleBuffer.d.ts +14 -0
- package/dist/elements/SampleBuffer.js +52 -0
- package/dist/elements/TargetController.js +130 -156
- package/dist/elements/TimegroupController.js +17 -19
- package/dist/elements/durationConverter.js +15 -4
- package/dist/elements/parseTimeToMs.js +4 -10
- package/dist/elements/printTaskStatus.d.ts +2 -0
- package/dist/elements/updateAnimations.js +39 -59
- package/dist/getRenderInfo.d.ts +2 -2
- package/dist/getRenderInfo.js +59 -67
- package/dist/gui/ContextMixin.js +150 -288
- package/dist/gui/EFConfiguration.js +27 -43
- package/dist/gui/EFFilmstrip.d.ts +3 -3
- package/dist/gui/EFFilmstrip.js +440 -620
- package/dist/gui/EFFitScale.d.ts +2 -2
- package/dist/gui/EFFitScale.js +112 -135
- package/dist/gui/EFFocusOverlay.js +45 -61
- package/dist/gui/EFPreview.js +30 -49
- package/dist/gui/EFScrubber.js +78 -99
- package/dist/gui/EFTimeDisplay.js +49 -70
- package/dist/gui/EFToggleLoop.js +17 -34
- package/dist/gui/EFTogglePlay.js +37 -58
- package/dist/gui/EFWorkbench.js +66 -88
- package/dist/gui/TWMixin.js +2 -48
- package/dist/gui/TWMixin2.js +31 -0
- package/dist/gui/efContext.js +2 -6
- package/dist/gui/fetchContext.js +1 -3
- package/dist/gui/focusContext.js +1 -3
- package/dist/gui/focusedElementContext.js +2 -6
- package/dist/gui/playingContext.js +1 -4
- package/dist/gui/services/ElementConnectionManager.browsertest.d.ts +1 -0
- package/dist/gui/services/ElementConnectionManager.d.ts +59 -0
- package/dist/gui/services/ElementConnectionManager.js +128 -0
- package/dist/gui/services/PlaybackController.browsertest.d.ts +1 -0
- package/dist/gui/services/PlaybackController.d.ts +103 -0
- package/dist/gui/services/PlaybackController.js +290 -0
- package/dist/index.js +5 -30
- package/dist/msToTimeCode.js +11 -13
- package/dist/services/MediaSourceManager.d.ts +62 -0
- package/dist/services/MediaSourceManager.js +211 -0
- package/dist/style.css +2 -1
- package/dist/transcoding/cache/CacheManager.d.ts +73 -0
- package/dist/transcoding/cache/RequestDeduplicator.d.ts +29 -0
- package/dist/transcoding/cache/RequestDeduplicator.js +53 -0
- package/dist/transcoding/cache/RequestDeduplicator.test.d.ts +1 -0
- package/dist/transcoding/types/index.d.ts +242 -0
- package/dist/transcoding/utils/MediaUtils.d.ts +9 -0
- package/dist/transcoding/utils/UrlGenerator.d.ts +26 -0
- package/dist/transcoding/utils/UrlGenerator.js +45 -0
- package/dist/transcoding/utils/constants.d.ts +27 -0
- package/dist/utils/LRUCache.d.ts +34 -0
- package/dist/utils/LRUCache.js +115 -0
- package/package.json +4 -3
- package/src/elements/EFAudio.browsertest.ts +709 -0
- package/src/elements/EFAudio.ts +59 -15
- package/src/elements/EFCaptions.browsertest.ts +0 -1
- package/src/elements/EFImage.browsertest.ts +42 -1
- package/src/elements/EFImage.ts +23 -3
- package/src/elements/EFMedia/AssetIdMediaEngine.test.ts +222 -0
- package/src/elements/EFMedia/AssetIdMediaEngine.ts +70 -0
- package/src/elements/EFMedia/AssetMediaEngine.ts +210 -0
- package/src/elements/EFMedia/BaseMediaEngine.test.ts +164 -0
- package/src/elements/EFMedia/BaseMediaEngine.ts +170 -0
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +400 -0
- package/src/elements/EFMedia/BufferedSeekingInput.ts +267 -0
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +165 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +110 -0
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.browsertest.ts +554 -0
- package/src/elements/EFMedia/audioTasks/makeAudioBufferTask.ts +81 -0
- package/src/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.ts +241 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.browsertest.ts +59 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.ts +23 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.browsertest.ts +55 -0
- package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +35 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +42 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +34 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +23 -0
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +174 -0
- package/src/elements/EFMedia/services/AudioElementFactory.browsertest.ts +325 -0
- package/src/elements/EFMedia/services/AudioElementFactory.ts +119 -0
- package/src/elements/EFMedia/services/MediaSourceService.browsertest.ts +257 -0
- package/src/elements/EFMedia/services/MediaSourceService.ts +102 -0
- package/src/elements/EFMedia/shared/AudioSpanUtils.ts +128 -0
- package/src/elements/EFMedia/shared/BufferUtils.ts +310 -0
- package/src/elements/EFMedia/shared/MediaTaskUtils.ts +44 -0
- package/src/elements/EFMedia/shared/RenditionHelpers.browsertest.ts +247 -0
- package/src/elements/EFMedia/shared/RenditionHelpers.ts +79 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.browsertest.ts +128 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.test.ts +233 -0
- package/src/elements/EFMedia/tasks/makeMediaEngineTask.ts +89 -0
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.browsertest.ts +555 -0
- package/src/elements/EFMedia/videoTasks/makeVideoBufferTask.ts +79 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.browsertest.ts +59 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInitSegmentFetchTask.ts +23 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInputTask.browsertest.ts +55 -0
- package/src/elements/EFMedia/videoTasks/makeVideoInputTask.ts +45 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSeekTask.ts +44 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +57 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +32 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +56 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +23 -0
- package/src/elements/EFMedia.browsertest.ts +696 -271
- package/src/elements/EFMedia.ts +218 -776
- package/src/elements/EFTemporal.browsertest.ts +0 -1
- package/src/elements/EFTemporal.ts +13 -3
- package/src/elements/EFTimegroup.browsertest.ts +6 -3
- package/src/elements/EFTimegroup.ts +221 -27
- package/src/elements/EFVideo.browsertest.ts +758 -0
- package/src/elements/EFVideo.ts +418 -68
- package/src/elements/EFWaveform.ts +5 -5
- package/src/elements/MediaController.ts +98 -0
- package/src/elements/SampleBuffer.ts +97 -0
- package/src/elements/printTaskStatus.ts +16 -0
- package/src/elements/updateAnimations.ts +6 -0
- package/src/gui/ContextMixin.ts +23 -104
- package/src/gui/TWMixin.ts +10 -3
- package/src/gui/services/ElementConnectionManager.browsertest.ts +263 -0
- package/src/gui/services/ElementConnectionManager.ts +224 -0
- package/src/gui/services/PlaybackController.browsertest.ts +437 -0
- package/src/gui/services/PlaybackController.ts +521 -0
- package/src/services/MediaSourceManager.ts +333 -0
- package/src/transcoding/cache/CacheManager.ts +208 -0
- package/src/transcoding/cache/RequestDeduplicator.test.ts +170 -0
- package/src/transcoding/cache/RequestDeduplicator.ts +65 -0
- package/src/transcoding/types/index.ts +265 -0
- package/src/transcoding/utils/MediaUtils.ts +63 -0
- package/src/transcoding/utils/UrlGenerator.ts +68 -0
- package/src/transcoding/utils/constants.ts +36 -0
- package/src/utils/LRUCache.ts +153 -0
- package/test/EFVideo.framegen.browsertest.ts +127 -0
- package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__32da3954ba60c96ad732020c65a08ebc/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__b0b2b07efcf607de8ee0f650328c32f7/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a75c2252b542e0c152c780e9a8d7b154/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a64ff1cfb1b52cae14df4b5dfa1e222b/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__91e8a522f950809b9f09f4173113b4b0/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__e66d2c831d951e74ad0aeaa6489795d0/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_1_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__26197f6f7c46cacb0a71134131c3f775/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_2_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__4cb6774cd3650ccf59c8f8dc6678c0b9/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_3_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0b3b2b1c8933f7fcf8a9ecaa88d58b41/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_init_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__0798c479b44aaeef850609a430f6e613/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/data.bin +1 -0
- package/test/__cache__/GET__api_v1_transcode_manifest_json_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__3be92a0437de726b431ed5af2369158a/metadata.json +19 -0
- package/test/createJitTestClips.ts +425 -0
- package/test/recordReplayProxyPlugin.js +302 -0
- package/test/useAssetMSW.ts +49 -0
- package/test/useMSW.ts +44 -0
- package/types.json +1 -1
- package/dist/gui/TWMixin.css.js +0 -4
- /package/dist/elements/{TargetController.test.d.ts → TargetController.browsertest.d.ts} +0 -0
- /package/src/elements/{TargetController.test.ts → TargetController.browsertest.ts} +0 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { test as baseTest, describe } from "vitest";
|
|
2
|
+
import { BufferedSeekingInput, NoSample } from "./BufferedSeekingInput";
|
|
3
|
+
|
|
4
|
+
const test = baseTest.extend<{
|
|
5
|
+
fiveSampleBuffer: BufferedSeekingInput;
|
|
6
|
+
inputAtStart: BufferedSeekingInput;
|
|
7
|
+
inputAtMiddle: BufferedSeekingInput;
|
|
8
|
+
segment2: BufferedSeekingInput;
|
|
9
|
+
}>({
|
|
10
|
+
fiveSampleBuffer: async ({}, use) => {
|
|
11
|
+
const response = await fetch("/jit-segments/segment-0ms-2s-low.mp4");
|
|
12
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
13
|
+
const input = new BufferedSeekingInput(arrayBuffer, {
|
|
14
|
+
videoBufferSize: 5,
|
|
15
|
+
audioBufferSize: 5,
|
|
16
|
+
});
|
|
17
|
+
await use(input);
|
|
18
|
+
},
|
|
19
|
+
inputAtStart: async ({}, use) => {
|
|
20
|
+
const response = await fetch("/jit-segments/segment-0ms-2s-low.mp4");
|
|
21
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
22
|
+
const input = new BufferedSeekingInput(arrayBuffer);
|
|
23
|
+
await use(input);
|
|
24
|
+
},
|
|
25
|
+
inputAtMiddle: async ({}, use) => {
|
|
26
|
+
const response = await fetch("/jit-segments/segment-6000ms-1s-low.mp4");
|
|
27
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
28
|
+
const input = new BufferedSeekingInput(arrayBuffer);
|
|
29
|
+
await use(input);
|
|
30
|
+
},
|
|
31
|
+
segment2: async ({}, use) => {
|
|
32
|
+
const response = await fetch("/jit-segments/segment-2.mp4");
|
|
33
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
34
|
+
const input = new BufferedSeekingInput(arrayBuffer);
|
|
35
|
+
await use(input);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("BufferedSeekingInput", () => {
|
|
40
|
+
describe("computeDuration", () => {
|
|
41
|
+
test("computes duration", async ({
|
|
42
|
+
expect,
|
|
43
|
+
inputAtStart,
|
|
44
|
+
inputAtMiddle,
|
|
45
|
+
}) => {
|
|
46
|
+
await expect(inputAtStart.computeDuration()).resolves.toBe(2);
|
|
47
|
+
await expect(inputAtMiddle.computeDuration()).resolves.toBeCloseTo(0.96);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("basic seeking", () => {
|
|
52
|
+
test("seeks to frame at 0 seconds", async ({ expect, inputAtStart }) => {
|
|
53
|
+
const sample = await inputAtStart.seek(1, 0);
|
|
54
|
+
expect(sample.timestamp).toBe(0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("seeks to frame at 0.02 seconds", async ({ expect, inputAtStart }) => {
|
|
58
|
+
const sample = await inputAtStart.seek(1, 20);
|
|
59
|
+
expect(sample.timestamp).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("seeks to frame at 0.04 seconds", async ({ expect, inputAtStart }) => {
|
|
63
|
+
const sample = await inputAtStart.seek(1, 40);
|
|
64
|
+
expect(sample.timestamp).toBe(0.04);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("deterministic seeking behavior", () => {
|
|
69
|
+
test("seeks to exact sample timestamps", async ({
|
|
70
|
+
expect,
|
|
71
|
+
inputAtStart,
|
|
72
|
+
}) => {
|
|
73
|
+
expect((await inputAtStart.seek(1, 0)).timestamp).toBe(0);
|
|
74
|
+
expect((await inputAtStart.seek(1, 40)).timestamp).toBe(0.04);
|
|
75
|
+
expect((await inputAtStart.seek(1, 80)).timestamp).toBe(0.08);
|
|
76
|
+
expect((await inputAtStart.seek(1, 120)).timestamp).toBe(0.12);
|
|
77
|
+
expect((await inputAtStart.seek(1, 160)).timestamp).toBe(0.16);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("seeks between samples returns previous sample", async ({
|
|
81
|
+
expect,
|
|
82
|
+
inputAtStart,
|
|
83
|
+
}) => {
|
|
84
|
+
expect((await inputAtStart.seek(1, 30)).timestamp).toBe(0);
|
|
85
|
+
expect((await inputAtStart.seek(1, 60)).timestamp).toBe(0.04);
|
|
86
|
+
expect((await inputAtStart.seek(1, 100)).timestamp).toBe(0.08);
|
|
87
|
+
expect((await inputAtStart.seek(1, 140)).timestamp).toBe(0.12);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("seeks before first sample", async ({ expect, inputAtStart }) => {
|
|
91
|
+
inputAtStart.clearBuffer(1);
|
|
92
|
+
expect((await inputAtStart.seek(1, 0)).timestamp).toBe(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("seeks to later samples in media", async ({
|
|
96
|
+
expect,
|
|
97
|
+
inputAtStart,
|
|
98
|
+
}) => {
|
|
99
|
+
const result200 = await inputAtStart.seek(1, 200);
|
|
100
|
+
const result1000 = await inputAtStart.seek(1, 1000);
|
|
101
|
+
|
|
102
|
+
expect(result200.timestamp! * 1000).toBeLessThanOrEqual(200);
|
|
103
|
+
expect(result1000.timestamp! * 1000).toBeLessThanOrEqual(1000);
|
|
104
|
+
expect(result200.timestamp).toBeGreaterThanOrEqual(0);
|
|
105
|
+
expect(result1000.timestamp).toBeGreaterThanOrEqual(result200.timestamp!);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("never returns future sample", async ({ expect, inputAtStart }) => {
|
|
109
|
+
const testCases = [
|
|
110
|
+
{ seekTimeMs: 0, expectedTimestamp: 0 },
|
|
111
|
+
{ seekTimeMs: 10, expectedTimestamp: 0 },
|
|
112
|
+
{ seekTimeMs: 30, expectedTimestamp: 0 },
|
|
113
|
+
{ seekTimeMs: 40, expectedTimestamp: 0.04 },
|
|
114
|
+
{ seekTimeMs: 50, expectedTimestamp: 0.04 },
|
|
115
|
+
{ seekTimeMs: 70, expectedTimestamp: 0.04 },
|
|
116
|
+
{ seekTimeMs: 80, expectedTimestamp: 0.08 },
|
|
117
|
+
{ seekTimeMs: 90, expectedTimestamp: 0.08 },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
for (const { seekTimeMs, expectedTimestamp } of testCases) {
|
|
121
|
+
const result = await inputAtStart.seek(1, seekTimeMs);
|
|
122
|
+
expect(result.timestamp).toBe(expectedTimestamp);
|
|
123
|
+
|
|
124
|
+
const resultTimeMs = result.timestamp! * 1000;
|
|
125
|
+
expect(resultTimeMs).toBeLessThanOrEqual(seekTimeMs);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("buffer state management", () => {
|
|
131
|
+
test("starts with empty buffer", async ({ expect, inputAtStart }) => {
|
|
132
|
+
expect(inputAtStart.getBufferSize(1)).toBe(0);
|
|
133
|
+
expect(inputAtStart.getBufferTimestamps(1)).toEqual([]);
|
|
134
|
+
expect(inputAtStart.getBufferContents(1)).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("maintains separate buffers per track", async ({
|
|
138
|
+
expect,
|
|
139
|
+
inputAtStart,
|
|
140
|
+
}) => {
|
|
141
|
+
await inputAtStart.seek(1, 0);
|
|
142
|
+
const track1BufferSize = inputAtStart.getBufferSize(1);
|
|
143
|
+
expect(track1BufferSize).toBeGreaterThan(0);
|
|
144
|
+
|
|
145
|
+
expect(inputAtStart.getBufferSize(2)).toBe(0);
|
|
146
|
+
|
|
147
|
+
await inputAtStart.seek(2, 0);
|
|
148
|
+
expect(inputAtStart.getBufferSize(2)).toBeGreaterThan(0);
|
|
149
|
+
expect(inputAtStart.getBufferSize(1)).toBe(track1BufferSize);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("buffer accumulates samples in order", async ({
|
|
153
|
+
expect,
|
|
154
|
+
inputAtStart,
|
|
155
|
+
}) => {
|
|
156
|
+
inputAtStart.clearBuffer(1);
|
|
157
|
+
|
|
158
|
+
await inputAtStart.seek(1, 0);
|
|
159
|
+
await inputAtStart.seek(1, 40);
|
|
160
|
+
await inputAtStart.seek(1, 80);
|
|
161
|
+
|
|
162
|
+
const timestamps = inputAtStart.getBufferTimestamps(1);
|
|
163
|
+
expect(timestamps).toContain(0);
|
|
164
|
+
expect(timestamps).toContain(0.04);
|
|
165
|
+
expect(timestamps).toContain(0.08);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("buffer extends one sample ahead", async ({
|
|
169
|
+
expect,
|
|
170
|
+
fiveSampleBuffer,
|
|
171
|
+
}) => {
|
|
172
|
+
await fiveSampleBuffer.seek(1, 960);
|
|
173
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
174
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("buffer resets when seeking back before the buffer", async ({
|
|
179
|
+
expect,
|
|
180
|
+
fiveSampleBuffer,
|
|
181
|
+
}) => {
|
|
182
|
+
await fiveSampleBuffer.seek(1, 960);
|
|
183
|
+
await fiveSampleBuffer.seek(1, 0);
|
|
184
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([0]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("buffer is maintained when seeking forwards within the buffer", async ({
|
|
188
|
+
expect,
|
|
189
|
+
fiveSampleBuffer,
|
|
190
|
+
}) => {
|
|
191
|
+
await fiveSampleBuffer.seek(1, 960);
|
|
192
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
193
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
194
|
+
]);
|
|
195
|
+
await fiveSampleBuffer.seek(1, 900);
|
|
196
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
197
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
198
|
+
]);
|
|
199
|
+
await fiveSampleBuffer.seek(1, 960);
|
|
200
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
201
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("buffer is maintained when seeking backwards within the buffer", async ({
|
|
206
|
+
expect,
|
|
207
|
+
fiveSampleBuffer,
|
|
208
|
+
}) => {
|
|
209
|
+
await fiveSampleBuffer.seek(1, 960);
|
|
210
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
211
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
212
|
+
]);
|
|
213
|
+
await fiveSampleBuffer.seek(1, 900);
|
|
214
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
215
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
216
|
+
]);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("buffer is maintained when seeking backwards to start of buffer", async ({
|
|
220
|
+
expect,
|
|
221
|
+
fiveSampleBuffer,
|
|
222
|
+
}) => {
|
|
223
|
+
await fiveSampleBuffer.seek(1, 960);
|
|
224
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
225
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
226
|
+
]);
|
|
227
|
+
await fiveSampleBuffer.seek(1, 800);
|
|
228
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
229
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
230
|
+
]);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("buffer is reset when seeking backwards to arbitrary time before buffer", async ({
|
|
234
|
+
expect,
|
|
235
|
+
fiveSampleBuffer,
|
|
236
|
+
}) => {
|
|
237
|
+
await fiveSampleBuffer.seek(1, 960);
|
|
238
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
239
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
240
|
+
]);
|
|
241
|
+
await fiveSampleBuffer.seek(1, 720);
|
|
242
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
243
|
+
0.56, 0.6, 0.64, 0.68, 0.72,
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("buffer is maintained when seeking forwards to end of buffer", async ({
|
|
248
|
+
expect,
|
|
249
|
+
fiveSampleBuffer,
|
|
250
|
+
}) => {
|
|
251
|
+
await fiveSampleBuffer.seek(1, 960);
|
|
252
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
253
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
254
|
+
]);
|
|
255
|
+
await fiveSampleBuffer.seek(1, 900);
|
|
256
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
257
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
258
|
+
]);
|
|
259
|
+
await fiveSampleBuffer.seek(1, 960);
|
|
260
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
261
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
262
|
+
]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("buffer is maintained when seeking forwards past the buffer", async ({
|
|
266
|
+
expect,
|
|
267
|
+
fiveSampleBuffer,
|
|
268
|
+
}) => {
|
|
269
|
+
await fiveSampleBuffer.seek(1, 960);
|
|
270
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
271
|
+
0.8, 0.84, 0.88, 0.92, 0.96,
|
|
272
|
+
]);
|
|
273
|
+
await fiveSampleBuffer.seek(1, 1000);
|
|
274
|
+
expect(fiveSampleBuffer.getBufferTimestamps(1)).toEqual([
|
|
275
|
+
0.84, 0.88, 0.92, 0.96, 1,
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("seeing to time not in buffer (time is before buffer)", () => {
|
|
281
|
+
test("throws error", async ({ expect, segment2 }) => {
|
|
282
|
+
await expect(segment2.seek(1, 0)).rejects.toThrow(NoSample);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe("seeking forward at 1ms intervals", () => {
|
|
287
|
+
test("returns all samples in the media", async ({
|
|
288
|
+
expect,
|
|
289
|
+
inputAtStart,
|
|
290
|
+
}) => {
|
|
291
|
+
const timestamps = new Set<number>();
|
|
292
|
+
for (let i = 0; i < 1999; i++) {
|
|
293
|
+
const sample = await inputAtStart.seek(1, i);
|
|
294
|
+
timestamps.add(sample.timestamp!);
|
|
295
|
+
}
|
|
296
|
+
expect(Array.from(timestamps)).toEqual([
|
|
297
|
+
0, 0.04, 0.08, 0.12, 0.16, 0.2, 0.24, 0.28, 0.32, 0.36, 0.4, 0.44, 0.48,
|
|
298
|
+
0.52, 0.56, 0.6, 0.64, 0.68, 0.72, 0.76, 0.8, 0.84, 0.88, 0.92, 0.96, 1,
|
|
299
|
+
1.04, 1.08, 1.12, 1.16, 1.2, 1.24, 1.28, 1.32, 1.36, 1.4, 1.44, 1.48,
|
|
300
|
+
1.52, 1.56, 1.6, 1.64, 1.68, 1.72, 1.76, 1.8, 1.84, 1.88, 1.92, 1.96,
|
|
301
|
+
]);
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("error handling", () => {
|
|
306
|
+
test("throws error for non-existent track", async ({
|
|
307
|
+
expect,
|
|
308
|
+
inputAtStart,
|
|
309
|
+
}) => {
|
|
310
|
+
await expect(inputAtStart.seek(999, 0)).rejects.toThrow(
|
|
311
|
+
"Track 999 not found",
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("concurrency handling", () => {
|
|
317
|
+
test("handles concurrent seeking operations safely", async ({
|
|
318
|
+
expect,
|
|
319
|
+
inputAtStart,
|
|
320
|
+
}) => {
|
|
321
|
+
const seek1 = inputAtStart.seek(1, 0);
|
|
322
|
+
const seek2 = inputAtStart.seek(1, 40);
|
|
323
|
+
const seek3 = inputAtStart.seek(1, 80);
|
|
324
|
+
const seek4 = inputAtStart.seek(1, 120);
|
|
325
|
+
|
|
326
|
+
const [sample1, sample2, sample3, sample4] = await Promise.all([
|
|
327
|
+
seek1,
|
|
328
|
+
seek2,
|
|
329
|
+
seek3,
|
|
330
|
+
seek4,
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
expect(sample1.timestamp).toBe(0);
|
|
334
|
+
expect(sample2.timestamp).toBe(0.04);
|
|
335
|
+
expect(sample3.timestamp).toBe(0.08);
|
|
336
|
+
expect(sample4.timestamp).toBe(0.12);
|
|
337
|
+
|
|
338
|
+
const bufferTimestamps = inputAtStart.getBufferTimestamps(1);
|
|
339
|
+
expect(bufferTimestamps.length).toBeGreaterThan(0);
|
|
340
|
+
for (let i = 1; i < bufferTimestamps.length; i++) {
|
|
341
|
+
expect(bufferTimestamps[i]).toBeGreaterThanOrEqual(
|
|
342
|
+
bufferTimestamps[i - 1]!,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("handles concurrent seeks with backward jumps", async ({
|
|
348
|
+
expect,
|
|
349
|
+
inputAtStart,
|
|
350
|
+
}) => {
|
|
351
|
+
await inputAtStart.seek(1, 200);
|
|
352
|
+
|
|
353
|
+
const seek1 = inputAtStart.seek(1, 40);
|
|
354
|
+
const seek2 = inputAtStart.seek(1, 160);
|
|
355
|
+
const seek3 = inputAtStart.seek(1, 0);
|
|
356
|
+
|
|
357
|
+
const [sample1, sample2, sample3] = await Promise.all([
|
|
358
|
+
seek1,
|
|
359
|
+
seek2,
|
|
360
|
+
seek3,
|
|
361
|
+
]);
|
|
362
|
+
|
|
363
|
+
expect(sample1.timestamp).toBe(0.04);
|
|
364
|
+
expect(sample2.timestamp).toBe(0.16);
|
|
365
|
+
expect(sample3.timestamp).toBe(0);
|
|
366
|
+
expect(inputAtStart.getBufferSize(1)).toBeGreaterThan(0);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("handles concurrent seeks to same time", async ({
|
|
370
|
+
expect,
|
|
371
|
+
inputAtStart,
|
|
372
|
+
}) => {
|
|
373
|
+
const seeks = Array(5)
|
|
374
|
+
.fill(null)
|
|
375
|
+
.map(() => inputAtStart.seek(1, 80));
|
|
376
|
+
const results = await Promise.all(seeks);
|
|
377
|
+
|
|
378
|
+
for (const result of results) {
|
|
379
|
+
expect(result.timestamp).toBe(0.08);
|
|
380
|
+
}
|
|
381
|
+
expect(inputAtStart.getBufferSize(1)).toBeGreaterThan(0);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("handles mixed concurrent operations across different tracks", async ({
|
|
385
|
+
expect,
|
|
386
|
+
inputAtStart,
|
|
387
|
+
}) => {
|
|
388
|
+
const track1Seek1 = inputAtStart.seek(1, 40);
|
|
389
|
+
const track1Seek2 = inputAtStart.seek(1, 80);
|
|
390
|
+
|
|
391
|
+
const [result1, result2] = await Promise.all([track1Seek1, track1Seek2]);
|
|
392
|
+
|
|
393
|
+
expect(result1.timestamp).toBe(0.04);
|
|
394
|
+
expect(result2.timestamp).toBe(0.08);
|
|
395
|
+
|
|
396
|
+
const track1Buffer = inputAtStart.getBufferTimestamps(1);
|
|
397
|
+
expect(track1Buffer.length).toBeGreaterThan(0);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AudioSampleSink,
|
|
3
|
+
BufferSource,
|
|
4
|
+
Input,
|
|
5
|
+
MP4,
|
|
6
|
+
VideoSampleSink,
|
|
7
|
+
} from "mediabunny";
|
|
8
|
+
import { type MediaSample, SampleBuffer } from "../SampleBuffer";
|
|
9
|
+
|
|
10
|
+
interface BufferedSeekingInputOptions {
|
|
11
|
+
videoBufferSize?: number;
|
|
12
|
+
audioBufferSize?: number;
|
|
13
|
+
/**
|
|
14
|
+
* FFmpeg start_time offset in milliseconds from the processed video.
|
|
15
|
+
* Applied during seeking to correct for timing shifts introduced by FFmpeg processing.
|
|
16
|
+
*/
|
|
17
|
+
startTimeOffsetMs?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultOptions: BufferedSeekingInputOptions = {
|
|
21
|
+
videoBufferSize: 30,
|
|
22
|
+
audioBufferSize: 100,
|
|
23
|
+
startTimeOffsetMs: 0,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class NoSample extends RangeError {}
|
|
27
|
+
|
|
28
|
+
export class BufferedSeekingInput {
|
|
29
|
+
private input: Input;
|
|
30
|
+
private trackIterators: Map<number, AsyncIterator<MediaSample>> = new Map();
|
|
31
|
+
private trackBuffers: Map<number, SampleBuffer> = new Map();
|
|
32
|
+
private options: BufferedSeekingInputOptions;
|
|
33
|
+
// Separate locks for different operation types to prevent unnecessary blocking
|
|
34
|
+
private trackIteratorCreationPromises: Map<number, Promise<any>> = new Map();
|
|
35
|
+
private trackSeekPromises: Map<number, Promise<any>> = new Map();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* FFmpeg start_time offset in milliseconds from the processed video.
|
|
39
|
+
* Applied during seeking to correct for timing shifts introduced by FFmpeg processing.
|
|
40
|
+
*/
|
|
41
|
+
private readonly startTimeOffsetMs: number;
|
|
42
|
+
|
|
43
|
+
constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions) {
|
|
44
|
+
const bufferSource = new BufferSource(arrayBuffer);
|
|
45
|
+
const input = new Input({
|
|
46
|
+
source: bufferSource,
|
|
47
|
+
formats: [MP4],
|
|
48
|
+
});
|
|
49
|
+
this.input = input;
|
|
50
|
+
this.options = { ...defaultOptions, ...options };
|
|
51
|
+
this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Buffer inspection API for testing
|
|
55
|
+
getBufferSize(trackId: number): number {
|
|
56
|
+
const buffer = this.trackBuffers.get(trackId);
|
|
57
|
+
return buffer ? buffer.length : 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getBufferContents(trackId: number): readonly MediaSample[] {
|
|
61
|
+
const buffer = this.trackBuffers.get(trackId);
|
|
62
|
+
return buffer ? Object.freeze([...buffer.getContents()]) : [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getBufferTimestamps(trackId: number): number[] {
|
|
66
|
+
const contents = this.getBufferContents(trackId);
|
|
67
|
+
return contents.map((sample) => sample.timestamp || 0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
clearBuffer(trackId: number): void {
|
|
71
|
+
const buffer = this.trackBuffers.get(trackId);
|
|
72
|
+
if (buffer) {
|
|
73
|
+
buffer.clear();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
computeDuration() {
|
|
78
|
+
return this.input.computeDuration();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async getTrack(trackId: number) {
|
|
82
|
+
const tracks = await this.input.getTracks();
|
|
83
|
+
const track = tracks.find((track) => track.id === trackId);
|
|
84
|
+
if (!track) {
|
|
85
|
+
throw new Error(`Track ${trackId} not found`);
|
|
86
|
+
}
|
|
87
|
+
return track;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async getAudioTrack(trackId: number) {
|
|
91
|
+
const tracks = await this.input.getAudioTracks();
|
|
92
|
+
const track = tracks.find(
|
|
93
|
+
(track) => track.id === trackId && track.type === "audio",
|
|
94
|
+
);
|
|
95
|
+
if (!track) {
|
|
96
|
+
throw new Error(`Track ${trackId} not found`);
|
|
97
|
+
}
|
|
98
|
+
return track;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async getVideoTrack(trackId: number) {
|
|
102
|
+
const tracks = await this.input.getVideoTracks();
|
|
103
|
+
const track = tracks.find(
|
|
104
|
+
(track) => track.id === trackId && track.type === "video",
|
|
105
|
+
);
|
|
106
|
+
if (!track) {
|
|
107
|
+
throw new Error(`Track ${trackId} not found`);
|
|
108
|
+
}
|
|
109
|
+
return track;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async getFirstVideoTrack() {
|
|
113
|
+
const tracks = await this.input.getVideoTracks();
|
|
114
|
+
return tracks[0];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async getFirstAudioTrack() {
|
|
118
|
+
const tracks = await this.input.getAudioTracks();
|
|
119
|
+
return tracks[0];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async getTrackIterator(trackId: number) {
|
|
123
|
+
if (this.trackIterators.has(trackId)) {
|
|
124
|
+
// biome-ignore lint/style/noNonNullAssertion: we know the map has the key
|
|
125
|
+
return this.trackIterators.get(trackId)!;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Serialize iterator creation per track (but don't block seeks)
|
|
129
|
+
const existingIteratorCreation =
|
|
130
|
+
this.trackIteratorCreationPromises.get(trackId);
|
|
131
|
+
if (existingIteratorCreation) {
|
|
132
|
+
await existingIteratorCreation;
|
|
133
|
+
// Check again after waiting - another operation might have created it
|
|
134
|
+
if (this.trackIterators.has(trackId)) {
|
|
135
|
+
// biome-ignore lint/style/noNonNullAssertion: we know the map has the key
|
|
136
|
+
return this.trackIterators.get(trackId)!;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const creationPromise = this.createIteratorSafe(trackId);
|
|
141
|
+
this.trackIteratorCreationPromises.set(trackId, creationPromise);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
const iterator = await creationPromise;
|
|
145
|
+
return iterator;
|
|
146
|
+
} finally {
|
|
147
|
+
this.trackIteratorCreationPromises.delete(trackId);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async createIteratorSafe(trackId: number) {
|
|
152
|
+
const track = await this.getTrack(trackId);
|
|
153
|
+
if (track.type === "audio") {
|
|
154
|
+
const track = await this.getAudioTrack(trackId);
|
|
155
|
+
const sampleSink = new AudioSampleSink(track);
|
|
156
|
+
const iterator = sampleSink.samples();
|
|
157
|
+
this.trackIterators.set(trackId, iterator);
|
|
158
|
+
return iterator;
|
|
159
|
+
}
|
|
160
|
+
{
|
|
161
|
+
const track = await this.getVideoTrack(trackId);
|
|
162
|
+
const sampleSink = new VideoSampleSink(track);
|
|
163
|
+
const iterator = sampleSink.samples();
|
|
164
|
+
this.trackIterators.set(trackId, iterator);
|
|
165
|
+
return iterator;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async createTrackBuffer(trackId: number) {
|
|
170
|
+
const track = await this.getTrack(trackId);
|
|
171
|
+
if (track.type === "audio") {
|
|
172
|
+
const bufferSize = this.options.audioBufferSize;
|
|
173
|
+
this.trackBuffers.set(trackId, new SampleBuffer(bufferSize));
|
|
174
|
+
} else {
|
|
175
|
+
const bufferSize = this.options.videoBufferSize;
|
|
176
|
+
this.trackBuffers.set(trackId, new SampleBuffer(bufferSize));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async seek(trackId: number, timeMs: number) {
|
|
181
|
+
// Apply the start time offset to deal with files that don't start on the zero
|
|
182
|
+
const correctedTimeMs = timeMs + this.startTimeOffsetMs;
|
|
183
|
+
|
|
184
|
+
// Serialize seek operations per track (but don't block iterator creation)
|
|
185
|
+
const existingSeek = this.trackSeekPromises.get(trackId);
|
|
186
|
+
if (existingSeek) {
|
|
187
|
+
await existingSeek;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const seekPromise = this.seekSafe(trackId, correctedTimeMs);
|
|
191
|
+
this.trackSeekPromises.set(trackId, seekPromise);
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
return await seekPromise;
|
|
195
|
+
} finally {
|
|
196
|
+
this.trackSeekPromises.delete(trackId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async resetIterator(trackId: number) {
|
|
201
|
+
const trackBuffer = this.trackBuffers.get(trackId);
|
|
202
|
+
trackBuffer?.clear();
|
|
203
|
+
// Clean up iterator safely - wait for any ongoing iterator creation
|
|
204
|
+
const ongoingIteratorCreation =
|
|
205
|
+
this.trackIteratorCreationPromises.get(trackId);
|
|
206
|
+
if (ongoingIteratorCreation) {
|
|
207
|
+
await ongoingIteratorCreation;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const iterator = this.trackIterators.get(trackId);
|
|
211
|
+
if (iterator) {
|
|
212
|
+
try {
|
|
213
|
+
await iterator.return?.();
|
|
214
|
+
} catch (_error) {
|
|
215
|
+
// Iterator cleanup failed, continue anyway
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
this.trackIterators.delete(trackId);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private async seekSafe(trackId: number, timeMs: number) {
|
|
222
|
+
// Get or create track-specific buffer
|
|
223
|
+
if (!this.trackBuffers.has(trackId)) {
|
|
224
|
+
await this.createTrackBuffer(trackId);
|
|
225
|
+
}
|
|
226
|
+
// biome-ignore lint/style/noNonNullAssertion: we know the map has the key
|
|
227
|
+
const trackBuffer = this.trackBuffers.get(trackId)!;
|
|
228
|
+
|
|
229
|
+
if (timeMs < trackBuffer.firstTimestamp * 1000) {
|
|
230
|
+
await this.resetIterator(trackId);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const alreadyInBuffer = trackBuffer.find(timeMs);
|
|
234
|
+
const track = await this.getTrack(trackId);
|
|
235
|
+
|
|
236
|
+
// Early validation: check if seek time is outside track bounds
|
|
237
|
+
const firstTimestampMs = (await track.getFirstTimestamp()) * 1000;
|
|
238
|
+
const lastSampleEndMs = (await track.computeDuration()) * 1000; // computeDuration returns end time of last sample
|
|
239
|
+
|
|
240
|
+
if (timeMs < firstTimestampMs || timeMs >= lastSampleEndMs) {
|
|
241
|
+
throw new NoSample(
|
|
242
|
+
`Seek time ${timeMs}ms is outside track range [${firstTimestampMs}ms, ${lastSampleEndMs}ms]`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (alreadyInBuffer) return alreadyInBuffer;
|
|
247
|
+
|
|
248
|
+
const iterator = await this.getTrackIterator(trackId);
|
|
249
|
+
while (true) {
|
|
250
|
+
const { done, value: decodedSample } = await iterator.next();
|
|
251
|
+
if (decodedSample) {
|
|
252
|
+
trackBuffer.push(decodedSample);
|
|
253
|
+
}
|
|
254
|
+
const foundSample = trackBuffer.find(timeMs);
|
|
255
|
+
if (foundSample) {
|
|
256
|
+
return foundSample;
|
|
257
|
+
}
|
|
258
|
+
if (done) {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
throw new NoSample(
|
|
264
|
+
`Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|