diff --git a/.changeset/eighty-masks-switch.md b/.changeset/eighty-masks-switch.md new file mode 100644 index 0000000000..0512624c7b --- /dev/null +++ b/.changeset/eighty-masks-switch.md @@ -0,0 +1,9 @@ +--- +'@alfalab/core-components': patch +'@alfalab/core-components-gallery': patch +--- + +##### Gallery + +- Заменен статический импорт hls.js на динамический. +- Добавлена retry логика для надежной загрузки. diff --git a/packages/gallery/src/components/image-viewer/video/index.tsx b/packages/gallery/src/components/image-viewer/video/index.tsx index 4b806fb9c3..9899319201 100644 --- a/packages/gallery/src/components/image-viewer/video/index.tsx +++ b/packages/gallery/src/components/image-viewer/video/index.tsx @@ -5,9 +5,10 @@ import React, { useContext, useEffect, useRef, + useState, } from 'react'; import cn from 'classnames'; -import Hls from 'hls.js'; +import type Hls from 'hls.js'; import { Circle } from '@alfalab/core-components-icon-view/circle'; import PlayCompactMIcon from '@alfalab/icons-glyph/PlayCompactMIcon'; @@ -28,6 +29,7 @@ type Props = { export const Video = ({ url, index, className, isActive }: Props) => { const playerRef = useRef(null); const timer = useRef>(); + const [hlsSupported, setHlsSupported] = useState(true); const { setImageMeta, @@ -49,32 +51,86 @@ export const Video = ({ url, index, className, isActive }: Props) => { /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [index]); + const loadHlsLibrary = useCallback( + async (attempt = 1, maxAttempts = 3): Promise => { + try { + const { default: HlsLib } = await import( + /* webpackChunkName: "hls-js-gallery" */ 'hls.js' + ); + + return HlsLib; + } catch { + if (attempt < maxAttempts) { + /* Экспоненциальная задержка ретрая: 300ms, 600ms, 1200ms */ + await new Promise((resolve) => { + setTimeout( + () => { + resolve(); + }, + 300 * 2 ** (attempt - 1), + ); + }); + + return loadHlsLibrary(attempt + 1, maxAttempts); + } + + setHlsSupported(false); + setImageMeta({ player: { current: null }, broken: true }, index); + + return null; + } + }, + [setImageMeta, index], + ); + useEffect(() => { - const hls = new Hls(); - - if (Hls.isSupported()) { - hls.on(Hls.Events.ERROR, (_, data) => { - if (data.fatal) { - switch (data.type) { - case Hls.ErrorTypes.MEDIA_ERROR: - hls.recoverMediaError(); - break; - case Hls.ErrorTypes.NETWORK_ERROR: - setImageMeta({ player: { current: null }, broken: true }, index); - break; - default: - hls.destroy(); - break; - } + let hls: Hls | null = null; + + const initHls = async () => { + try { + const HlsLib = await loadHlsLibrary(); + + if (!HlsLib || !playerRef.current) { + return; } - }); - hls.loadSource(url); - if (playerRef.current) { - hls.attachMedia(playerRef.current); - hls.subtitleDisplay = false; + if (!HlsLib.isSupported()) { + setHlsSupported(false); + + return; + } + + hls = new HlsLib(); + + hls.on(HlsLib.Events.ERROR, (_, data) => { + if (data.fatal && hls) { + switch (data.type) { + case HlsLib.ErrorTypes.MEDIA_ERROR: + hls.recoverMediaError(); + break; + case HlsLib.ErrorTypes.NETWORK_ERROR: + setImageMeta({ player: { current: null }, broken: true }, index); + break; + default: + hls.destroy(); + break; + } + } + }); + + hls.loadSource(url); + + if (playerRef.current) { + hls.attachMedia(playerRef.current); + hls.subtitleDisplay = false; + } + } catch { + setHlsSupported(false); + setImageMeta({ player: { current: null }, broken: true }, index); } - } + }; + + initHls(); return () => { if (hls) { @@ -183,7 +239,7 @@ export const Video = ({ url, index, className, isActive }: Props) => { playsInline={true} muted={mutedVideo} loop={true} - src={Hls.isSupported() ? undefined : url} + src={hlsSupported ? undefined : url} className={cn(styles.video, { [styles.mobile]: view === 'mobile' }, className)} >