@hh.ru/magritte-ui-video 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/Video.d.ts +13 -0
  2. package/Video.js +63 -0
  3. package/Video.js.map +1 -0
  4. package/VideoControls.d.ts +8 -0
  5. package/VideoControls.js +214 -0
  6. package/VideoControls.js.map +1 -0
  7. package/VideoPlayer.d.ts +4 -0
  8. package/VideoPlayer.js +197 -0
  9. package/VideoPlayer.js.map +1 -0
  10. package/index.css +355 -0
  11. package/index.d.ts +2 -0
  12. package/index.js +33 -0
  13. package/index.js.map +1 -0
  14. package/index.mock.d.ts +2 -0
  15. package/index.mock.js +16 -0
  16. package/index.mock.js.map +1 -0
  17. package/internal/ControlButton.d.ts +28 -0
  18. package/internal/ControlButton.js +63 -0
  19. package/internal/ControlButton.js.map +1 -0
  20. package/internal/Scrubber.d.ts +11 -0
  21. package/internal/Scrubber.js +125 -0
  22. package/internal/Scrubber.js.map +1 -0
  23. package/internal/SeekIndicator.d.ts +7 -0
  24. package/internal/SeekIndicator.js +33 -0
  25. package/internal/SeekIndicator.js.map +1 -0
  26. package/internal/TimeLabel.d.ts +10 -0
  27. package/internal/TimeLabel.js +22 -0
  28. package/internal/TimeLabel.js.map +1 -0
  29. package/internal/VolumeSlider.d.ts +6 -0
  30. package/internal/VolumeSlider.js +60 -0
  31. package/internal/VolumeSlider.js.map +1 -0
  32. package/internal/context.d.ts +22 -0
  33. package/internal/context.js +20 -0
  34. package/internal/context.js.map +1 -0
  35. package/internal/controlKey.d.ts +7 -0
  36. package/internal/controlKey.js +12 -0
  37. package/internal/controlKey.js.map +1 -0
  38. package/internal/formatTime.d.ts +10 -0
  39. package/internal/formatTime.js +19 -0
  40. package/internal/formatTime.js.map +1 -0
  41. package/internal/store.d.ts +109 -0
  42. package/internal/store.js +475 -0
  43. package/internal/store.js.map +1 -0
  44. package/internal/useCanvasGestures.d.ts +38 -0
  45. package/internal/useCanvasGestures.js +162 -0
  46. package/internal/useCanvasGestures.js.map +1 -0
  47. package/internal/useInert.d.ts +6 -0
  48. package/internal/useInert.js +11 -0
  49. package/internal/useInert.js.map +1 -0
  50. package/internal/useSliderPointer.d.ts +15 -0
  51. package/internal/useSliderPointer.js +46 -0
  52. package/internal/useSliderPointer.js.map +1 -0
  53. package/internal/useVideoHotkeys.d.ts +10 -0
  54. package/internal/useVideoHotkeys.js +47 -0
  55. package/internal/useVideoHotkeys.js.map +1 -0
  56. package/package.json +42 -3
  57. package/types.d.ts +97 -0
  58. package/types.js +3 -0
  59. package/types.js.map +1 -0
  60. package/video-C-iklAZX.js +5 -0
  61. package/video-C-iklAZX.js.map +1 -0
  62. package/README.md +0 -3
