@ray-js/ipc-player-integration 0.0.49 → 0.0.50-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.
Files changed (55) hide show
  1. package/lib/ctx/ctx.composition.js +6 -1
  2. package/lib/ctx/ctx.js +11 -1
  3. package/lib/features/initPlayerWidgets/index.d.ts +8 -0
  4. package/lib/features/initPlayerWidgets/index.js +13 -0
  5. package/lib/i18n/index.d.ts +8 -0
  6. package/lib/i18n/strings.d.ts +4 -0
  7. package/lib/i18n/strings.js +4 -0
  8. package/lib/interface.d.ts +9 -1
  9. package/lib/ui/bottomLeftContent.js +23 -8
  10. package/lib/ui/constant.d.ts +10 -0
  11. package/lib/ui/constant.js +12 -1
  12. package/lib/ui/ui.js +78 -4
  13. package/lib/ui/ui.less +15 -0
  14. package/lib/utils/content/dpCode.d.ts +1 -0
  15. package/lib/utils/content/dpCode.js +5 -1
  16. package/lib/utils/storage/index.d.ts +2 -2
  17. package/lib/widgets/index.d.ts +1 -0
  18. package/lib/widgets/index.js +2 -1
  19. package/lib/widgets/realTimeMagnification/realTimeMagnification.js +18 -1
  20. package/lib/widgets/toggleVerticalFull/toggleVerticalFull.less +1 -1
  21. package/lib/widgets/zoomTurntable/constants.d.ts +92 -0
  22. package/lib/widgets/zoomTurntable/constants.js +148 -0
  23. package/lib/widgets/zoomTurntable/entryAnchor.d.ts +7 -0
  24. package/lib/widgets/zoomTurntable/entryAnchor.js +7 -0
  25. package/lib/widgets/zoomTurntable/index.d.ts +8 -0
  26. package/lib/widgets/zoomTurntable/index.js +8 -0
  27. package/lib/widgets/zoomTurntable/lensModel.d.ts +53 -0
  28. package/lib/widgets/zoomTurntable/lensModel.js +137 -0
  29. package/lib/widgets/zoomTurntable/lensModel.test.d.ts +1 -0
  30. package/lib/widgets/zoomTurntable/lensModel.test.js +336 -0
  31. package/lib/widgets/zoomTurntable/lensSkill.d.ts +89 -0
  32. package/lib/widgets/zoomTurntable/lensSkill.js +332 -0
  33. package/lib/widgets/zoomTurntable/measureVisible.d.ts +22 -0
  34. package/lib/widgets/zoomTurntable/measureVisible.js +76 -0
  35. package/lib/widgets/zoomTurntable/turntable/index.d.ts +10 -0
  36. package/lib/widgets/zoomTurntable/turntable/index.js +76 -0
  37. package/lib/widgets/zoomTurntable/turntable/index.less +11 -0
  38. package/lib/widgets/zoomTurntable/turntable/rjs/index.js +242 -0
  39. package/lib/widgets/zoomTurntable/turntable/rjs/index.json +3 -0
  40. package/lib/widgets/zoomTurntable/turntable/rjs/index.rjs +691 -0
  41. package/lib/widgets/zoomTurntable/turntable/rjs/index.tyml +7 -0
  42. package/lib/widgets/zoomTurntable/turntable/type.d.ts +95 -0
  43. package/lib/widgets/zoomTurntable/turntable/type.js +1 -0
  44. package/lib/widgets/zoomTurntable/useLensZoom.d.ts +37 -0
  45. package/lib/widgets/zoomTurntable/useLensZoom.js +418 -0
  46. package/lib/widgets/zoomTurntable/zoomTurntableDock.d.ts +17 -0
  47. package/lib/widgets/zoomTurntable/zoomTurntableDock.js +228 -0
  48. package/lib/widgets/zoomTurntable/zoomTurntableDock.less +11 -0
  49. package/lib/widgets/zoomTurntable/zoomTurntableEntry.d.ts +21 -0
  50. package/lib/widgets/zoomTurntable/zoomTurntableEntry.js +253 -0
  51. package/lib/widgets/zoomTurntable/zoomTurntableEntry.less +51 -0
  52. package/lib/widgets/zoomTurntable/zoomTurntableTip.d.ts +16 -0
  53. package/lib/widgets/zoomTurntable/zoomTurntableTip.js +76 -0
  54. package/lib/widgets/zoomTurntable/zoomTurntableTip.less +56 -0
  55. package/package.json +3 -3
