@editframe/elements 0.17.6-beta.0 → 0.18.7-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/dist/EF_FRAMEGEN.js +1 -1
- package/dist/elements/EFAudio.d.ts +21 -2
- package/dist/elements/EFAudio.js +41 -11
- package/dist/elements/EFImage.d.ts +1 -0
- package/dist/elements/EFImage.js +11 -3
- package/dist/elements/EFMedia/AssetIdMediaEngine.d.ts +18 -0
- package/dist/elements/EFMedia/AssetIdMediaEngine.js +41 -0
- package/dist/elements/EFMedia/AssetMediaEngine.browsertest.d.ts +0 -0
- package/dist/elements/EFMedia/AssetMediaEngine.d.ts +45 -0
- package/dist/elements/EFMedia/AssetMediaEngine.js +135 -0
- package/dist/elements/EFMedia/BaseMediaEngine.d.ts +55 -0
- package/dist/elements/EFMedia/BaseMediaEngine.js +115 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.d.ts +43 -0
- package/dist/elements/EFMedia/BufferedSeekingInput.js +179 -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 +81 -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 +141 -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 +30 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.d.ts +0 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.d.ts +7 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +32 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +28 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.d.ts +4 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +17 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.d.ts +3 -0
- package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +107 -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/PrecisionUtils.d.ts +28 -0
- package/dist/elements/EFMedia/shared/PrecisionUtils.js +29 -0
- package/dist/elements/EFMedia/shared/RenditionHelpers.d.ts +19 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.d.ts +18 -0
- package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +60 -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 +34 -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 +28 -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 +17 -0
- package/dist/elements/EFMedia.browsertest.d.ts +1 -0
- package/dist/elements/EFMedia.d.ts +63 -111
- package/dist/elements/EFMedia.js +117 -1113
- package/dist/elements/EFTemporal.d.ts +1 -1
- package/dist/elements/EFTemporal.js +1 -1
- package/dist/elements/EFTimegroup.d.ts +11 -0
- package/dist/elements/EFTimegroup.js +83 -13
- package/dist/elements/EFVideo.d.ts +54 -32
- package/dist/elements/EFVideo.js +100 -207
- package/dist/elements/EFWaveform.js +2 -2
- package/dist/elements/SampleBuffer.d.ts +14 -0
- package/dist/elements/SampleBuffer.js +52 -0
- package/dist/getRenderInfo.js +2 -1
- package/dist/gui/ContextMixin.js +3 -2
- package/dist/gui/EFFilmstrip.d.ts +3 -3
- package/dist/gui/EFFilmstrip.js +1 -1
- package/dist/gui/EFFitScale.d.ts +2 -2
- package/dist/gui/TWMixin.js +1 -1
- package/dist/style.css +1 -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 +3 -3
- package/src/elements/EFAudio.browsertest.ts +189 -49
- package/src/elements/EFAudio.ts +59 -13
- package/src/elements/EFImage.browsertest.ts +42 -0
- 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.browsertest.ts +100 -0
- package/src/elements/EFMedia/AssetMediaEngine.ts +255 -0
- package/src/elements/EFMedia/BaseMediaEngine.test.ts +164 -0
- package/src/elements/EFMedia/BaseMediaEngine.ts +219 -0
- package/src/elements/EFMedia/BufferedSeekingInput.browsertest.ts +481 -0
- package/src/elements/EFMedia/BufferedSeekingInput.ts +324 -0
- package/src/elements/EFMedia/JitMediaEngine.browsertest.ts +165 -0
- package/src/elements/EFMedia/JitMediaEngine.ts +166 -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 +250 -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 +43 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.chunkboundary.regression.browsertest.ts +199 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSeekTask.ts +64 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.ts +45 -0
- package/src/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.ts +24 -0
- package/src/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.ts +183 -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/PrecisionUtils.ts +46 -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 +68 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.browsertest.ts +57 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentFetchTask.ts +43 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.browsertest.ts +56 -0
- package/src/elements/EFMedia/videoTasks/makeVideoSegmentIdTask.ts +24 -0
- package/src/elements/EFMedia.browsertest.ts +706 -273
- package/src/elements/EFMedia.ts +136 -1769
- package/src/elements/EFTemporal.ts +3 -4
- package/src/elements/EFTimegroup.browsertest.ts +6 -3
- package/src/elements/EFTimegroup.ts +147 -21
- package/src/elements/EFVideo.browsertest.ts +980 -169
- package/src/elements/EFVideo.ts +113 -458
- package/src/elements/EFWaveform.ts +1 -1
- package/src/elements/MediaController.ts +2 -12
- package/src/elements/SampleBuffer.ts +95 -0
- package/src/gui/ContextMixin.ts +3 -6
- 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 +269 -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 +39 -30
- 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_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_1_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__9ed2d25c675aa6bb6ff5b3ae23887c71/metadata.json +22 -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_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_2_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__d5a3309a2bf756dd6e304807eb402f56/metadata.json +22 -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_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_audio_3_mp4_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4_bytes_0__773254bb671e3466fca8677139fb239e/metadata.json +22 -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_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_4_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a6fb05a22b18d850f7f2950bbcdbdeed/metadata.json +21 -0
- package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/data.bin +0 -0
- package/test/__cache__/GET__api_v1_transcode_high_5_m4s_url_http_3A_2F_2Fweb_3A3000_2Fhead_moov_480p_mp4__a50058c7c3602e90879fe3428ed891f4/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 +320 -188
- package/test/recordReplayProxyPlugin.js +352 -0
- package/test/useAssetMSW.ts +1 -1
- package/test/useMSW.ts +35 -22
- package/types.json +1 -1
- package/dist/JitTranscodingClient.d.ts +0 -167
- package/dist/JitTranscodingClient.js +0 -373
- package/dist/ScrubTrackManager.d.ts +0 -96
- package/dist/ScrubTrackManager.js +0 -216
- package/dist/elements/printTaskStatus.js +0 -11
- package/src/elements/__screenshots__/EFMedia.browsertest.ts/EFMedia-JIT-audio-playback-audioBufferTask-should-work-in-JIT-mode-without-URL-errors-1.png +0 -0
- package/test/EFVideo.frame-tasks.browsertest.ts +0 -524
- /package/dist/{DecoderResetFrequency.test.d.ts → elements/EFMedia/AssetIdMediaEngine.test.d.ts} +0 -0
- /package/dist/{DecoderResetRecovery.test.d.ts → elements/EFMedia/BaseMediaEngine.test.d.ts} +0 -0
- /package/dist/{JitTranscodingClient.browsertest.d.ts → elements/EFMedia/BufferedSeekingInput.browsertest.d.ts} +0 -0
- /package/dist/{JitTranscodingClient.test.d.ts → elements/EFMedia/shared/RenditionHelpers.browsertest.d.ts} +0 -0
- /package/dist/{ScrubTrackIntegration.test.d.ts → elements/EFMedia/tasks/makeMediaEngineTask.browsertest.d.ts} +0 -0
- /package/dist/{SegmentSwitchLoading.test.d.ts → elements/EFMedia/tasks/makeMediaEngineTask.test.d.ts} +0 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AudioSampleSink,
|
|
3
|
+
BufferSource,
|
|
4
|
+
Input,
|
|
5
|
+
MP4,
|
|
6
|
+
VideoSampleSink,
|
|
7
|
+
} from "mediabunny";
|
|
8
|
+
import { type MediaSample, SampleBuffer } from "../SampleBuffer";
|
|
9
|
+
import { roundToMilliseconds } from "./shared/PrecisionUtils";
|
|
10
|
+
|
|
11
|
+
interface BufferedSeekingInputOptions {
|
|
12
|
+
videoBufferSize?: number;
|
|
13
|
+
audioBufferSize?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Timeline offset in milliseconds to map user timeline to media timeline.
|
|
16
|
+
* Applied during seeking to handle media that doesn't start at 0ms.
|
|
17
|
+
*/
|
|
18
|
+
startTimeOffsetMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultOptions: BufferedSeekingInputOptions = {
|
|
22
|
+
videoBufferSize: 30,
|
|
23
|
+
audioBufferSize: 100,
|
|
24
|
+
startTimeOffsetMs: 0,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class NoSample extends RangeError {}
|
|
28
|
+
|
|
29
|
+
export class BufferedSeekingInput {
|
|
30
|
+
private input: Input;
|
|
31
|
+
private trackIterators: Map<number, AsyncIterator<MediaSample>> = new Map();
|
|
32
|
+
private trackBuffers: Map<number, SampleBuffer> = new Map();
|
|
33
|
+
private options: BufferedSeekingInputOptions;
|
|
34
|
+
// Separate locks for different operation types to prevent unnecessary blocking
|
|
35
|
+
private trackIteratorCreationPromises: Map<number, Promise<any>> = new Map();
|
|
36
|
+
private trackSeekPromises: Map<number, Promise<any>> = new Map();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Timeline offset in milliseconds to map user timeline to media timeline.
|
|
40
|
+
* Applied during seeking to handle media that doesn't start at 0ms.
|
|
41
|
+
*/
|
|
42
|
+
private readonly startTimeOffsetMs: number;
|
|
43
|
+
|
|
44
|
+
constructor(arrayBuffer: ArrayBuffer, options?: BufferedSeekingInputOptions) {
|
|
45
|
+
const bufferSource = new BufferSource(arrayBuffer);
|
|
46
|
+
const input = new Input({
|
|
47
|
+
source: bufferSource,
|
|
48
|
+
formats: [MP4],
|
|
49
|
+
});
|
|
50
|
+
this.input = input;
|
|
51
|
+
this.options = { ...defaultOptions, ...options };
|
|
52
|
+
this.startTimeOffsetMs = this.options.startTimeOffsetMs ?? 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Buffer inspection API for testing
|
|
56
|
+
getBufferSize(trackId: number): number {
|
|
57
|
+
const buffer = this.trackBuffers.get(trackId);
|
|
58
|
+
return buffer ? buffer.length : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getBufferContents(trackId: number): readonly MediaSample[] {
|
|
62
|
+
const buffer = this.trackBuffers.get(trackId);
|
|
63
|
+
return buffer ? Object.freeze([...buffer.getContents()]) : [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getBufferTimestamps(trackId: number): number[] {
|
|
67
|
+
const contents = this.getBufferContents(trackId);
|
|
68
|
+
return contents.map((sample) => sample.timestamp || 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clearBuffer(trackId: number): void {
|
|
72
|
+
const buffer = this.trackBuffers.get(trackId);
|
|
73
|
+
if (buffer) {
|
|
74
|
+
buffer.clear();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
computeDuration() {
|
|
79
|
+
return this.input.computeDuration();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getTrack(trackId: number) {
|
|
83
|
+
const tracks = await this.input.getTracks();
|
|
84
|
+
const track = tracks.find((track) => track.id === trackId);
|
|
85
|
+
if (!track) {
|
|
86
|
+
throw new Error(`Track ${trackId} not found`);
|
|
87
|
+
}
|
|
88
|
+
return track;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async getAudioTrack(trackId: number) {
|
|
92
|
+
const tracks = await this.input.getAudioTracks();
|
|
93
|
+
const track = tracks.find(
|
|
94
|
+
(track) => track.id === trackId && track.type === "audio",
|
|
95
|
+
);
|
|
96
|
+
if (!track) {
|
|
97
|
+
throw new Error(`Track ${trackId} not found`);
|
|
98
|
+
}
|
|
99
|
+
return track;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getVideoTrack(trackId: number) {
|
|
103
|
+
const tracks = await this.input.getVideoTracks();
|
|
104
|
+
const track = tracks.find(
|
|
105
|
+
(track) => track.id === trackId && track.type === "video",
|
|
106
|
+
);
|
|
107
|
+
if (!track) {
|
|
108
|
+
throw new Error(`Track ${trackId} not found`);
|
|
109
|
+
}
|
|
110
|
+
return track;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async getFirstVideoTrack() {
|
|
114
|
+
const tracks = await this.input.getVideoTracks();
|
|
115
|
+
return tracks[0];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async getFirstAudioTrack() {
|
|
119
|
+
const tracks = await this.input.getAudioTracks();
|
|
120
|
+
return tracks[0];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async getTrackIterator(trackId: number) {
|
|
124
|
+
if (this.trackIterators.has(trackId)) {
|
|
125
|
+
// biome-ignore lint/style/noNonNullAssertion: we know the map has the key
|
|
126
|
+
return this.trackIterators.get(trackId)!;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Serialize iterator creation per track (but don't block seeks)
|
|
130
|
+
const existingIteratorCreation =
|
|
131
|
+
this.trackIteratorCreationPromises.get(trackId);
|
|
132
|
+
if (existingIteratorCreation) {
|
|
133
|
+
await existingIteratorCreation;
|
|
134
|
+
// Check again after waiting - another operation might have created it
|
|
135
|
+
if (this.trackIterators.has(trackId)) {
|
|
136
|
+
// biome-ignore lint/style/noNonNullAssertion: we know the map has the key
|
|
137
|
+
return this.trackIterators.get(trackId)!;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const creationPromise = this.createIteratorSafe(trackId);
|
|
142
|
+
this.trackIteratorCreationPromises.set(trackId, creationPromise);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const iterator = await creationPromise;
|
|
146
|
+
return iterator;
|
|
147
|
+
} finally {
|
|
148
|
+
this.trackIteratorCreationPromises.delete(trackId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private async createIteratorSafe(trackId: number) {
|
|
153
|
+
const track = await this.getTrack(trackId);
|
|
154
|
+
if (track.type === "audio") {
|
|
155
|
+
const track = await this.getAudioTrack(trackId);
|
|
156
|
+
const sampleSink = new AudioSampleSink(track);
|
|
157
|
+
const iterator = sampleSink.samples();
|
|
158
|
+
this.trackIterators.set(trackId, iterator);
|
|
159
|
+
return iterator;
|
|
160
|
+
}
|
|
161
|
+
{
|
|
162
|
+
const track = await this.getVideoTrack(trackId);
|
|
163
|
+
const sampleSink = new VideoSampleSink(track);
|
|
164
|
+
const iterator = sampleSink.samples();
|
|
165
|
+
this.trackIterators.set(trackId, iterator);
|
|
166
|
+
return iterator;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async createTrackBuffer(trackId: number) {
|
|
171
|
+
const track = await this.getTrack(trackId);
|
|
172
|
+
if (track.type === "audio") {
|
|
173
|
+
const bufferSize = this.options.audioBufferSize;
|
|
174
|
+
this.trackBuffers.set(trackId, new SampleBuffer(bufferSize));
|
|
175
|
+
} else {
|
|
176
|
+
const bufferSize = this.options.videoBufferSize;
|
|
177
|
+
this.trackBuffers.set(trackId, new SampleBuffer(bufferSize));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async seek(trackId: number, timeMs: number) {
|
|
182
|
+
// Apply timeline offset to map user timeline to media timeline
|
|
183
|
+
const mediaTimeMs = timeMs + this.startTimeOffsetMs;
|
|
184
|
+
|
|
185
|
+
// Round using consistent precision handling
|
|
186
|
+
const roundedMediaTimeMs = roundToMilliseconds(mediaTimeMs);
|
|
187
|
+
|
|
188
|
+
// Serialize seek operations per track (but don't block iterator creation)
|
|
189
|
+
const existingSeek = this.trackSeekPromises.get(trackId);
|
|
190
|
+
if (existingSeek) {
|
|
191
|
+
await existingSeek;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const seekPromise = this.seekSafe(trackId, roundedMediaTimeMs);
|
|
195
|
+
this.trackSeekPromises.set(trackId, seekPromise);
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
return await seekPromise;
|
|
199
|
+
} finally {
|
|
200
|
+
this.trackSeekPromises.delete(trackId);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private async resetIterator(trackId: number) {
|
|
205
|
+
const trackBuffer = this.trackBuffers.get(trackId);
|
|
206
|
+
trackBuffer?.clear();
|
|
207
|
+
// Clean up iterator safely - wait for any ongoing iterator creation
|
|
208
|
+
const ongoingIteratorCreation =
|
|
209
|
+
this.trackIteratorCreationPromises.get(trackId);
|
|
210
|
+
if (ongoingIteratorCreation) {
|
|
211
|
+
await ongoingIteratorCreation;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const iterator = this.trackIterators.get(trackId);
|
|
215
|
+
if (iterator) {
|
|
216
|
+
try {
|
|
217
|
+
await iterator.return?.();
|
|
218
|
+
} catch (_error) {
|
|
219
|
+
// Iterator cleanup failed, continue anyway
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
this.trackIterators.delete(trackId);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async seekSafe(trackId: number, timeMs: number) {
|
|
226
|
+
// Get or create track-specific buffer
|
|
227
|
+
if (!this.trackBuffers.has(trackId)) {
|
|
228
|
+
await this.createTrackBuffer(trackId);
|
|
229
|
+
}
|
|
230
|
+
// biome-ignore lint/style/noNonNullAssertion: we know the map has the key
|
|
231
|
+
const trackBuffer = this.trackBuffers.get(trackId)!;
|
|
232
|
+
|
|
233
|
+
const track = await this.getTrack(trackId);
|
|
234
|
+
|
|
235
|
+
// Early validation: check if seek time is outside track bounds
|
|
236
|
+
// Use consistent precision handling throughout
|
|
237
|
+
const firstTimestampMs = roundToMilliseconds(
|
|
238
|
+
(await track.getFirstTimestamp()) * 1000,
|
|
239
|
+
);
|
|
240
|
+
let roundedTimeMs = roundToMilliseconds(timeMs);
|
|
241
|
+
|
|
242
|
+
// During rapid scrubbing, track.computeDuration() may only return the duration
|
|
243
|
+
// of currently loaded segments. Only validate against the start time, as the
|
|
244
|
+
// end time may not be accurate until all segments are loaded.
|
|
245
|
+
if (roundedTimeMs < firstTimestampMs) {
|
|
246
|
+
// GRACEFUL HANDLING: During rapid seeking, tasks can complete out of order, causing
|
|
247
|
+
// the audio buffer to contain segments for a different time range than the seek target.
|
|
248
|
+
// Only apply graceful adjustment if we have buffer contents that suggest a race condition.
|
|
249
|
+
// For empty buffers, allow normal seeking to proceed which may load the appropriate segments.
|
|
250
|
+
|
|
251
|
+
const bufferContents = trackBuffer.getContents();
|
|
252
|
+
|
|
253
|
+
if (bufferContents.length > 0) {
|
|
254
|
+
// We have loaded segments but they're for a different time range - adjust gracefully
|
|
255
|
+
timeMs = firstTimestampMs;
|
|
256
|
+
roundedTimeMs = roundToMilliseconds(timeMs);
|
|
257
|
+
} else {
|
|
258
|
+
// Empty buffer - let normal seeking proceed to load appropriate segments
|
|
259
|
+
// This maintains normal seeking behavior for tests and initial loads
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Note: If seeking beyond currently loaded segments, allow it to proceed
|
|
264
|
+
// The segment loading logic will handle fetching the needed segments
|
|
265
|
+
// No logging needed as this is a normal part of seeking behavior
|
|
266
|
+
|
|
267
|
+
// Check if we need to reset iterator for seeks outside current buffer range
|
|
268
|
+
const bufferContents = trackBuffer.getContents();
|
|
269
|
+
if (bufferContents.length > 0) {
|
|
270
|
+
const bufferStartMs = roundToMilliseconds(
|
|
271
|
+
trackBuffer.firstTimestamp * 1000,
|
|
272
|
+
);
|
|
273
|
+
const lastSample = bufferContents[bufferContents.length - 1];
|
|
274
|
+
const bufferEndMs = lastSample
|
|
275
|
+
? roundToMilliseconds(
|
|
276
|
+
(lastSample.timestamp + (lastSample.duration || 0)) * 1000,
|
|
277
|
+
)
|
|
278
|
+
: bufferStartMs;
|
|
279
|
+
|
|
280
|
+
// If seeking outside current buffer range, reset iterator to load appropriate data
|
|
281
|
+
if (roundedTimeMs < bufferStartMs || roundedTimeMs > bufferEndMs) {
|
|
282
|
+
await this.resetIterator(trackId);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const alreadyInBuffer = trackBuffer.find(timeMs);
|
|
287
|
+
if (alreadyInBuffer) return alreadyInBuffer;
|
|
288
|
+
|
|
289
|
+
const iterator = await this.getTrackIterator(trackId);
|
|
290
|
+
while (true) {
|
|
291
|
+
const { done, value: decodedSample } = await iterator.next();
|
|
292
|
+
if (decodedSample) {
|
|
293
|
+
trackBuffer.push(decodedSample);
|
|
294
|
+
}
|
|
295
|
+
const foundSample = trackBuffer.find(timeMs);
|
|
296
|
+
if (foundSample) {
|
|
297
|
+
return foundSample;
|
|
298
|
+
}
|
|
299
|
+
if (done) {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// If no exact sample found and we've reached the end of the track,
|
|
305
|
+
// check if the seek time is beyond the actual track duration.
|
|
306
|
+
// If so, return the last available sample instead of throwing an error.
|
|
307
|
+
const finalBufferContents = trackBuffer.getContents();
|
|
308
|
+
if (finalBufferContents.length > 0) {
|
|
309
|
+
const lastSample = finalBufferContents[finalBufferContents.length - 1];
|
|
310
|
+
const lastSampleEndMs = roundToMilliseconds(
|
|
311
|
+
((lastSample?.timestamp || 0) + (lastSample?.duration || 0)) * 1000,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// If seeking past the last sample, return the last sample silently
|
|
315
|
+
if (roundToMilliseconds(timeMs) >= lastSampleEndMs) {
|
|
316
|
+
return lastSample;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
throw new NoSample(
|
|
321
|
+
`Sample not found for time ${timeMs} in ${track.type} track ${trackId}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe } from "vitest";
|
|
2
|
+
import { test as baseTest } from "../../../test/useMSW.js";
|
|
3
|
+
|
|
4
|
+
import type { ManifestResponse } from "../../transcoding/types/index.js";
|
|
5
|
+
import { UrlGenerator } from "../../transcoding/utils/UrlGenerator";
|
|
6
|
+
import "../EFVideo.js";
|
|
7
|
+
import type { EFVideo } from "../EFVideo.js";
|
|
8
|
+
import { JitMediaEngine } from "./JitMediaEngine";
|
|
9
|
+
|
|
10
|
+
const test = baseTest.extend<{
|
|
11
|
+
emptyManifestResponse: ManifestResponse;
|
|
12
|
+
urlGenerator: UrlGenerator;
|
|
13
|
+
manifestUrl: string;
|
|
14
|
+
mediaEngine: JitMediaEngine;
|
|
15
|
+
abortSignal: AbortSignal;
|
|
16
|
+
testUrl: string;
|
|
17
|
+
host: EFVideo;
|
|
18
|
+
}>({
|
|
19
|
+
mediaEngine: async ({ manifestUrl, urlGenerator, host }, use: any) => {
|
|
20
|
+
const engine = await JitMediaEngine.fetch(host, urlGenerator, manifestUrl);
|
|
21
|
+
await use(engine);
|
|
22
|
+
},
|
|
23
|
+
manifestUrl: async ({ urlGenerator, host }, use: any) => {
|
|
24
|
+
const url = urlGenerator.generateManifestUrl(host.src);
|
|
25
|
+
await use(url);
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
emptyManifestResponse: async ({}, use: any) => {
|
|
29
|
+
const emptyResponse: ManifestResponse = {
|
|
30
|
+
version: "1.0",
|
|
31
|
+
type: "cmaf",
|
|
32
|
+
duration: 60,
|
|
33
|
+
durationMs: 60000,
|
|
34
|
+
segmentDuration: 4000,
|
|
35
|
+
baseUrl: "http://api.example.com/",
|
|
36
|
+
sourceUrl: "http://example.com/video.mp4",
|
|
37
|
+
audioRenditions: [],
|
|
38
|
+
videoRenditions: [],
|
|
39
|
+
endpoints: {
|
|
40
|
+
initSegment: "http://api.example.com/init/{renditionId}",
|
|
41
|
+
mediaSegment:
|
|
42
|
+
"http://api.example.com/segment/{segmentId}/{renditionId}",
|
|
43
|
+
},
|
|
44
|
+
jitInfo: {
|
|
45
|
+
parallelTranscodingSupported: true,
|
|
46
|
+
expectedTranscodeLatency: 1000,
|
|
47
|
+
segmentCount: 15,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
await use(emptyResponse);
|
|
51
|
+
},
|
|
52
|
+
host: async ({}, use: any) => {
|
|
53
|
+
const configuration = document.createElement("ef-configuration");
|
|
54
|
+
// Use integrated proxy server (same host/port as test runner)
|
|
55
|
+
const apiHost = `${window.location.protocol}//${window.location.host}`;
|
|
56
|
+
configuration.setAttribute("api-host", apiHost);
|
|
57
|
+
configuration.apiHost = apiHost;
|
|
58
|
+
const host = document.createElement("ef-video");
|
|
59
|
+
configuration.appendChild(host);
|
|
60
|
+
host.src = "http://web:3000/head-moov-480p.mp4";
|
|
61
|
+
await use(host);
|
|
62
|
+
},
|
|
63
|
+
urlGenerator: async ({}, use: any) => {
|
|
64
|
+
// UrlGenerator points to integrated proxy server (same host/port as test runner)
|
|
65
|
+
const apiHost = `${window.location.protocol}//${window.location.host}`;
|
|
66
|
+
const generator = new UrlGenerator(() => apiHost);
|
|
67
|
+
await use(generator);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
abortSignal: async ({}, use: any) => {
|
|
71
|
+
const signal = new AbortController().signal;
|
|
72
|
+
await use(signal);
|
|
73
|
+
},
|
|
74
|
+
testUrl: async ({}, use: any) => {
|
|
75
|
+
const url = "http://api.example.com/manifest";
|
|
76
|
+
await use(url);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("JitMediaEngine", () => {
|
|
81
|
+
test("provides duration from manifest data", async ({
|
|
82
|
+
mediaEngine,
|
|
83
|
+
expect,
|
|
84
|
+
}) => {
|
|
85
|
+
expect(mediaEngine.durationMs).toBe(10000);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("provides source URL from manifest data", async ({
|
|
89
|
+
mediaEngine,
|
|
90
|
+
host,
|
|
91
|
+
expect,
|
|
92
|
+
}) => {
|
|
93
|
+
expect(mediaEngine.src).toBe(host.src);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("returns audio rendition with correct properties", ({
|
|
97
|
+
mediaEngine,
|
|
98
|
+
host,
|
|
99
|
+
expect,
|
|
100
|
+
}) => {
|
|
101
|
+
const audioRendition = mediaEngine.audioRendition;
|
|
102
|
+
|
|
103
|
+
expect(audioRendition).toBeDefined();
|
|
104
|
+
expect(audioRendition!.id).toBe("audio");
|
|
105
|
+
expect(audioRendition!.trackId).toBeUndefined();
|
|
106
|
+
expect(audioRendition!.src).toBe(host.src);
|
|
107
|
+
expect(audioRendition!.segmentDurationMs).toBe(2000);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("returns undefined audio rendition when none available", ({
|
|
111
|
+
urlGenerator,
|
|
112
|
+
emptyManifestResponse,
|
|
113
|
+
host,
|
|
114
|
+
expect,
|
|
115
|
+
}) => {
|
|
116
|
+
const engine = new JitMediaEngine(
|
|
117
|
+
host,
|
|
118
|
+
urlGenerator,
|
|
119
|
+
emptyManifestResponse,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(engine.audioRendition).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("returns video rendition with correct properties", ({
|
|
126
|
+
mediaEngine,
|
|
127
|
+
host,
|
|
128
|
+
expect,
|
|
129
|
+
}) => {
|
|
130
|
+
const videoRendition = mediaEngine.videoRendition;
|
|
131
|
+
|
|
132
|
+
expect(videoRendition).toBeDefined();
|
|
133
|
+
expect(videoRendition!.id).toBe("high");
|
|
134
|
+
expect(videoRendition!.trackId).toBeUndefined();
|
|
135
|
+
expect(videoRendition!.src).toBe(host.src);
|
|
136
|
+
expect(videoRendition!.segmentDurationMs).toBe(2000);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("returns undefined video rendition when none available", ({
|
|
140
|
+
urlGenerator,
|
|
141
|
+
emptyManifestResponse,
|
|
142
|
+
host,
|
|
143
|
+
expect,
|
|
144
|
+
}) => {
|
|
145
|
+
const engine = new JitMediaEngine(
|
|
146
|
+
host,
|
|
147
|
+
urlGenerator,
|
|
148
|
+
emptyManifestResponse,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(engine.videoRendition).toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("provides templates from manifest endpoints", ({
|
|
155
|
+
mediaEngine,
|
|
156
|
+
expect,
|
|
157
|
+
}) => {
|
|
158
|
+
expect(mediaEngine.templates).toEqual({
|
|
159
|
+
initSegment:
|
|
160
|
+
"http://localhost:63315/api/v1/transcode/{rendition}/init.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
161
|
+
mediaSegment:
|
|
162
|
+
"http://localhost:63315/api/v1/transcode/{rendition}/{segmentId}.m4s?url=http%3A%2F%2Fweb%3A3000%2Fhead-moov-480p.mp4",
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AudioRendition,
|
|
3
|
+
MediaEngine,
|
|
4
|
+
RenditionId,
|
|
5
|
+
VideoRendition,
|
|
6
|
+
} from "../../transcoding/types";
|
|
7
|
+
import type { ManifestResponse } from "../../transcoding/types/index.js";
|
|
8
|
+
import type { UrlGenerator } from "../../transcoding/utils/UrlGenerator";
|
|
9
|
+
import type { EFMedia } from "../EFMedia.js";
|
|
10
|
+
import { BaseMediaEngine } from "./BaseMediaEngine";
|
|
11
|
+
|
|
12
|
+
export class JitMediaEngine extends BaseMediaEngine implements MediaEngine {
|
|
13
|
+
static async fetch(host: EFMedia, urlGenerator: UrlGenerator, url: string) {
|
|
14
|
+
const response = await host.fetch(url);
|
|
15
|
+
const data = (await response.json()) as ManifestResponse;
|
|
16
|
+
return new JitMediaEngine(host, urlGenerator, data);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
public host: EFMedia,
|
|
21
|
+
private urlGenerator: UrlGenerator,
|
|
22
|
+
private data: ManifestResponse,
|
|
23
|
+
) {
|
|
24
|
+
super();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get durationMs() {
|
|
28
|
+
return this.data.durationMs;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get src() {
|
|
32
|
+
return this.data.sourceUrl;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get audioRendition(): AudioRendition | undefined {
|
|
36
|
+
const rendition = this.data.audioRenditions[0];
|
|
37
|
+
|
|
38
|
+
if (!rendition) return undefined;
|
|
39
|
+
return {
|
|
40
|
+
id: rendition.id as RenditionId,
|
|
41
|
+
trackId: undefined,
|
|
42
|
+
src: this.data.sourceUrl,
|
|
43
|
+
segmentDurationMs: rendition.segmentDurationMs,
|
|
44
|
+
segmentDurationsMs: rendition.segmentDurationsMs,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get videoRendition(): VideoRendition | undefined {
|
|
49
|
+
const rendition = this.data.videoRenditions[0];
|
|
50
|
+
|
|
51
|
+
if (!rendition) return undefined;
|
|
52
|
+
return {
|
|
53
|
+
id: rendition.id as RenditionId,
|
|
54
|
+
trackId: undefined,
|
|
55
|
+
src: this.data.sourceUrl,
|
|
56
|
+
segmentDurationMs: rendition.segmentDurationMs,
|
|
57
|
+
segmentDurationsMs: rendition.segmentDurationsMs,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get templates() {
|
|
62
|
+
return this.data.endpoints;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async fetchInitSegment(
|
|
66
|
+
rendition: { id?: RenditionId; trackId: number | undefined; src: string },
|
|
67
|
+
signal: AbortSignal,
|
|
68
|
+
) {
|
|
69
|
+
if (!rendition.id) {
|
|
70
|
+
throw new Error("Rendition ID is required for JIT metadata");
|
|
71
|
+
}
|
|
72
|
+
const url = this.urlGenerator.generateSegmentUrl(
|
|
73
|
+
"init",
|
|
74
|
+
rendition.id,
|
|
75
|
+
this,
|
|
76
|
+
);
|
|
77
|
+
const response = await this.host.fetch(url, { signal });
|
|
78
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
79
|
+
return arrayBuffer;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async fetchMediaSegmentImpl(
|
|
83
|
+
segmentId: number,
|
|
84
|
+
rendition: { id?: RenditionId; trackId: number | undefined; src: string },
|
|
85
|
+
) {
|
|
86
|
+
if (!rendition.id) {
|
|
87
|
+
throw new Error("Rendition ID is required for JIT metadata");
|
|
88
|
+
}
|
|
89
|
+
const url = this.urlGenerator.generateSegmentUrl(
|
|
90
|
+
segmentId,
|
|
91
|
+
rendition.id,
|
|
92
|
+
this,
|
|
93
|
+
);
|
|
94
|
+
return this.fetchMediaCache(url);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
computeSegmentId(
|
|
98
|
+
desiredSeekTimeMs: number,
|
|
99
|
+
rendition: VideoRendition | AudioRendition,
|
|
100
|
+
) {
|
|
101
|
+
// Don't request segments beyond the actual file duration
|
|
102
|
+
// Note: seeking to exactly durationMs should be allowed (it's the last moment of the file)
|
|
103
|
+
if (desiredSeekTimeMs > this.durationMs) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Use actual segment durations if available (more accurate)
|
|
108
|
+
if (
|
|
109
|
+
rendition.segmentDurationsMs &&
|
|
110
|
+
rendition.segmentDurationsMs.length > 0
|
|
111
|
+
) {
|
|
112
|
+
let cumulativeTime = 0;
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < rendition.segmentDurationsMs.length; i++) {
|
|
115
|
+
const segmentDuration = rendition.segmentDurationsMs[i];
|
|
116
|
+
if (segmentDuration === undefined) {
|
|
117
|
+
throw new Error("Segment duration is required for JIT metadata");
|
|
118
|
+
}
|
|
119
|
+
const segmentStartMs = cumulativeTime;
|
|
120
|
+
const segmentEndMs = cumulativeTime + segmentDuration;
|
|
121
|
+
|
|
122
|
+
// Check if the desired seek time falls within this segment
|
|
123
|
+
// Special case: for the last segment, include the exact end time
|
|
124
|
+
const isLastSegment = i === rendition.segmentDurationsMs.length - 1;
|
|
125
|
+
const includesEndTime =
|
|
126
|
+
isLastSegment && desiredSeekTimeMs === this.durationMs;
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
desiredSeekTimeMs >= segmentStartMs &&
|
|
130
|
+
(desiredSeekTimeMs < segmentEndMs || includesEndTime)
|
|
131
|
+
) {
|
|
132
|
+
return i + 1; // Convert 0-based to 1-based segment ID
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
cumulativeTime += segmentDuration;
|
|
136
|
+
|
|
137
|
+
// If we've reached or exceeded file duration, stop
|
|
138
|
+
if (cumulativeTime >= this.durationMs) {
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// If we didn't find a segment, return undefined
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Fall back to fixed duration calculation for backward compatibility
|
|
148
|
+
if (!rendition.segmentDurationMs) {
|
|
149
|
+
throw new Error("Segment duration is required for JIT metadata");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const segmentIndex = Math.floor(
|
|
153
|
+
desiredSeekTimeMs / rendition.segmentDurationMs,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Calculate the actual segment start time
|
|
157
|
+
const segmentStartMs = segmentIndex * rendition.segmentDurationMs;
|
|
158
|
+
|
|
159
|
+
// If this segment would start at or beyond file duration, it doesn't exist
|
|
160
|
+
if (segmentStartMs >= this.durationMs) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return segmentIndex + 1; // Convert 0-based to 1-based
|
|
165
|
+
}
|
|
166
|
+
}
|