package/Video.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { VideoControls } from './VideoControls';
2
+ import { VideoPlayer } from './VideoPlayer';
3
+ import { type VideoHandle, type VideoProps } from './types';
4
+ interface VideoComponents {
5
+ Player: typeof VideoPlayer;
6
+ Controls: typeof VideoControls;
7
+ }
8
+ /**
9
+ * Корень видео: хранит состояние воспроизведения и раздаёт его частям (`Video.Player`, `Video.Controls`).
10
+ * Собственной разметки не создаёт
11
+ */
12
+ export declare const Video: React.ForwardRefExoticComponent<VideoProps & React.RefAttributes<VideoHandle>> & VideoComponents;
13
+ export {};
package/Video.js ADDED
@@ -0,0 +1,63 @@
1
+ import './index.css';
2
+ import { jsx } from 'react/jsx-runtime';
3
+ import { forwardRef, useRef, useState, useEffect, useImperativeHandle, useMemo } from 'react';
4
+ import { VideoControls } from './VideoControls.js';
5
+ import { VideoPlayer } from './VideoPlayer.js';
6
+ import { VideoContext } from './internal/context.js';
7
+ import { createVideoStore } from './internal/store.js';
8
+ import 'react-dom';
9
+ import 'classnames';
10
+ import '@hh.ru/magritte-common-use-translation';
11
+ import '@hh.ru/magritte-ui-breakpoint';
12
+ import '@hh.ru/magritte-ui-icon/icon';
13
+ import '@hh.ru/magritte-ui-typography';
14
+ import './internal/ControlButton.js';
15
+ import '@hh.ru/magritte-ui-tooltip';
16
+ import './video-C-iklAZX.js';
17
+ import './internal/Scrubber.js';
18
+ import './internal/formatTime.js';
19
+ import './internal/useSliderPointer.js';
20
+ import './internal/useVideoHotkeys.js';
21
+ import '@hh.ru/magritte-common-keyboard';
22
+ import './internal/TimeLabel.js';
23
+ import './internal/VolumeSlider.js';
24
+ import './internal/controlKey.js';
25
+ import './internal/useInert.js';
26
+ import '@hh.ru/magritte-ui-button';
27
+ import '@hh.ru/magritte-ui-button-stack';
28
+ import '@hh.ru/magritte-ui-loader';
29
+ import '@hh.ru/magritte-ui-placeholder';
30
+ import './internal/SeekIndicator.js';
31
+ import './internal/useCanvasGestures.js';
32
+
33
+ const Video = forwardRef(
34
+ ({ src, sources, preview, duration, attachMedia, onPlay, onPause, onEnded, onError, children }, ref) => {
35
+ const callbacksRef = useRef({ attachMedia, onPlay, onPause, onEnded, onError });
36
+ callbacksRef.current = { attachMedia, onPlay, onPause, onEnded, onError };
37
+ const [store] = useState(() => createVideoStore({ src, sources, getCallbacks: () => callbacksRef.current }));
38
+ useEffect(() => {
39
+ store.setSource({ src, sources });
40
+ }, [store, src, sources]);
41
+ useImperativeHandle(
42
+ ref,
43
+ () => ({
44
+ play: store.play,
45
+ pause: store.pause,
46
+ seek: store.seek,
47
+ getVideoElement: store.getVideoElement
48
+ }),
49
+ [store]
50
+ );
51
+ const contextValue = useMemo(
52
+ () => ({ store, preview, duration }),
53
+ [store, preview, duration]
54
+ );
55
+ return /* @__PURE__ */ jsx(VideoContext.Provider, { value: contextValue, children });
56
+ }
57
+ );
58
+ Video.displayName = "Video";
59
+ Video.Player = VideoPlayer;
60
+ Video.Controls = VideoControls;
61
+
62
+ export { Video };
63
+ //# sourceMappingURL=Video.js.map
package/Video.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Video.js","sources":["src/Video.tsx"],"sourcesContent":["import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';\n\nimport { VideoControls } from '@hh.ru/magritte-ui-video/VideoControls';\nimport { VideoPlayer } from '@hh.ru/magritte-ui-video/VideoPlayer';\nimport { VideoContext, type VideoContextValue } from '@hh.ru/magritte-ui-video/internal/context';\nimport { createVideoStore } from '@hh.ru/magritte-ui-video/internal/store';\nimport { type VideoHandle, type VideoProps } from '@hh.ru/magritte-ui-video/types';\n\ninterface VideoComponents {\n Player: typeof VideoPlayer;\n Controls: typeof VideoControls;\n}\n\n/**\n * Корень видео: хранит состояние воспроизведения и раздаёт его частям (`Video.Player`, `Video.Controls`).\n * Собственной разметки не создаёт\n */\nexport const Video = forwardRef<VideoHandle, VideoProps>(\n ({ src, sources, preview, duration, attachMedia, onPlay, onPause, onEnded, onError, children }, ref) => {\n const callbacksRef = useRef({ attachMedia, onPlay, onPause, onEnded, onError });\n callbacksRef.current = { attachMedia, onPlay, onPause, onEnded, onError };\n\n const [store] = useState(() => createVideoStore({ src, sources, getCallbacks: () => callbacksRef.current }));\n\n // стор сравнивает источники по значению — новый массив sources на каждый рендер не перезагружает видео\n useEffect(() => {\n store.setSource({ src, sources });\n }, [store, src, sources]);\n\n useImperativeHandle(\n ref,\n () => ({\n play: store.play,\n pause: store.pause,\n seek: store.seek,\n getVideoElement: store.getVideoElement,\n }),\n [store]\n );\n\n const contextValue = useMemo<VideoContextValue>(\n () => ({ store, preview, duration }),\n [store, preview, duration]\n );\n\n return <VideoContext.Provider value={contextValue}>{children}</VideoContext.Provider>;\n }\n) as React.ForwardRefExoticComponent<VideoProps & React.RefAttributes<VideoHandle>> & VideoComponents;\n\nVideo.displayName = 'Video';\n\nVideo.Player = VideoPlayer;\nVideo.Controls = VideoControls;\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiBO,MAAM,KAAQ,GAAA,UAAA;AAAA,EACjB,CAAC,EAAE,GAAK,EAAA,OAAA,EAAS,OAAS,EAAA,QAAA,EAAU,WAAa,EAAA,MAAA,EAAQ,OAAS,EAAA,OAAA,EAAS,OAAS,EAAA,QAAA,IAAY,GAAQ,KAAA;AACpG,IAAM,MAAA,YAAA,GAAe,OAAO,EAAE,WAAA,EAAa,QAAQ,OAAS,EAAA,OAAA,EAAS,SAAS,CAAA,CAAA;AAC9E,IAAA,YAAA,CAAa,UAAU,EAAE,WAAA,EAAa,MAAQ,EAAA,OAAA,EAAS,SAAS,OAAQ,EAAA,CAAA;AAExE,IAAA,MAAM,CAAC,KAAK,CAAI,GAAA,QAAA,CAAS,MAAM,gBAAiB,CAAA,EAAE,GAAK,EAAA,OAAA,EAAS,YAAc,EAAA,MAAM,YAAa,CAAA,OAAA,EAAS,CAAC,CAAA,CAAA;AAG3G,IAAA,SAAA,CAAU,MAAM;AACZ,MAAA,KAAA,CAAM,SAAU,CAAA,EAAE,GAAK,EAAA,OAAA,EAAS,CAAA,CAAA;AAAA,KACjC,EAAA,CAAC,KAAO,EAAA,GAAA,EAAK,OAAO,CAAC,CAAA,CAAA;AAExB,IAAA,mBAAA;AAAA,MACI,GAAA;AAAA,MACA,OAAO;AAAA,QACH,MAAM,KAAM,CAAA,IAAA;AAAA,QACZ,OAAO,KAAM,CAAA,KAAA;AAAA,QACb,MAAM,KAAM,CAAA,IAAA;AAAA,QACZ,iBAAiB,KAAM,CAAA,eAAA;AAAA,OAC3B,CAAA;AAAA,MACA,CAAC,KAAK,CAAA;AAAA,KACV,CAAA;AAEA,IAAA,MAAM,YAAe,GAAA,OAAA;AAAA,MACjB,OAAO,EAAE,KAAO,EAAA,OAAA,EAAS,QAAS,EAAA,CAAA;AAAA,MAClC,CAAC,KAAO,EAAA,OAAA,EAAS,QAAQ,CAAA;AAAA,KAC7B,CAAA;AAEA,IAAA,2BAAQ,YAAa,CAAA,QAAA,EAAb,EAAsB,KAAA,EAAO,cAAe,QAAS,EAAA,CAAA,CAAA;AAAA,GACjE;AACJ,EAAA;AAEA,KAAA,CAAM,WAAc,GAAA,OAAA,CAAA;AAEpB,KAAA,CAAM,MAAS,GAAA,WAAA,CAAA;AACf,KAAA,CAAM,QAAW,GAAA,aAAA;;;;"}
@@ -0,0 +1,8 @@
1
+ import { type FC } from 'react';
2
+ import { type VideoControlsProps } from './types';
3
+ /**
4
+ * Видеоконтролы: перемотка, время, play/pause (desktop), скорость, громкость, fullscreen.
5
+ * Внутри `Video.Player` раскладываются поверх видео у нижнего края, снаружи — в потоке.
6
+ * Стор может перенести их в другой контейнер (`setControlsContainer`), в fullscreen — всегда внутрь плеера
7
+ */
8
+ export declare const VideoControls: FC<VideoControlsProps>;
@@ -0,0 +1,214 @@
1
+ import './index.css';
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { useContext, useRef, useEffect, useLayoutEffect } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import classnames from 'classnames';
6
+ import { useTranslation } from '@hh.ru/magritte-common-use-translation';
7
+ import { useBreakpoint } from '@hh.ru/magritte-ui-breakpoint';
8
+ import { SpeakerCrossedOutlinedSize24, SpeakerWaveOutlinedSize24, DashedSquareInOutlinedSize24, DashedSquareOutOutlinedSize24, PlayFilledSize24, ArrowPathClockwiseOutlinedSize24, PauseFilledSize24 } from '@hh.ru/magritte-ui-icon/icon';
9
+ import { Text } from '@hh.ru/magritte-ui-typography';
10
+ import { ControlButton } from './internal/ControlButton.js';
11
+ import { Scrubber } from './internal/Scrubber.js';
12
+ import { TimeLabel } from './internal/TimeLabel.js';
13
+ import { VolumeSlider } from './internal/VolumeSlider.js';
14
+ import { useVideoContext, VideoPlayerContext, useVideoState } from './internal/context.js';
15
+ import { findFocusableControl, CONTROL_KEY_ATTRIBUTE } from './internal/controlKey.js';
16
+ import { useInert } from './internal/useInert.js';
17
+ import { useVideoHotkeys } from './internal/useVideoHotkeys.js';
18
+ import { s as styles } from './video-C-iklAZX.js';
19
+ import '@hh.ru/magritte-ui-tooltip';
20
+ import './internal/formatTime.js';
21
+ import './internal/useSliderPointer.js';
22
+ import '@hh.ru/magritte-common-keyboard';
23
+
24
+ const STATUSES_WITH_CONTROLS = ["loading", "playing", "paused", "ended"];
25
+ const VideoControls = ({ "data-qa": dataQa = "video-controls" }) => {
26
+ const { store } = useVideoContext("Video.Controls");
27
+ const insidePlayer = useContext(VideoPlayerContext);
28
+ const { isMobile } = useBreakpoint();
29
+ const status = useVideoState(store, (state) => state.status);
30
+ const visible = useVideoState(store, (state) => state.controlsVisible);
31
+ const live = useVideoState(store, (state) => state.duration === null);
32
+ const rate = useVideoState(store, (state) => state.rate);
33
+ const muted = useVideoState(store, (state) => state.muted);
34
+ const fullscreen = useVideoState(store, (state) => state.fullscreen);
35
+ const container = useVideoState(store, (state) => state.controlsContainer);
36
+ const rootRef = useRef(null);
37
+ const pointerFocusRef = useRef(false);
38
+ const holdingRef = useRef(false);
39
+ const focusedControlRef = useRef(null);
40
+ const restoringFocusRef = useRef(false);
41
+ useInert(rootRef, !visible);
42
+ const onKeyDown = useVideoHotkeys(store);
43
+ const hasControls = STATUSES_WITH_CONTROLS.includes(status);
44
+ useEffect(
45
+ () => () => {
46
+ if (holdingRef.current) {
47
+ holdingRef.current = false;
48
+ store.holdControls(false);
49
+ }
50
+ },
51
+ [store, hasControls]
52
+ );
53
+ useLayoutEffect(() => {
54
+ const root = rootRef.current;
55
+ if (!root || !visible) {
56
+ focusedControlRef.current = null;
57
+ return;
58
+ }
59
+ const key = focusedControlRef.current;
60
+ const active = document.activeElement;
61
+ if (!key || active && active !== document.body) {
62
+ return;
63
+ }
64
+ const control = findFocusableControl(root, key);
65
+ if (!control) {
66
+ focusedControlRef.current = null;
67
+ return;
68
+ }
69
+ restoringFocusRef.current = true;
70
+ control.focus({ preventScroll: true });
71
+ restoringFocusRef.current = false;
72
+ });
73
+ const controlsTrl = useTranslation("Video", "controls");
74
+ const playTrl = useTranslation("Video", "play");
75
+ const pauseTrl = useTranslation("Video", "pause");
76
+ const replayTrl = useTranslation("Video", "replay");
77
+ const playbackRateTrl = useTranslation("Video", "playbackRate");
78
+ const muteTrl = useTranslation("Video", "mute");
79
+ const unmuteTrl = useTranslation("Video", "unmute");
80
+ const enterFullscreenTrl = useTranslation("Video", "enterFullscreen");
81
+ const exitFullscreenTrl = useTranslation("Video", "exitFullscreen");
82
+ if (!hasControls) {
83
+ return null;
84
+ }
85
+ const withTooltip = !isMobile && visible;
86
+ const showScrubber = !live || status === "loading";
87
+ const onFocus = (event) => {
88
+ focusedControlRef.current = event.target.closest(`[${CONTROL_KEY_ATTRIBUTE}]`)?.getAttribute(CONTROL_KEY_ATTRIBUTE) ?? null;
89
+ const pointerFocus = pointerFocusRef.current;
90
+ pointerFocusRef.current = false;
91
+ if (restoringFocusRef.current || pointerFocus || holdingRef.current || event.currentTarget.contains(event.relatedTarget)) {
92
+ return;
93
+ }
94
+ holdingRef.current = true;
95
+ store.holdControls(true);
96
+ };
97
+ const onBlur = (event) => {
98
+ if (!event.currentTarget.contains(event.relatedTarget)) {
99
+ focusedControlRef.current = null;
100
+ }
101
+ if (!holdingRef.current || event.currentTarget.contains(event.relatedTarget)) {
102
+ return;
103
+ }
104
+ holdingRef.current = false;
105
+ store.holdControls(false);
106
+ };
107
+ let playPause = { label: playTrl, icon: PlayFilledSize24 };
108
+ if (status === "ended") {
109
+ playPause = { label: replayTrl, icon: ArrowPathClockwiseOutlinedSize24 };
110
+ } else if (status === "playing") {
111
+ playPause = { label: pauseTrl, icon: PauseFilledSize24 };
112
+ }
113
+ const renderResult = /* @__PURE__ */ jsxs(
114
+ "div",
115
+ {
116
+ ref: rootRef,
117
+ role: "group",
118
+ "aria-label": controlsTrl,
119
+ className: classnames(styles.controls, {
120
+ [styles.controlsMobile]: isMobile,
121
+ [styles.controlsOverlay]: fullscreen || insidePlayer && !container,
122
+ [styles.controlsHidden]: !visible
123
+ }),
124
+ "data-video-controls": true,
125
+ "data-qa": dataQa,
126
+ onPointerDownCapture: () => {
127
+ pointerFocusRef.current = true;
128
+ },
129
+ onPointerUpCapture: () => {
130
+ pointerFocusRef.current = false;
131
+ },
132
+ onPointerMove: (event) => event.pointerType === "mouse" && store.notifyActivity(),
133
+ onFocus,
134
+ onBlur,
135
+ onKeyDown,
136
+ children: [
137
+ showScrubber && /* @__PURE__ */ jsx(Scrubber, { store }),
138
+ /* @__PURE__ */ jsxs("div", { className: styles.controlsRow, children: [
139
+ /* @__PURE__ */ jsxs("div", { className: styles.controlsGroupStart, children: [
140
+ !isMobile && /* @__PURE__ */ jsx(
141
+ ControlButton,
142
+ {
143
+ label: playPause.label,
144
+ icon: playPause.icon,
145
+ onClick: () => {
146
+ store.toggle();
147
+ store.notifyActivity();
148
+ },
149
+ withTooltip,
150
+ control: "play-pause",
151
+ "data-qa": "video-play-pause"
152
+ }
153
+ ),
154
+ /* @__PURE__ */ jsx("div", { className: styles.controlsTime, children: /* @__PURE__ */ jsx(TimeLabel, { store, live: live && status !== "loading" }) })
155
+ ] }),
156
+ /* @__PURE__ */ jsxs("div", { className: styles.controlsGroupEnd, children: [
157
+ /* @__PURE__ */ jsx(
158
+ ControlButton,
159
+ {
160
+ label: `${playbackRateTrl ?? ""}: ${rate}x`,
161
+ onClick: () => {
162
+ store.cycleRate();
163
+ store.notifyActivity();
164
+ },
165
+ withTooltip,
166
+ control: "playback-rate",
167
+ "data-qa": "video-playback-rate",
168
+ children: /* @__PURE__ */ jsx(Text, { typography: "subtitle-2-semibold", style: "constant", children: `${rate}x` })
169
+ }
170
+ ),
171
+ /* @__PURE__ */ jsxs("div", { className: styles.controlsVolume, children: [
172
+ /* @__PURE__ */ jsx(
173
+ ControlButton,
174
+ {
175
+ label: muted ? unmuteTrl : muteTrl,
176
+ icon: muted ? SpeakerCrossedOutlinedSize24 : SpeakerWaveOutlinedSize24,
177
+ onClick: () => {
178
+ store.setMuted(!muted);
179
+ store.notifyActivity();
180
+ },
181
+ withTooltip,
182
+ bare: true,
183
+ control: "mute",
184
+ "data-qa": "video-mute"
185
+ }
186
+ ),
187
+ !isMobile && /* @__PURE__ */ jsx(VolumeSlider, { store })
188
+ ] }),
189
+ /* @__PURE__ */ jsx(
190
+ ControlButton,
191
+ {
192
+ label: fullscreen ? exitFullscreenTrl : enterFullscreenTrl,
193
+ icon: fullscreen ? DashedSquareInOutlinedSize24 : DashedSquareOutOutlinedSize24,
194
+ onClick: () => {
195
+ store.toggleFullscreen();
196
+ store.notifyActivity();
197
+ },
198
+ withTooltip,
199
+ control: "fullscreen",
200
+ "data-qa": "video-fullscreen"
201
+ }
202
+ )
203
+ ] })
204
+ ] })
205
+ ]
206
+ }
207
+ );
208
+ const target = fullscreen && store.getRoot() || container;
209
+ return target ? createPortal(renderResult, target) : renderResult;
210
+ };
211
+ VideoControls.displayName = "Video.Controls";
212
+
213
+ export { VideoControls };
214
+ //# sourceMappingURL=VideoControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoControls.js","sources":["src/VideoControls.tsx"],"sourcesContent":["import { type FC, type FocusEvent, useContext, useEffect, useLayoutEffect, useRef } from 'react';\nimport { createPortal } from 'react-dom';\nimport classnames from 'classnames';\n\nimport { useTranslation } from '@hh.ru/magritte-common-use-translation';\nimport { useBreakpoint } from '@hh.ru/magritte-ui-breakpoint';\nimport {\n ArrowPathClockwiseOutlinedSize24,\n DashedSquareInOutlinedSize24,\n DashedSquareOutOutlinedSize24,\n PauseFilledSize24,\n PlayFilledSize24,\n SpeakerCrossedOutlinedSize24,\n SpeakerWaveOutlinedSize24,\n} from '@hh.ru/magritte-ui-icon/icon';\nimport { Text } from '@hh.ru/magritte-ui-typography';\nimport { ControlButton } from '@hh.ru/magritte-ui-video/internal/ControlButton';\nimport { Scrubber } from '@hh.ru/magritte-ui-video/internal/Scrubber';\nimport { TimeLabel } from '@hh.ru/magritte-ui-video/internal/TimeLabel';\nimport { VolumeSlider } from '@hh.ru/magritte-ui-video/internal/VolumeSlider';\nimport { useVideoContext, useVideoState, VideoPlayerContext } from '@hh.ru/magritte-ui-video/internal/context';\nimport { CONTROL_KEY_ATTRIBUTE, findFocusableControl } from '@hh.ru/magritte-ui-video/internal/controlKey';\nimport { useInert } from '@hh.ru/magritte-ui-video/internal/useInert';\nimport { useVideoHotkeys } from '@hh.ru/magritte-ui-video/internal/useVideoHotkeys';\nimport { type VideoControlsProps, type VideoStatus } from '@hh.ru/magritte-ui-video/types';\n\nimport styles from './video.less';\n\n/** Контролы есть только после запуска: в default и при ошибке их нет */\nconst STATUSES_WITH_CONTROLS: VideoStatus[] = ['loading', 'playing', 'paused', 'ended'];\n\n/**\n * Видеоконтролы: перемотка, время, play/pause (desktop), скорость, громкость, fullscreen.\n * Внутри `Video.Player` раскладываются поверх видео у нижнего края, снаружи — в потоке.\n * Стор может перенести их в другой контейнер (`setControlsContainer`), в fullscreen — всегда внутрь плеера\n */\nexport const VideoControls: FC<VideoControlsProps> = ({ 'data-qa': dataQa = 'video-controls' }) => {\n const { store } = useVideoContext('Video.Controls');\n const insidePlayer = useContext(VideoPlayerContext);\n const { isMobile } = useBreakpoint();\n\n const status = useVideoState(store, (state) => state.status);\n const visible = useVideoState(store, (state) => state.controlsVisible);\n const live = useVideoState(store, (state) => state.duration === null);\n const rate = useVideoState(store, (state) => state.rate);\n const muted = useVideoState(store, (state) => state.muted);\n const fullscreen = useVideoState(store, (state) => state.fullscreen);\n const container = useVideoState(store, (state) => state.controlsContainer);\n const rootRef = useRef<HTMLDivElement>(null);\n const pointerFocusRef = useRef(false);\n const holdingRef = useRef(false);\n // имя контрола с фокусом (`data-video-control`) — по нему фокус возвращается, если узел под ним подменили\n const focusedControlRef = useRef<string | null>(null);\n const restoringFocusRef = useRef(false);\n useInert(rootRef, !visible);\n // контролы бывают вне плеера (в потоке, во вьювере) — фокус в них тоже «фокус в видео»\n const onKeyDown = useVideoHotkeys(store);\n\n const hasControls = STATUSES_WITH_CONTROLS.includes(status);\n\n // Удержание (фокус с клавиатуры внутри) снимается по blur. Но если контролы пропали вместе с фокусом —\n // ошибка или смена источника (компонент рендерит null) либо размонтирование, — blur не придёт:\n // отпускаем удержание сами, иначе контролы больше никогда не спрячутся\n useEffect(\n () => () => {\n if (holdingRef.current) {\n holdingRef.current = false;\n store.holdControls(false);\n }\n },\n [store, hasControls]\n );\n\n // Кнопка со сменой иконки (звук, play/pause, fullscreen) пересоздаётся: у каждой иконки свой компонент.\n // Вход и выход из fullscreen переносит контролы в портал и обратно — пересоздаются все. Фокус в обоих случаях\n // падает на body без blur: возвращаем его на тот же контрол, чтобы клавиатурная навигация продолжалась с места\n useLayoutEffect(() => {\n const root = rootRef.current;\n if (!root || !visible) {\n focusedControlRef.current = null;\n return;\n }\n const key = focusedControlRef.current;\n const active = document.activeElement;\n if (!key || (active && active !== document.body)) {\n return;\n }\n const control = findFocusableControl(root, key);\n if (!control) {\n focusedControlRef.current = null;\n return;\n }\n // фокус не новый — удержание контролов остаётся как было\n restoringFocusRef.current = true;\n control.focus({ preventScroll: true });\n restoringFocusRef.current = false;\n });\n\n const controlsTrl = useTranslation('Video', 'controls');\n const playTrl = useTranslation('Video', 'play');\n const pauseTrl = useTranslation('Video', 'pause');\n const replayTrl = useTranslation('Video', 'replay');\n const playbackRateTrl = useTranslation('Video', 'playbackRate');\n const muteTrl = useTranslation('Video', 'mute');\n const unmuteTrl = useTranslation('Video', 'unmute');\n const enterFullscreenTrl = useTranslation('Video', 'enterFullscreen');\n const exitFullscreenTrl = useTranslation('Video', 'exitFullscreen');\n\n if (!hasControls) {\n return null;\n }\n\n const withTooltip = !isMobile && visible;\n // live до загрузки метаданных не отличить от обычного видео — ползунок прячем только когда длительность так и не появилась\n const showScrubber = !live || status === 'loading';\n\n // фокус с клавиатуры внутри контролов — взаимодействие: пока он там, контролы не прячутся.\n // Фокус после клика мышью не держит контролы, иначе они не спрячутся никогда\n const onFocus = (event: FocusEvent<HTMLDivElement>) => {\n focusedControlRef.current =\n (event.target as HTMLElement).closest(`[${CONTROL_KEY_ATTRIBUTE}]`)?.getAttribute(CONTROL_KEY_ATTRIBUTE) ??\n null;\n const pointerFocus = pointerFocusRef.current;\n pointerFocusRef.current = false;\n if (\n restoringFocusRef.current ||\n pointerFocus ||\n holdingRef.current ||\n event.currentTarget.contains(event.relatedTarget)\n ) {\n return;\n }\n holdingRef.current = true;\n store.holdControls(true);\n };\n const onBlur = (event: FocusEvent<HTMLDivElement>) => {\n if (!event.currentTarget.contains(event.relatedTarget)) {\n focusedControlRef.current = null;\n }\n if (!holdingRef.current || event.currentTarget.contains(event.relatedTarget)) {\n return;\n }\n holdingRef.current = false;\n store.holdControls(false);\n };\n\n let playPause = { label: playTrl, icon: PlayFilledSize24 };\n if (status === 'ended') {\n playPause = { label: replayTrl, icon: ArrowPathClockwiseOutlinedSize24 };\n } else if (status === 'playing') {\n playPause = { label: pauseTrl, icon: PauseFilledSize24 };\n }\n\n const renderResult = (\n <div\n ref={rootRef}\n role=\"group\"\n aria-label={controlsTrl}\n className={classnames(styles.controls, {\n [styles.controlsMobile]: isMobile,\n [styles.controlsOverlay]: fullscreen || (insidePlayer && !container),\n [styles.controlsHidden]: !visible,\n })}\n // холст игнорирует события из контролов по этому атрибуту\n data-video-controls\n data-qa={dataQa}\n onPointerDownCapture={() => {\n pointerFocusRef.current = true;\n }}\n // фокус от клика приходит до pointerup; если его не было — сбрасываем флаг\n onPointerUpCapture={() => {\n pointerFocusRef.current = false;\n }}\n onPointerMove={(event) => event.pointerType === 'mouse' && store.notifyActivity()}\n onFocus={onFocus}\n onBlur={onBlur}\n onKeyDown={onKeyDown}\n >\n {showScrubber && <Scrubber store={store} />}\n <div className={styles.controlsRow}>\n <div className={styles.controlsGroupStart}>\n {!isMobile && (\n <ControlButton\n label={playPause.label}\n icon={playPause.icon}\n onClick={() => {\n store.toggle();\n store.notifyActivity();\n }}\n withTooltip={withTooltip}\n control=\"play-pause\"\n data-qa=\"video-play-pause\"\n />\n )}\n <div className={styles.controlsTime}>\n <TimeLabel store={store} live={live && status !== 'loading'} />\n </div>\n </div>\n <div className={styles.controlsGroupEnd}>\n <ControlButton\n label={`${playbackRateTrl ?? ''}: ${rate}x`}\n onClick={() => {\n store.cycleRate();\n store.notifyActivity();\n }}\n withTooltip={withTooltip}\n control=\"playback-rate\"\n data-qa=\"video-playback-rate\"\n >\n <Text typography=\"subtitle-2-semibold\" style=\"constant\">\n {`${rate}x`}\n </Text>\n </ControlButton>\n <div className={styles.controlsVolume}>\n <ControlButton\n label={muted ? unmuteTrl : muteTrl}\n icon={muted ? SpeakerCrossedOutlinedSize24 : SpeakerWaveOutlinedSize24}\n onClick={() => {\n store.setMuted(!muted);\n store.notifyActivity();\n }}\n withTooltip={withTooltip}\n bare\n control=\"mute\"\n data-qa=\"video-mute\"\n />\n {!isMobile && <VolumeSlider store={store} />}\n </div>\n <ControlButton\n label={fullscreen ? exitFullscreenTrl : enterFullscreenTrl}\n icon={fullscreen ? DashedSquareInOutlinedSize24 : DashedSquareOutOutlinedSize24}\n onClick={() => {\n store.toggleFullscreen();\n store.notifyActivity();\n }}\n withTooltip={withTooltip}\n control=\"fullscreen\"\n data-qa=\"video-fullscreen\"\n />\n </div>\n </div>\n </div>\n );\n\n // в fullscreen разворачивается плеер — контролы переезжают в него; иначе — в заданный контейнер, если он есть\n const target = (fullscreen && store.getRoot()) || container;\n return target ? createPortal(renderResult, target) : renderResult;\n};\n\nVideoControls.displayName = 'Video.Controls';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { type FC } from 'react';
2
+ import { type VideoPlayerProps } from './types';
3
+ /** Холст видео: `<video>` и всё, что поверх него, — кнопка воспроизведения, длительность, загрузка, ошибка */
4
+ export declare const VideoPlayer: FC<VideoPlayerProps>;
package/VideoPlayer.js ADDED
@@ -0,0 +1,197 @@
1
+ import './index.css';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { useContext, useRef, useCallback, useEffect } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import classnames from 'classnames';
6
+ import { useTranslation } from '@hh.ru/magritte-common-use-translation';
7
+ import { useBreakpoint } from '@hh.ru/magritte-ui-breakpoint';
8
+ import { Button } from '@hh.ru/magritte-ui-button';
9
+ import { ButtonStack } from '@hh.ru/magritte-ui-button-stack';
10
+ import { PlayFilledSize24, ArrowPathClockwiseOutlinedSize24, PauseFilledSize24, ForwardFilledSize16 } from '@hh.ru/magritte-ui-icon/icon';
11
+ import { Loader } from '@hh.ru/magritte-ui-loader';
12
+ import { Placeholder } from '@hh.ru/magritte-ui-placeholder';
13
+ import { Text } from '@hh.ru/magritte-ui-typography';
14
+ import { SeekIndicator } from './internal/SeekIndicator.js';
15
+ import { useVideoContext, useVideoState, VideoPlayerHostContext, VideoPlayerContext } from './internal/context.js';
16
+ import { formatDuration } from './internal/formatTime.js';
17
+ import { BOOST_RATE } from './internal/store.js';
18
+ import { useCanvasGestures } from './internal/useCanvasGestures.js';
19
+ import { useInert } from './internal/useInert.js';
20
+ import { useVideoHotkeys } from './internal/useVideoHotkeys.js';
21
+ import { s as styles } from './video-C-iklAZX.js';
22
+ import '@hh.ru/magritte-common-keyboard';
23
+
24
+ const ERROR_PLACEHOLDER_MAX_WIDTH = 343;
25
+ const OVERLAY_ICON_PROPS = {
26
+ initialColor: "constant",
27
+ padding: 16,
28
+ borderRadius: "halfHeight",
29
+ backgroundStyle: "overlay"
30
+ };
31
+ const isOwnInteractiveTarget = (target) => target instanceof Element && !!target.closest('[data-video-controls], button, [role="slider"]');
32
+ const VideoPlayer = ({
33
+ aspectRatio,
34
+ borderRadius = 0,
35
+ showDuration = true,
36
+ "aria-label": ariaLabel,
37
+ "data-qa": dataQa = "video-player",
38
+ children
39
+ }) => {
40
+ const { store, preview, duration: durationProp } = useVideoContext("Video.Player");
41
+ const status = useVideoState(store, (state) => state.status);
42
+ const waiting = useVideoState(store, (state) => state.waiting);
43
+ const metadataDuration = useVideoState(store, (state) => state.duration);
44
+ const controlsVisible = useVideoState(store, (state) => state.controlsVisible);
45
+ const fullscreen = useVideoState(store, (state) => state.fullscreen);
46
+ const boosted = useVideoState(store, (state) => state.boosted);
47
+ const indicatorContainer = useVideoState(store, (state) => state.indicatorContainer);
48
+ const host = useContext(VideoPlayerHostContext);
49
+ const { isMobile } = useBreakpoint();
50
+ const centerButtonRef = useRef(null);
51
+ useInert(centerButtonRef, !controlsVisible);
52
+ const rootRef = useRef(null);
53
+ const setRoot = useCallback(
54
+ (element) => {
55
+ rootRef.current = element;
56
+ store.attachRoot(element);
57
+ },
58
+ [store]
59
+ );
60
+ const componentDescriptionTrl = useTranslation("Video", "componentDescription");
61
+ const playTrl = useTranslation("Video", "play");
62
+ const loadingTrl = useTranslation("Video", "loading");
63
+ const errorTitleTrl = useTranslation("Video", "errorTitle");
64
+ const errorDescriptionTrl = useTranslation("Video", "errorDescription");
65
+ const retryTrl = useTranslation("Video", "retry");
66
+ const pauseTrl = useTranslation("Video", "pause");
67
+ const replayTrl = useTranslation("Video", "replay");
68
+ const keepFocus = () => rootRef.current?.focus({ preventScroll: true });
69
+ const duration = metadataDuration ?? durationProp;
70
+ const isLoading = status === "loading" || status === "playing" && waiting;
71
+ const started = status === "playing" || status === "paused" || status === "ended";
72
+ useEffect(() => store.setHideWhenPaused(isMobile), [store, isMobile]);
73
+ const { seekIndicator, handlers } = useCanvasGestures({ store, isMobile, isOwnInteractiveTarget, keepFocus });
74
+ const onKeyDown = useVideoHotkeys(store);
75
+ let CenterIcon = PlayFilledSize24;
76
+ let centerLabel = playTrl;
77
+ if (status === "ended") {
78
+ CenterIcon = ArrowPathClockwiseOutlinedSize24;
79
+ centerLabel = replayTrl;
80
+ } else if (status === "playing") {
81
+ CenterIcon = PauseFilledSize24;
82
+ centerLabel = pauseTrl;
83
+ }
84
+ const renderBoostIndicator = () => {
85
+ const indicator = /* @__PURE__ */ jsxs("span", { className: styles.boostIndicator, "aria-hidden": true, "data-qa": "video-boost", children: [
86
+ /* @__PURE__ */ jsx(Text, { typography: "label-3-regular", style: "constant", children: `${BOOST_RATE}x` }),
87
+ /* @__PURE__ */ jsx(ForwardFilledSize16, { initialColor: "constant" })
88
+ ] });
89
+ return indicatorContainer && !fullscreen ? createPortal(indicator, indicatorContainer) : indicator;
90
+ };
91
+ const video = /* @__PURE__ */ jsx(
92
+ "video",
93
+ {
94
+ ref: store.attach,
95
+ className: classnames(styles.video, { [styles.videoHidden]: status === "error" }),
96
+ poster: preview,
97
+ playsInline: true,
98
+ preload: "metadata"
99
+ }
100
+ );
101
+ return /* @__PURE__ */ jsxs(
102
+ "div",
103
+ {
104
+ ref: setRoot,
105
+ role: "group",
106
+ "aria-label": ariaLabel ?? componentDescriptionTrl,
107
+ tabIndex: -1,
108
+ className: classnames(styles.player, {
109
+ [styles.playerFit]: aspectRatio === void 0,
110
+ [styles.playerEmbedded]: !!host && !fullscreen
111
+ }),
112
+ style: {
113
+ aspectRatio,
114
+ borderRadius: fullscreen ? 0 : borderRadius
115
+ },
116
+ "data-qa": dataQa,
117
+ "data-status": status,
118
+ onKeyDown,
119
+ ...handlers,
120
+ children: [
121
+ host ? host.renderVideo(video) : video,
122
+ status === "idle" && /* @__PURE__ */ jsxs(Fragment, { children: [
123
+ /* @__PURE__ */ jsx("span", { className: styles.overlayButton, children: /* @__PURE__ */ jsx(
124
+ PlayFilledSize24,
125
+ {
126
+ ...OVERLAY_ICON_PROPS,
127
+ "aria-label": playTrl,
128
+ onClick: () => {
129
+ keepFocus();
130
+ store.play();
131
+ },
132
+ "data-qa": "video-play"
133
+ }
134
+ ) }),
135
+ showDuration && duration !== void 0 && /* @__PURE__ */ jsx("span", { className: styles.duration, "data-qa": "video-duration", children: /* @__PURE__ */ jsx(Text, { typography: "paragraph-2-regular", style: "constant", children: formatDuration(duration) }) })
136
+ ] }),
137
+ isMobile && started && !waiting && /* @__PURE__ */ jsx(
138
+ "span",
139
+ {
140
+ ref: centerButtonRef,
141
+ className: classnames(styles.overlayButton, { [styles.overlayButtonHidden]: !controlsVisible }),
142
+ children: /* @__PURE__ */ jsx(
143
+ CenterIcon,
144
+ {
145
+ ...OVERLAY_ICON_PROPS,
146
+ "aria-label": centerLabel,
147
+ onClick: () => {
148
+ store.toggle();
149
+ store.notifyActivity();
150
+ },
151
+ "data-qa": "video-center-button"
152
+ }
153
+ )
154
+ }
155
+ ),
156
+ seekIndicator && /* @__PURE__ */ jsx(SeekIndicator, { ...seekIndicator }),
157
+ boosted && renderBoostIndicator(),
158
+ isLoading && /* @__PURE__ */ jsx("div", { className: styles.loader, children: /* @__PURE__ */ jsx(Loader, { size: 32, color: "constant", "aria-label": loadingTrl }) }),
159
+ status === "error" && /* @__PURE__ */ jsx("div", { className: styles.error, "data-qa": "video-error", children: /* @__PURE__ */ jsx(
160
+ Placeholder,
161
+ {
162
+ title: errorTitleTrl,
163
+ description: errorDescriptionTrl,
164
+ titleProps: {
165
+ Element: "div",
166
+ size: "medium",
167
+ alignment: "center",
168
+ style: "constant",
169
+ descriptionStyle: "constant-secondary",
170
+ notRegionTitle: true
171
+ },
172
+ maxWidth: ERROR_PLACEHOLDER_MAX_WIDTH,
173
+ buttons: /* @__PURE__ */ jsx(ButtonStack, { layout: "vertical", stretched: true, children: /* @__PURE__ */ jsx(
174
+ Button,
175
+ {
176
+ mode: "secondary",
177
+ style: "constant",
178
+ size: "medium",
179
+ onClick: () => {
180
+ keepFocus();
181
+ store.retry();
182
+ },
183
+ "data-qa": "video-retry",
184
+ children: retryTrl
185
+ }
186
+ ) })
187
+ }
188
+ ) }),
189
+ /* @__PURE__ */ jsx(VideoPlayerContext.Provider, { value: true, children })
190
+ ]
191
+ }
192
+ );
193
+ };
194
+ VideoPlayer.displayName = "Video.Player";
195
+
196
+ export { VideoPlayer };
197
+ //# sourceMappingURL=VideoPlayer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VideoPlayer.js","sources":["src/VideoPlayer.tsx"],"sourcesContent":["import { type FC, useCallback, useContext, useEffect, useRef } from 'react';\nimport { createPortal } from 'react-dom';\nimport classnames from 'classnames';\n\nimport { useTranslation } from '@hh.ru/magritte-common-use-translation';\nimport { useBreakpoint } from '@hh.ru/magritte-ui-breakpoint';\nimport { Button } from '@hh.ru/magritte-ui-button';\nimport { ButtonStack } from '@hh.ru/magritte-ui-button-stack';\nimport {\n ArrowPathClockwiseOutlinedSize24,\n ForwardFilledSize16,\n PauseFilledSize24,\n PlayFilledSize24,\n} from '@hh.ru/magritte-ui-icon/icon';\nimport { Loader } from '@hh.ru/magritte-ui-loader';\nimport { Placeholder } from '@hh.ru/magritte-ui-placeholder';\nimport { Text } from '@hh.ru/magritte-ui-typography';\nimport { SeekIndicator } from '@hh.ru/magritte-ui-video/internal/SeekIndicator';\nimport {\n useVideoContext,\n useVideoState,\n VideoPlayerContext,\n VideoPlayerHostContext,\n} from '@hh.ru/magritte-ui-video/internal/context';\nimport { formatDuration } from '@hh.ru/magritte-ui-video/internal/formatTime';\nimport { BOOST_RATE } from '@hh.ru/magritte-ui-video/internal/store';\nimport { useCanvasGestures } from '@hh.ru/magritte-ui-video/internal/useCanvasGestures';\nimport { useInert } from '@hh.ru/magritte-ui-video/internal/useInert';\nimport { useVideoHotkeys } from '@hh.ru/magritte-ui-video/internal/useVideoHotkeys';\nimport { type VideoPlayerProps } from '@hh.ru/magritte-ui-video/types';\n\nimport styles from './video.less';\n\nconst ERROR_PLACEHOLDER_MAX_WIDTH = 343;\n\n/** Круглая кнопка поверх видео: play в default и центральная play/pause на mobile */\nconst OVERLAY_ICON_PROPS = {\n initialColor: 'constant',\n padding: 16,\n borderRadius: 'halfHeight',\n backgroundStyle: 'overlay',\n} as const;\n\n/** Клики и тапы по контролам и кнопкам — не жесты холста */\nconst isOwnInteractiveTarget = (target: EventTarget): boolean =>\n target instanceof Element && !!target.closest('[data-video-controls], button, [role=\"slider\"]');\n\n/** Холст видео: `<video>` и всё, что поверх него, — кнопка воспроизведения, длительность, загрузка, ошибка */\nexport const VideoPlayer: FC<VideoPlayerProps> = ({\n aspectRatio,\n borderRadius = 0,\n showDuration = true,\n 'aria-label': ariaLabel,\n 'data-qa': dataQa = 'video-player',\n children,\n}) => {\n const { store, preview, duration: durationProp } = useVideoContext('Video.Player');\n const status = useVideoState(store, (state) => state.status);\n const waiting = useVideoState(store, (state) => state.waiting);\n const metadataDuration = useVideoState(store, (state) => state.duration);\n const controlsVisible = useVideoState(store, (state) => state.controlsVisible);\n const fullscreen = useVideoState(store, (state) => state.fullscreen);\n const boosted = useVideoState(store, (state) => state.boosted);\n const indicatorContainer = useVideoState(store, (state) => state.indicatorContainer);\n const host = useContext(VideoPlayerHostContext);\n const { isMobile } = useBreakpoint();\n const centerButtonRef = useRef<HTMLSpanElement>(null);\n useInert(centerButtonRef, !controlsVisible);\n const rootRef = useRef<HTMLDivElement | null>(null);\n const setRoot = useCallback(\n (element: HTMLDivElement | null) => {\n rootRef.current = element;\n store.attachRoot(element);\n },\n [store]\n );\n\n const componentDescriptionTrl = useTranslation('Video', 'componentDescription');\n const playTrl = useTranslation('Video', 'play');\n const loadingTrl = useTranslation('Video', 'loading');\n const errorTitleTrl = useTranslation('Video', 'errorTitle');\n const errorDescriptionTrl = useTranslation('Video', 'errorDescription');\n const retryTrl = useTranslation('Video', 'retry');\n const pauseTrl = useTranslation('Video', 'pause');\n const replayTrl = useTranslation('Video', 'replay');\n\n // кнопка, на которой фокус, сейчас исчезнет — оставляем фокус в плеере, а не на body\n const keepFocus = () => rootRef.current?.focus({ preventScroll: true });\n\n const duration = metadataDuration ?? durationProp;\n const isLoading = status === 'loading' || (status === 'playing' && waiting);\n const started = status === 'playing' || status === 'paused' || status === 'ended';\n\n // mobile: на паузе по центру видео кнопка воспроизведения — контролы можно прятать и на паузе; на desktop\n // поверх видео на паузе ничего нет, контролы остаются\n useEffect(() => store.setHideWhenPaused(isMobile), [store, isMobile]);\n\n const { seekIndicator, handlers } = useCanvasGestures({ store, isMobile, isOwnInteractiveTarget, keepFocus });\n const onKeyDown = useVideoHotkeys(store);\n\n let CenterIcon = PlayFilledSize24;\n let centerLabel = playTrl;\n if (status === 'ended') {\n CenterIcon = ArrowPathClockwiseOutlinedSize24;\n centerLabel = replayTrl;\n } else if (status === 'playing') {\n CenterIcon = PauseFilledSize24;\n centerLabel = pauseTrl;\n }\n\n // плашка ускорения — вверху плеера или в заданном контейнере (под шапкой вьювера); в fullscreen — всегда в плеере\n const renderBoostIndicator = () => {\n const indicator = (\n <span className={styles.boostIndicator} aria-hidden data-qa=\"video-boost\">\n <Text typography=\"label-3-regular\" style=\"constant\">\n {`${BOOST_RATE}x`}\n </Text>\n <ForwardFilledSize16 initialColor=\"constant\" />\n </span>\n );\n return indicatorContainer && !fullscreen ? createPortal(indicator, indicatorContainer) : indicator;\n };\n\n const video = (\n <video\n ref={store.attach}\n className={classnames(styles.video, { [styles.videoHidden]: status === 'error' })}\n poster={preview}\n playsInline\n preload=\"metadata\"\n />\n );\n\n return (\n <div\n ref={setRoot}\n role=\"group\"\n aria-label={ariaLabel ?? componentDescriptionTrl}\n tabIndex={-1}\n className={classnames(styles.player, {\n [styles.playerFit]: aspectRatio === undefined,\n [styles.playerEmbedded]: !!host && !fullscreen,\n })}\n // в fullscreen плеер занимает весь экран — скругление сбрасываем\n style={{\n aspectRatio,\n borderRadius: fullscreen ? 0 : borderRadius,\n }}\n data-qa={dataQa}\n data-status={status}\n onKeyDown={onKeyDown}\n {...handlers}\n >\n {host ? host.renderVideo(video) : video}\n\n {status === 'idle' && (\n <>\n {/* обёртка — позиционирование и размытие подложки, кнопку даёт сама иконка */}\n <span className={styles.overlayButton}>\n <PlayFilledSize24\n {...OVERLAY_ICON_PROPS}\n aria-label={playTrl}\n onClick={() => {\n keepFocus();\n store.play();\n }}\n data-qa=\"video-play\"\n />\n </span>\n {showDuration && duration !== undefined && (\n <span className={styles.duration} data-qa=\"video-duration\">\n <Text typography=\"paragraph-2-regular\" style=\"constant\">\n {formatDuration(duration)}\n </Text>\n </span>\n )}\n </>\n )}\n\n {isMobile && started && !waiting && (\n <span\n ref={centerButtonRef}\n className={classnames(styles.overlayButton, { [styles.overlayButtonHidden]: !controlsVisible })}\n >\n <CenterIcon\n {...OVERLAY_ICON_PROPS}\n aria-label={centerLabel}\n onClick={() => {\n store.toggle();\n store.notifyActivity();\n }}\n data-qa=\"video-center-button\"\n />\n </span>\n )}\n\n {seekIndicator && <SeekIndicator {...seekIndicator} />}\n\n {boosted && renderBoostIndicator()}\n\n {isLoading && (\n <div className={styles.loader}>\n <Loader size={32} color=\"constant\" aria-label={loadingTrl} />\n </div>\n )}\n\n {status === 'error' && (\n <div className={styles.error} data-qa=\"video-error\">\n <Placeholder\n title={errorTitleTrl}\n description={errorDescriptionTrl}\n titleProps={{\n Element: 'div',\n size: 'medium',\n alignment: 'center',\n style: 'constant',\n descriptionStyle: 'constant-secondary',\n notRegionTitle: true,\n }}\n maxWidth={ERROR_PLACEHOLDER_MAX_WIDTH}\n buttons={\n <ButtonStack layout=\"vertical\" stretched>\n <Button\n mode=\"secondary\"\n style=\"constant\"\n size=\"medium\"\n onClick={() => {\n keepFocus();\n store.retry();\n }}\n data-qa=\"video-retry\"\n >\n {retryTrl}\n </Button>\n </ButtonStack>\n }\n />\n </div>\n )}\n\n <VideoPlayerContext.Provider value>{children}</VideoPlayerContext.Provider>\n </div>\n );\n};\n\nVideoPlayer.displayName = 'Video.Player';\n"],"names":[],"mappings":"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"}