@@ -0,0 +1,7 @@
1
+ <!-- 样式全部内联:rjs 目录下若存在 index.tyss,引用方构建时
2
+ style-compiler-loader 会为其注入 ./ray-styles.tyss 的 import,
3
+ 而该伴生文件不在 lib 产物里,会报 Cannot find module './ray-styles.tyss' -->
4
+ <canvas
5
+ canvas-id="{{canvasId}}"
6
+ style="display: block; width: {{canvasWidth}}px; height: {{canvasHeight}}px;"
7
+ ></canvas>
@@ -0,0 +1,95 @@
1
+ export interface ZoomMark {
2
+ /** 刻度对应的焦距值(画面绝对倍率 curM) */
3
+ value: number;
4
+ /** 显示标签,如 '1' */
5
+ label: string;
6
+ }
7
+ export interface ZoomTurntableStyleOptions {
8
+ /** 顶部预留空间,scale=1 时的 px 值,默认 50 */
9
+ topSafeArea?: number;
10
+ /** 当前倍率字号,scale=1 时的 px 值,默认跟随 markLabelFontSize */
11
+ currentLabelFontSize?: number;
12
+ /** 普通刻度字号,scale=1 时的 px 值,默认 14 */
13
+ markLabelFontSize?: number;
14
+ /** 指示圆环半径,scale=1 时的 px 值,默认 12 */
15
+ pointerRadius?: number;
16
+ /** 指示圆环线宽,scale=1 时的 px 值,默认 3 */
17
+ pointerStrokeWidth?: number;
18
+ /** 普通刻度点半径,scale=1 时的 px 值,默认 2.4 */
19
+ markDotRadius?: number;
20
+ /** 右侧模式刻度文字向椭圆内侧收的横向距离,scale=1 时的 px 值,默认 22 */
21
+ innerLabelOffsetX?: number;
22
+ /** 右侧模式刻度文字向椭圆内侧收的纵向距离,scale=1 时的 px 值,默认 22 */
23
+ innerLabelOffsetY?: number;
24
+ /** 虚线长度,scale=1 时的 px 值,默认 8 */
25
+ dashLength?: number;
26
+ /** 虚线间隔,scale=1 时的 px 值,默认 8 */
27
+ dashGap?: number;
28
+ /** 虚线宽度,scale=1 时的 px 值,默认 2.2 */
29
+ trackLineWidth?: number;
30
+ /** 遮罩透明度,默认 0.44 */
31
+ overlayOpacity?: number;
32
+ /** 从上弧顶往下截取的比例,0-1,默认 0.2 */
33
+ cropRatio?: number;
34
+ /**
35
+ * 绘制内容整体纵向偏移,scale=1 时的 px 值,正值下移,默认 0。
36
+ * 仅平移画布坐标系,不影响刻度与文案的角度分布。
37
+ */
38
+ contentOffsetY?: number;
39
+ /**
40
+ * 触摸响应余量(横向 / 纵向),scale=1 时的 px 值,默认 0。
41
+ * canvas 是唯一能接收 touch 的元素,尺寸严格等于椭圆截面时,
42
+ * 椭圆上方与两侧的空白区域滑不动。向外扩一圈可让整个转盘区域都可拖动,
43
+ * 绘制原点会同步补偿,视觉位置不变。
44
+ */
45
+ hitPaddingX?: number;
46
+ hitPaddingY?: number;
47
+ /**
48
+ * 轨道(含刻度点、指针)与标记文字整体向椭圆内侧收的距离,
49
+ * scale=1 时的 px 值,默认 0。用于加大它们与遮罩边缘的间距;
50
+ * 遮罩半径不变,故不影响组件占位尺寸。
51
+ */
52
+ trackEdgeInset?: number;
53
+ }
54
+ export interface ZoomTurntableProps {
55
+ /** 整体缩放倍数,默认 1。1.0 = 完整椭圆 351×516,可见画布约 351×149;椭圆比例固定 351:516 */
56
+ scale?: number;
57
+ /** 展示方向。bottom 为底部向上展开,right 为横屏右侧向左展开 */
58
+ orientation?: 'bottom' | 'right';
59
+ /** 从上弧顶往下截取的比例,0-1,默认 0.2 */
60
+ cropRatio?: number;
61
+ /** 最小焦距值(thresholds[0]) */
62
+ min?: number;
63
+ /** 最大焦距值(thresholds 末位 = 手势放大上限) */
64
+ max?: number;
65
+ /** 步进精度,默认 0.1 */
66
+ step?: number;
67
+ /** 受控值(画面绝对倍率 curM) */
68
+ value?: number;
69
+ /** 非受控初始值 */
70
+ defaultValue?: number;
71
+ /** 值变化回调(拖动中实时触发) */
72
+ onChange?: (value: number) => void;
73
+ /** 拖动结束回调 */
74
+ onChangeEnd?: (value: number) => void;
75
+ /** 手指按上转盘(用于禁用原生手势) */
76
+ onTouchStart?: () => void;
77
+ /** 手指离开转盘,含 touchcancel(用于恢复原生手势) */
78
+ onTouchEnd?: () => void;
79
+ /** 主刻度标记数组 */
80
+ marks?: ZoomMark[];
81
+ /**
82
+ * 次刻度值列表。逐镜头区间按各自 step 生成,因此不同区间刻度疏密可以不同。
83
+ * 不传则沿 progress 均匀分布(兜底行为)。
84
+ */
85
+ ticks?: number[];
86
+ /**
87
+ * 非线性曲线:'log' 为对数曲线,数值为幂次因子(>1 小值精细)。
88
+ * 本项目固定用 1(线性)—— 倍率域已是分段线性叠加,再叠对数会让刻度与镜头临界点错位。
89
+ */
90
+ curve?: 'log' | number;
91
+ /** 当前值与指示圆环颜色,默认 '#FFFFFF' */
92
+ activeColor?: string;
93
+ /** 视觉样式微调项 */
94
+ styleOptions?: ZoomTurntableStyleOptions;
95
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { ComponentConfigProps } from '../../interface';
2
+ import { ZoomSkill } from './lensSkill';
3
+ type Options = ComponentConfigProps & {
4
+ /** 业务侧的功能开关;实际是否渲染仍由 ipc_multi_zoom 解析出的 cnt 决定 */
5
+ showZoomTurntable?: boolean;
6
+ /** 调试用:直接注入镜头能力,跳过 ipc_multi_zoom DP 解析 */
7
+ mockZoomSkill?: ZoomSkill | boolean;
8
+ /** 跳过能力加载:Entry 常驻负责加载,Dock 作为消费方传 true 避免重复解析 */
9
+ skipLoad?: boolean;
10
+ };
11
+ /**
12
+ * 焦距能力与双层缩放状态机。
13
+ * zoomSkill / zoomLensIndex 存在 ctx 共享 atom 上,Entry / Dock / RealTimeMagnification 读同一份。
14
+ */
15
+ export declare const useLensZoom: (options: Options) => {
16
+ /** 是否具备多镜头变焦能力(不具备则整个 widget 不出现) */
17
+ multiLensReady: boolean;
18
+ zoomSkill: ZoomSkill | null;
19
+ /** 镜头列表(按 value 升序),用于渲染倍率 tab */
20
+ lenses: import("./lensSkill").Lens[];
21
+ lensIndex: number;
22
+ /** 画面绝对倍率 */
23
+ curM: number;
24
+ min: number;
25
+ max: number;
26
+ step: number;
27
+ marks: {
28
+ value: number;
29
+ label: string;
30
+ }[];
31
+ /** 次刻度值:逐镜头区间按各自 step 生成,间距逐段不同 */
32
+ ticks: number[];
33
+ beginDrag: (this: unknown, value: number) => void;
34
+ applyZoom: (this: unknown, targetM: number) => Promise<void>;
35
+ switchToLens: (this: unknown, index: number) => Promise<void>;
36
+ };
37
+ export {};
@@ -0,0 +1,418 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { useMemoizedFn } from 'ahooks';
3
+ import { useStore } from '../../ctx/store';
4
+ import { PlayState } from '../../interface';
5
+ import { useDpState } from '../../hooks/useDpState';
6
+ import { setScaleMultipleEvent, zoomLevelChange } from '../../ui/constant';
7
+ import { MOCK_ZOOM_SKILL, MULTI_ZOOM_DP_CODE, dumpChannelApis, isChannelChangeSupported, isZoomSkillValid, lensCount, parseZoomSkill, queryCurrentLens, showSmallMonitor, subscribeChannelChange, switchLens } from './lensSkill';
8
+ import { DEFAULT_STEP, D_MIN, buildMarks, buildTickValues, indexOfLensId, matchLens, maxScaleMultiple, toDisplayZoom, toScaleMultiple, zoomBounds } from './lensModel';
9
+ import { CHANNEL_SETTLE_TIMEOUT, D_MATCH_EPS, GESTURE_SWITCH_DEBOUNCE, SWITCH_LENS_TIMEOUT, zoomTurntableLensChange, zoomTurntableZoomChange } from './constants';
10
+ /**
11
+ * 最近一次的本地数字缩放 d,按 devId 记录。
12
+ * 放模块级:Dock 动态挂载/卸载,组件内 ref 存不住,但它只是派生中间量,不值得占一个 atom。
13
+ */
14
+ const lastDStore = new Map();
15
+ const getLastD = devId => {
16
+ var _lastDStore$get;
17
+ return (_lastDStore$get = lastDStore.get(devId)) !== null && _lastDStore$get !== void 0 ? _lastDStore$get : D_MIN;
18
+ };
19
+ const setLastD = (devId, d) => lastDStore.set(devId, d);
20
+
21
+ /**
22
+ * 进行中的切镜头(自己下发的那次):目标镜头下标 + 超时兜底 timer。
23
+ *
24
+ * 同样放模块级按 devId 共享 —— Entry / Dock 是两个 hook 实例,
25
+ * 发起方(Dock 拖动、Entry 点 tab)与 channelChange 订阅方(Entry)常常不是同一个,
26
+ * 存组件内 ref 的话订阅方看不到 pending,发起方也等不到清除,会白屏蔽回读到超时。
27
+ */
28
+
29
+ const pendingSwitchStore = new Map();
30
+ /**
31
+ * 全景小窗当前的开关状态,按 devId 记。
32
+ * 同样放模块级:Entry / Dock 是两个 hook 实例,共用一份才能去重 ——
33
+ * 小窗只在「跨过 d=1 这条线」时切一次,回读每来一帧就调一次原生方法没必要。
34
+ */
35
+ const smallMonitorStore = new Map();
36
+
37
+ /** 自增序号:连着切到同一个镜头时,靠它认出「pending 还是不是我这次的」 */
38
+ let switchSeq = 0;
39
+ /** 是否正在切镜头(屏蔽回读用) */
40
+ const isSwitching = devId => pendingSwitchStore.has(devId);
41
+ const takePendingSwitch = devId => {
42
+ const pending = pendingSwitchStore.get(devId);
43
+ if (pending) {
44
+ clearTimeout(pending.timer);
45
+ pendingSwitchStore.delete(devId);
46
+ }
47
+ return pending;
48
+ };
49
+
50
+ /**
51
+ * 焦距能力与双层缩放状态机。
52
+ * zoomSkill / zoomLensIndex 存在 ctx 共享 atom 上,Entry / Dock / RealTimeMagnification 读同一份。
53
+ */
54
+ export const useLensZoom = options => {
55
+ var _zoomSkill$lenses$sli, _zoomSkill$lenses, _zoomSkill$lenses$zoo, _zoomSkill$lenses2;
56
+ const {
57
+ devId,
58
+ event,
59
+ showZoomTurntable = false,
60
+ mockZoomSkill,
61
+ skipLoad = false,
62
+ setZoomSkill,
63
+ setZoomLensIndex,
64
+ IPCPlayerInstance
65
+ } = options;
66
+ const {
67
+ zoomSkill,
68
+ zoomLensIndex,
69
+ playState
70
+ } = useStore({
71
+ zoomSkill: options.zoomSkill,
72
+ zoomLensIndex: options.zoomLensIndex,
73
+ playState: options.playState
74
+ });
75
+
76
+ /** 镜头能力 DP;消费方(skipLoad)传空 devId 跳过订阅,直接读共享 atom */
77
+ const [dpState] = useDpState({
78
+ devId: skipLoad ? '' : devId,
79
+ dpCodes: [MULTI_ZOOM_DP_CODE]
80
+ });
81
+ const multiZoomDpValue = dpState[MULTI_ZOOM_DP_CODE];
82
+
83
+ /** 画面绝对倍率;挂载时按已记录的 d 恢复读数 */
84
+ const [curM, setCurM] = useState(() => zoomSkill ? toDisplayZoom(zoomSkill, zoomLensIndex, getLastD(devId)) : D_MIN);
85
+ /** 拖动中:屏蔽回读,避免指针被拽回 */
86
+ const draggingRef = useRef(false);
87
+ /** 手势缩放跨临界点判定的 trailing 防抖 */
88
+ const gestureTimerRef = useRef();
89
+ const lensIndexRef = useRef(zoomLensIndex);
90
+ lensIndexRef.current = zoomLensIndex;
91
+ const skillRef = useRef(zoomSkill);
92
+ skillRef.current = zoomSkill;
93
+
94
+ // ---------------- 能力加载 ----------------
95
+ useEffect(() => {
96
+ if (skipLoad || !devId || !showZoomTurntable) return undefined;
97
+
98
+ // mockZoomSkill 接管 DP 值:没有 ipc_multi_zoom 的设备上入口出不来,无法自测
99
+ if (mockZoomSkill) {
100
+ console.log('[zoomTurntable] mock 模式:跳过 ipc_multi_zoom DP 解析');
101
+ }
102
+ const raw = mockZoomSkill === true ? MOCK_ZOOM_SKILL : mockZoomSkill || multiZoomDpValue;
103
+ const skill = parseZoomSkill(raw);
104
+ if (!isZoomSkillValid(skill)) {
105
+ // DP 未上报 / 值畸形 / 单镜头设备:能力不可用,入口不出现
106
+ setZoomSkill(null);
107
+ return undefined;
108
+ }
109
+ setZoomSkill(skill);
110
+ return undefined;
111
+ }, [devId, showZoomTurntable, mockZoomSkill, multiZoomDpValue, skipLoad]);
112
+
113
+ /** 多镜头能力就绪:ipc_multi_zoom 解析出的 cnt > 1(单镜头设备不出现) */
114
+ const multiLensReady = showZoomTurntable && lensCount(zoomSkill) > 1;
115
+
116
+ // 手势缩放上限按当前镜头换算后下发给播放器:maxScaleMultiple 是 d 口径,
117
+ // 直接用画面绝对上限(如 6.0)会让画面放到该镜头区间之外。切镜头后跟着更新。
118
+ useEffect(() => {
119
+ if (skipLoad || !multiLensReady || !zoomSkill) return;
120
+ if (typeof (IPCPlayerInstance === null || IPCPlayerInstance === void 0 ? void 0 : IPCPlayerInstance.setMaxScaleMultiple) !== 'function') {
121
+ console.warn('[zoomTurntable] 播放器实例未就绪,maxScaleMultiple 未下发');
122
+ return;
123
+ }
124
+ const maxD = maxScaleMultiple(zoomSkill, zoomLensIndex);
125
+ IPCPlayerInstance.setMaxScaleMultiple({
126
+ maxScaleMultiple: maxD,
127
+ success: () => console.log('[zoomTurntable] setMaxScaleMultiple success', maxD),
128
+ fail: err => console.warn('[zoomTurntable] setMaxScaleMultiple fail', maxD, JSON.stringify(err !== null && err !== void 0 ? err : null))
129
+ });
130
+ }, [skipLoad, multiLensReady, zoomSkill, zoomLensIndex, IPCPlayerInstance]);
131
+
132
+ // ---------------- 全景小窗 ----------------
133
+ /**
134
+ * 开关全景小窗,并按 devId 去重(同一状态不重复下发)。
135
+ * 画面停在镜头光学正倍数(d≈1,即 1.0x / 3.0x / 8.0x)时不需要小窗;
136
+ * 在镜头区间内数字放大后(d>1)才显示,小窗放的是当前镜头的全景画面。
137
+ */
138
+ const applySmallMonitor = useMemoizedFn(visible => {
139
+ if (smallMonitorStore.get(devId) === visible) return;
140
+ smallMonitorStore.set(devId, visible);
141
+ showSmallMonitor(IPCPlayerInstance, visible);
142
+ });
143
+
144
+ /** 由本地数字缩放 d 决定小窗开关:d>1 说明画面不在镜头正倍数上 */
145
+ const syncSmallMonitorByD = useMemoizedFn(d => {
146
+ applySmallMonitor(d > 1 + D_MATCH_EPS);
147
+ });
148
+
149
+ // ---------------- 同步:把外部的「当前镜头 + d」落到 UI ----------------
150
+ /** 仅同步状态,不做任何下发;出流回读与 channelChange 共用 */
151
+ const syncToLens = useMemoizedFn((index, d) => {
152
+ const skill = skillRef.current;
153
+ if (!skill) return;
154
+ setZoomLensIndex(index);
155
+ lensIndexRef.current = index;
156
+ setLastD(devId, d);
157
+ setCurM(toDisplayZoom(skill, index, d));
158
+ });
159
+
160
+ // 出流后设备侧才答得上来「当前是哪个镜头」,故等 PLAYING 再查;
161
+ // 停流 → 再出流会重查一次,期间被别的端切过镜头也能对上
162
+ useEffect(() => {
163
+ if (skipLoad || !devId || !multiLensReady || playState !== PlayState.PLAYING) return undefined;
164
+ let cancelled = false;
165
+ queryCurrentLens(devId).then(current => {
166
+ // 查不到就保持现状,不要把镜头重置到第一个
167
+ if (cancelled || !current || !skillRef.current) return;
168
+ syncToLens(indexOfLensId(skillRef.current, current.lens_id), current.multiple);
169
+ // 出流时画面可能已经是放大态(上次退出前放大过),小窗状态跟着回读值对齐一次
170
+ syncSmallMonitorByD(current.multiple);
171
+ });
172
+ return () => {
173
+ cancelled = true;
174
+ // 停流 / 卸载后原生那边的小窗状态无从得知,去重记录一并丢掉,
175
+ // 下次出流按回读值重新下发一次,避免「记录说已开、实际已关」再也发不出去
176
+ smallMonitorStore.delete(devId);
177
+ };
178
+ }, [devId, multiLensReady, playState, skipLoad, syncToLens, syncSmallMonitorByD]);
179
+
180
+ // ---------------- 下发:curM → 切镜头 + d ----------------
181
+ const beginDrag = useMemoizedFn(value => {
182
+ draggingRef.current = true;
183
+ setCurM(value);
184
+ });
185
+
186
+ /**
187
+ * 镜头已切到 index 后的收尾:画面比例主动归 1 倍,读数即该镜头的 basicRatio。
188
+ * 正常路径由 channelChange 触发(等设备真正切完再动画面,中间态更干净);
189
+ * 宿主不支持该事件时,退回 switchChannel 成功即收尾。
190
+ */
191
+ const settleLensSwitched = useMemoizedFn(index => {
192
+ var _skillRef$current;
193
+ const lens = (_skillRef$current = skillRef.current) === null || _skillRef$current === void 0 || (_skillRef$current = _skillRef$current.lenses) === null || _skillRef$current === void 0 ? void 0 : _skillRef$current[index];
194
+ if (!lens) return;
195
+ console.log('[scaleMultiple] zoomTurntable emit setScaleMultipleEvent', D_MIN, `lensIndex=${index}`, `lastD=${getLastD(devId)}`);
196
+ event.emit(setScaleMultipleEvent, D_MIN);
197
+ syncToLens(index, D_MIN);
198
+ // 切镜头即回到该镜头的光学正倍数,小窗关掉(onChannelChange / 超时兜底都会走到这里)
199
+ applySmallMonitor(false);
200
+ event.emit(zoomTurntableLensChange, {
201
+ lensId: lens.id,
202
+ lensIndex: index,
203
+ value: lens.basicRatio
204
+ });
205
+ event.emit(zoomTurntableZoomChange, {
206
+ curM: lens.basicRatio,
207
+ d: D_MIN,
208
+ lensIndex: index
209
+ });
210
+ });
211
+
212
+ /** 把目标倍率落到指定镜头上:同镜头内下发本地缩放,跨镜头则切物理镜头并归 1 倍 */
213
+ const applyToLens = useMemoizedFn(async (nextIndex, targetM) => {
214
+ var _skill$lenses;
215
+ const skill = skillRef.current;
216
+ if (!(skill !== null && skill !== void 0 && (_skill$lenses = skill.lenses) !== null && _skill$lenses !== void 0 && _skill$lenses[nextIndex])) return;
217
+ setCurM(targetM);
218
+ const prevIndex = lensIndexRef.current;
219
+ if (nextIndex === prevIndex) {
220
+ const d = toScaleMultiple(skill, prevIndex, targetM);
221
+ setLastD(devId, d);
222
+ event.emit(setScaleMultipleEvent, d);
223
+ event.emit(zoomTurntableZoomChange, {
224
+ curM: targetM,
225
+ d,
226
+ lensIndex: prevIndex
227
+ });
228
+ // 同镜头内变倍:直接按下发值切小窗,不必等画面回读
229
+ syncSmallMonitorByD(d);
230
+ return;
231
+ }
232
+
233
+ // 切物理镜头:从下发到 channelChange 回来这段屏蔽回读
234
+ console.log('[zoomTurntable] 切换镜头', `${prevIndex} -> ${nextIndex}`, `channel=${skill.lenses[nextIndex].id}`, `targetM=${targetM.toFixed(2)}`);
235
+ takePendingSwitch(devId); // 上一次没结算完的直接作废,以最新这次为准
236
+ switchSeq += 1;
237
+ const seq = switchSeq;
238
+ /** 读数退回「当前镜头 + 已记录的 d」;切没切成不确定时才用 */
239
+ const rollback = () => setCurM(toDisplayZoom(skill, lensIndexRef.current, getLastD(devId)));
240
+
241
+ // 第一段超时:switchChannel 回调始终不来 —— 切没切成未知,只能回滚
242
+ pendingSwitchStore.set(devId, {
243
+ seq,
244
+ index: nextIndex,
245
+ timer: setTimeout(() => {
246
+ pendingSwitchStore.delete(devId);
247
+ rollback();
248
+ console.warn('[zoomTurntable] switchChannel 无回调,读数已回滚', `${prevIndex} -> ${nextIndex}`, `${SWITCH_LENS_TIMEOUT}ms`);
249
+ }, SWITCH_LENS_TIMEOUT)
250
+ });
251
+ const ok = await switchLens(devId, skill.lenses[nextIndex]);
252
+ const pending = pendingSwitchStore.get(devId);
253
+ if ((pending === null || pending === void 0 ? void 0 : pending.seq) !== seq) return; // 已被超时 / 新的切换接管
254
+
255
+ if (!ok) {
256
+ takePendingSwitch(devId);
257
+ console.warn('[zoomTurntable] 切换镜头失败,读数已回滚', `${prevIndex} -> ${nextIndex}`);
258
+ setCurM(toDisplayZoom(skill, prevIndex, getLastD(devId)));
259
+ return;
260
+ }
261
+
262
+ // 下发成功不等于画面切完:收尾(归 1 倍 + 刷读数)留给 channelChange,
263
+ // 那时设备已经真的切过去了,画面不会先跳一下再稳定
264
+ if (!isChannelChangeSupported()) {
265
+ takePendingSwitch(devId);
266
+ settleLensSwitched(nextIndex);
267
+ return;
268
+ }
269
+
270
+ // 第二段超时:指令已被受理、只差事件 —— 此时**不能回滚**(物理镜头其实已经切了,
271
+ // 回滚读数只会和画面对不上),改为自己按下发结果收尾。
272
+ // 真机现状:设备侧暂未推 channelChange,切镜头基本都走到这里兜底
273
+ clearTimeout(pending.timer);
274
+ pending.timer = setTimeout(() => {
275
+ var _pendingSwitchStore$g;
276
+ if (((_pendingSwitchStore$g = pendingSwitchStore.get(devId)) === null || _pendingSwitchStore$g === void 0 ? void 0 : _pendingSwitchStore$g.seq) !== seq) return;
277
+ pendingSwitchStore.delete(devId);
278
+ // 走到这儿说明事件没在窗口内进 handler。定位顺序见下面这几条日志:
279
+ // 上面没有「channelChange 事件到达」→ 宿主没派发 / 方法名不对(看 dump);
280
+ // 有到达但没有「镜头切换到位」→ 被 deviceId 或 channel 校验挡掉了
281
+ console.warn('[zoomTurntable] 等 channelChange 超时,按下发结果收尾', `${prevIndex} -> ${nextIndex}`, `channel=${skill.lenses[nextIndex].id}`, `${CHANNEL_SETTLE_TIMEOUT}ms`, `onChannelChange 可用=${isChannelChangeSupported()}`);
282
+ dumpChannelApis();
283
+ settleLensSwitched(nextIndex);
284
+ }, CHANNEL_SETTLE_TIMEOUT);
285
+ });
286
+
287
+ /** 拖动结束:按倍率匹配镜头后落地 */
288
+ const applyZoom = useMemoizedFn(async targetM => {
289
+ draggingRef.current = false;
290
+ const skill = skillRef.current;
291
+ if (!skill) return;
292
+ await applyToLens(matchLens(skill, targetM), targetM);
293
+ });
294
+
295
+ /** 点击倍率 tab:直接切到该镜头并归位到其光学倍数;不走 matchLens,免得容差判到相邻镜头 */
296
+ const switchToLens = useMemoizedFn(async index => {
297
+ var _skill$lenses2;
298
+ const skill = skillRef.current;
299
+ const lens = skill === null || skill === void 0 || (_skill$lenses2 = skill.lenses) === null || _skill$lenses2 === void 0 ? void 0 : _skill$lenses2[index];
300
+ if (!lens) return;
301
+ draggingRef.current = false;
302
+ await applyToLens(index, lens.basicRatio);
303
+ });
304
+
305
+ // ---------------- 镜头切换到位:channelChange ----------------
306
+ /**
307
+ * 切镜头的统一收尾入口:自己下发的(switchChannel)与外部来源(设备侧 / 其他端)都走这里。
308
+ * 自己下发的那次不在 switchChannel 回调里收尾,就是为了等这条事件 —— 设备真的切过去了再动画面。
309
+ */
310
+ const onChannelChange = useMemoizedFn(_ref => {
311
+ let {
312
+ channel
313
+ } = _ref;
314
+ const skill = skillRef.current;
315
+ if (!skill) {
316
+ console.warn('[zoomTurntable] 收到 channelChange 但镜头能力未就绪,已忽略', channel);
317
+ return;
318
+ }
319
+ const index = skill.lenses.findIndex(item => item.id === channel);
320
+ if (index < 0) {
321
+ console.warn('[zoomTurntable] channelChange 的 channel 不在镜头列表内', channel);
322
+ return;
323
+ }
324
+
325
+ // 自己下发的切换到位了,清掉超时兜底;此时镜头下标还没动过,不能按「和当前相同」早退
326
+ const pending = takePendingSwitch(devId);
327
+ if (!pending && index === lensIndexRef.current) {
328
+ console.log('[zoomTurntable] channelChange 指向当前镜头,无需处理', index);
329
+ return; // 外部来源的重复事件
330
+ }
331
+ console.log('[zoomTurntable] 镜头切换到位', `${lensIndexRef.current} -> ${index}`, pending ? '(本端下发)' : '(外部来源)');
332
+ // 读数以设备为准,进行中的拖动作废
333
+ draggingRef.current = false;
334
+ settleLensSwitched(index);
335
+ });
336
+ useEffect(() => {
337
+ // 订没订上必须能从日志看出来:没订上时切镜头一定走超时回滚,两者表象一样
338
+ if (skipLoad || !devId || !multiLensReady) {
339
+ console.log('[zoomTurntable] 跳过 channelChange 订阅', `skipLoad=${skipLoad}`, `devId=${devId || '(空)'}`, `multiLensReady=${multiLensReady}`);
340
+ return undefined;
341
+ }
342
+ // 订阅前先把宿主 ty.ipc 上 channel 相关的方法名打出来:事件收不到时先排除「方法名不对」
343
+ dumpChannelApis();
344
+ return subscribeChannelChange(devId, onChannelChange);
345
+ }, [devId, multiLensReady, skipLoad, onChannelChange]);
346
+
347
+ // ---------------- 回读:d → curM,并处理手势跨临界点 ----------------
348
+
349
+ /**
350
+ * 手势缩放(双指缩放播放器,非转盘)跨临界点时自动切镜头。
351
+ *
352
+ * zoomLevelChange 高频上报,按协议 §4.4 用 trailing 防抖等手势停下再判定;
353
+ * 待 APP 提供 zoomFree 的「手势是否结束」参数(§4.3 第 5 条)后可去掉防抖。
354
+ */
355
+ const scheduleGestureLensSwitch = useMemoizedFn(value => {
356
+ if (gestureTimerRef.current) clearTimeout(gestureTimerRef.current);
357
+ gestureTimerRef.current = setTimeout(() => {
358
+ gestureTimerRef.current = undefined;
359
+ const skill = skillRef.current;
360
+ if (!skill || draggingRef.current || isSwitching(devId)) return;
361
+ const nextIndex = matchLens(skill, value);
362
+ if (nextIndex === lensIndexRef.current) return;
363
+ console.log('[zoomTurntable] 手势跨临界点,切换镜头', `curM=${value.toFixed(2)}`, `${lensIndexRef.current} -> ${nextIndex}`);
364
+ applyToLens(nextIndex, value);
365
+ }, GESTURE_SWITCH_DEBOUNCE);
366
+ });
367
+ const onZoomLevelChange = useMemoizedFn(d => {
368
+ if (!(d > 0)) return;
369
+ setLastD(devId, d);
370
+ // 切镜头途中的中间态不算数(收尾时统一关掉),拖动中画面还没动
371
+ if (draggingRef.current || isSwitching(devId)) return;
372
+ const skill = skillRef.current;
373
+ if (!skill) return;
374
+ // 手势缩放不经过 applyToLens,小窗只能从这条回读上判定
375
+ syncSmallMonitorByD(d);
376
+ const next = toDisplayZoom(skill, lensIndexRef.current, d);
377
+ setCurM(next);
378
+ event.emit(zoomTurntableZoomChange, {
379
+ curM: next,
380
+ d,
381
+ lensIndex: lensIndexRef.current
382
+ });
383
+ scheduleGestureLensSwitch(next);
384
+ });
385
+ useEffect(() => {
386
+ if (!multiLensReady) return undefined;
387
+ event.on(zoomLevelChange, onZoomLevelChange);
388
+ return () => {
389
+ event.off(zoomLevelChange, onZoomLevelChange);
390
+ };
391
+ }, [event, multiLensReady, onZoomLevelChange]);
392
+ useEffect(() => {
393
+ return () => {
394
+ if (gestureTimerRef.current) clearTimeout(gestureTimerRef.current);
395
+ };
396
+ }, []);
397
+ const [min, max] = zoomSkill ? zoomBounds(zoomSkill) : [D_MIN, D_MIN];
398
+ return {
399
+ /** 是否具备多镜头变焦能力(不具备则整个 widget 不出现) */
400
+ multiLensReady,
401
+ zoomSkill,
402
+ /** 镜头列表(按 value 升序),用于渲染倍率 tab */
403
+ lenses: (_zoomSkill$lenses$sli = zoomSkill === null || zoomSkill === void 0 || (_zoomSkill$lenses = zoomSkill.lenses) === null || _zoomSkill$lenses === void 0 ? void 0 : _zoomSkill$lenses.slice(0, lensCount(zoomSkill))) !== null && _zoomSkill$lenses$sli !== void 0 ? _zoomSkill$lenses$sli : [],
404
+ lensIndex: zoomLensIndex,
405
+ /** 画面绝对倍率 */
406
+ curM,
407
+ min,
408
+ max,
409
+ // 步进直接取协议配置值(不做二次加工),各镜头可各自声明不同精度
410
+ step: (_zoomSkill$lenses$zoo = zoomSkill === null || zoomSkill === void 0 || (_zoomSkill$lenses2 = zoomSkill.lenses) === null || _zoomSkill$lenses2 === void 0 || (_zoomSkill$lenses2 = _zoomSkill$lenses2[zoomLensIndex]) === null || _zoomSkill$lenses2 === void 0 ? void 0 : _zoomSkill$lenses2.step) !== null && _zoomSkill$lenses$zoo !== void 0 ? _zoomSkill$lenses$zoo : DEFAULT_STEP,
411
+ marks: zoomSkill ? buildMarks(zoomSkill) : [],
412
+ /** 次刻度值:逐镜头区间按各自 step 生成,间距逐段不同 */
413
+ ticks: zoomSkill ? buildTickValues(zoomSkill) : [],
414
+ beginDrag,
415
+ applyZoom,
416
+ switchToLens
417
+ };
418
+ };
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { ComponentConfigProps } from '../../interface';
3
+ import './zoomTurntableDock.less';
4
+ type Props = ComponentConfigProps & {
5
+ /** 由 ui.tsx 的 CoverView 分支注入(absolute-item-container) */
6
+ className?: string;
7
+ };
8
+ /**
9
+ * 焦距转盘浮层。
10
+ *
11
+ * 必须由 CoverView 承载(Entry 里 addContent 不设 absoluteNoCoverView):
12
+ * 播放器是原生组件、层级高于普通 View,放在普通 View 里的 canvas 会被它盖住,
13
+ * touchstart 到不了 canvas,转盘完全滑不动(实测 setupEventListeners 成功但无任何 touch 日志)。
14
+ * 定位与层级由承载的 CoverView 通过 absolutePosition 提供。
15
+ */
16
+ export declare const ZoomTurntableDock: (props: Props) => React.JSX.Element | null;
17
+ export {};