@shortkitsdk/react-native 0.2.30 → 0.2.32
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/android/libs/shortkit-release.aar +0 -0
- package/ios/ShortKitBridge.swift +77 -3
- package/ios/ShortKitFeedView.swift +1 -12
- package/ios/ShortKitModule.mm +5 -1
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Info.plist +2 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.abi.json +5652 -972
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +111 -9
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios.swiftinterface +111 -9
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64/ShortKitSDK.framework/_CodeSignature/CodeResources +9 -9
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Info.plist +2 -2
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +5652 -972
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +111 -9
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +111 -9
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +5652 -972
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +111 -9
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/Modules/ShortKitSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +111 -9
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/ShortKitSDK +0 -0
- package/ios/ShortKitSDK.xcframework/ios-arm64_x86_64-simulator/ShortKitSDK.framework/_CodeSignature/CodeResources +17 -17
- package/ios/ShortKitWidgetNativeView.swift +23 -0
- package/ios/ShortKitWidgetNativeViewManager.mm +1 -0
- package/package.json +1 -1
- package/src/ShortKitCommands.ts +12 -2
- package/src/ShortKitContext.ts +7 -1
- package/src/ShortKitWidget.tsx +47 -2
- package/src/specs/NativeShortKitModule.ts +9 -1
- package/src/specs/ShortKitWidgetViewNativeComponent.ts +6 -0
- package/src/types.ts +15 -0
- package/ios/DebugPanelView.swift +0 -302
package/src/ShortKitContext.ts
CHANGED
|
@@ -55,7 +55,13 @@ export interface ShortKitContextValue {
|
|
|
55
55
|
|
|
56
56
|
// Download
|
|
57
57
|
downloadState: DownloadState;
|
|
58
|
-
downloadVideo: (
|
|
58
|
+
downloadVideo: (
|
|
59
|
+
itemId: string,
|
|
60
|
+
options?: {
|
|
61
|
+
mode?: 'interruptive' | 'nonInterruptive';
|
|
62
|
+
overlayMode?: 'none' | 'static' | 'deterministic';
|
|
63
|
+
}
|
|
64
|
+
) => Promise<string>;
|
|
59
65
|
cancelDownload: () => void;
|
|
60
66
|
}
|
|
61
67
|
|
package/src/ShortKitWidget.tsx
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
import React, { useContext, useLayoutEffect, useMemo } from 'react';
|
|
1
|
+
import React, { useContext, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { View, StyleSheet } from 'react-native';
|
|
3
3
|
import type { ShortKitWidgetProps } from './types';
|
|
4
4
|
import ShortKitWidgetView from './specs/ShortKitWidgetViewNativeComponent';
|
|
5
|
+
import NativeShortKitModule from './specs/NativeShortKitModule';
|
|
5
6
|
import { ShortKitInitContext } from './ShortKitContext';
|
|
6
7
|
import { serializeWidgetConfig } from './serialization';
|
|
7
8
|
import { registerOverlayComponent } from './ShortKitOverlaySurface';
|
|
8
9
|
import { registerCarouselOverlayComponent } from './ShortKitCarouselOverlaySurface';
|
|
9
10
|
import { registerVideoCarouselOverlayComponent } from './ShortKitVideoCarouselOverlaySurface';
|
|
10
11
|
|
|
12
|
+
// Local UUID generator. We don't pull in a runtime dep just for this; a
|
|
13
|
+
// timestamp + random suffix is more than unique enough to disambiguate
|
|
14
|
+
// concurrently-mounted widgets within a single app session.
|
|
15
|
+
function generateWidgetId(): string {
|
|
16
|
+
return `widget-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
/**
|
|
12
20
|
* Horizontal carousel widget component. Displays a row of video cards
|
|
13
21
|
* with automatic rotation. Wraps a native Fabric view.
|
|
@@ -15,13 +23,36 @@ import { registerVideoCarouselOverlayComponent } from './ShortKitVideoCarouselOv
|
|
|
15
23
|
* Must be rendered inside a `<ShortKitProvider>`.
|
|
16
24
|
*/
|
|
17
25
|
export function ShortKitWidget(props: ShortKitWidgetProps) {
|
|
18
|
-
const { config, items, style } = props;
|
|
26
|
+
const { config, items, onCardTap, style } = props;
|
|
19
27
|
|
|
20
28
|
const isInitialized = useContext(ShortKitInitContext);
|
|
21
29
|
if (!isInitialized) {
|
|
22
30
|
throw new Error('ShortKitWidget must be used within a ShortKitProvider');
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
// Stable widget ID for the lifetime of this component, ONLY when the host
|
|
34
|
+
// wires `onCardTap`. Skipping it when unset means the native side leaves
|
|
35
|
+
// `widgetVC.onCardTap` nil, which means the SDK's existing
|
|
36
|
+
// `clickAction: 'feed'` modal-presentation path stays intact for hosts
|
|
37
|
+
// that don't opt in. Backwards compatible.
|
|
38
|
+
const widgetId = useMemo(
|
|
39
|
+
() => (onCardTap ? generateWidgetId() : undefined),
|
|
40
|
+
// We deliberately compute this once per "host wired callback" lifetime,
|
|
41
|
+
// not per render. The boolean test on first render is what matters; if
|
|
42
|
+
// the host swaps onCardTap from set → unset later we keep the same
|
|
43
|
+
// widgetId (harmless — no events will fire because the JS subscription
|
|
44
|
+
// below tears down). Going the other way (unset → set), we generate a
|
|
45
|
+
// fresh id then.
|
|
46
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
47
|
+
[Boolean(onCardTap)],
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Keep a stable ref to the latest onCardTap so the subscription doesn't
|
|
51
|
+
// need to tear down + rebuild every time the host re-renders with a new
|
|
52
|
+
// closure identity.
|
|
53
|
+
const onCardTapRef = useRef(onCardTap);
|
|
54
|
+
onCardTapRef.current = onCardTap;
|
|
55
|
+
|
|
25
56
|
useLayoutEffect(() => {
|
|
26
57
|
if (config?.overlay && config.overlay !== 'none') {
|
|
27
58
|
registerOverlayComponent(config.overlay.name, config.overlay.component);
|
|
@@ -40,6 +71,19 @@ export function ShortKitWidget(props: ShortKitWidgetProps) {
|
|
|
40
71
|
}
|
|
41
72
|
}, [config?.overlay, config?.feedConfig]);
|
|
42
73
|
|
|
74
|
+
// Subscribe to the global widget-card-tap event, filtered to this
|
|
75
|
+
// instance's widgetId.
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (!NativeShortKitModule || !onCardTap) return;
|
|
78
|
+
|
|
79
|
+
const subscription = NativeShortKitModule.onWidgetCardTap((event) => {
|
|
80
|
+
if (event.widgetId !== widgetId) return;
|
|
81
|
+
onCardTapRef.current?.(event.playbackId, event.index);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return () => subscription.remove();
|
|
85
|
+
}, [widgetId, onCardTap]);
|
|
86
|
+
|
|
43
87
|
const serializedConfig = useMemo(() => {
|
|
44
88
|
return serializeWidgetConfig(config ?? {});
|
|
45
89
|
}, [config]);
|
|
@@ -55,6 +99,7 @@ export function ShortKitWidget(props: ShortKitWidgetProps) {
|
|
|
55
99
|
style={styles.widget}
|
|
56
100
|
config={serializedConfig}
|
|
57
101
|
items={serializedItems}
|
|
102
|
+
widgetId={widgetId}
|
|
58
103
|
/>
|
|
59
104
|
</View>
|
|
60
105
|
);
|
|
@@ -127,6 +127,12 @@ type VideoCarouselCellTapEvent = Readonly<{
|
|
|
127
127
|
pageIndex: Int32;
|
|
128
128
|
}>;
|
|
129
129
|
|
|
130
|
+
type WidgetCardTapEvent = Readonly<{
|
|
131
|
+
widgetId: string;
|
|
132
|
+
playbackId: string;
|
|
133
|
+
index: Int32;
|
|
134
|
+
}>;
|
|
135
|
+
|
|
130
136
|
type DownloadStartedEvent = Readonly<{
|
|
131
137
|
itemId: string;
|
|
132
138
|
}>;
|
|
@@ -134,6 +140,7 @@ type DownloadStartedEvent = Readonly<{
|
|
|
134
140
|
type DownloadProgressEvent = Readonly<{
|
|
135
141
|
itemId: string;
|
|
136
142
|
progress: number;
|
|
143
|
+
phase: 'downloading' | 'compositing' | 'finalizing';
|
|
137
144
|
}>;
|
|
138
145
|
|
|
139
146
|
type DownloadCompletedEvent = Readonly<{
|
|
@@ -300,7 +307,7 @@ export interface Spec extends TurboModule {
|
|
|
300
307
|
getStoryboardData(playbackId: string): Promise<string>;
|
|
301
308
|
|
|
302
309
|
// --- Download ---
|
|
303
|
-
downloadVideo(itemId: string, mode: string): Promise<string>;
|
|
310
|
+
downloadVideo(itemId: string, mode: string, overlayMode: string): Promise<string>;
|
|
304
311
|
cancelDownload(): void;
|
|
305
312
|
|
|
306
313
|
// --- Carousel accessors ---
|
|
@@ -329,6 +336,7 @@ export interface Spec extends TurboModule {
|
|
|
329
336
|
readonly onDidFetchContentItems: EventEmitter<DidFetchContentItemsEvent>;
|
|
330
337
|
readonly onFeedReady: EventEmitter<FeedReadyEvent>;
|
|
331
338
|
readonly onVideoCarouselCellTap: EventEmitter<VideoCarouselCellTapEvent>;
|
|
339
|
+
readonly onWidgetCardTap: EventEmitter<WidgetCardTapEvent>;
|
|
332
340
|
|
|
333
341
|
// --- Overlay per-surface events ---
|
|
334
342
|
readonly onOverlayActiveChanged: EventEmitter<OverlayActiveEvent>;
|
|
@@ -4,6 +4,12 @@ import { codegenNativeComponent } from 'react-native';
|
|
|
4
4
|
export interface NativeProps extends ViewProps {
|
|
5
5
|
config: string;
|
|
6
6
|
items?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Stable per-instance widget identifier. Used to route the global
|
|
9
|
+
* `NativeShortKitModule.onWidgetCardTap` event back to the originating
|
|
10
|
+
* `<ShortKitWidget>` when multiple widgets are mounted.
|
|
11
|
+
*/
|
|
12
|
+
widgetId?: string;
|
|
7
13
|
}
|
|
8
14
|
|
|
9
15
|
export default codegenNativeComponent<NativeProps>(
|
package/src/types.ts
CHANGED
|
@@ -73,6 +73,10 @@ export interface ContentItem {
|
|
|
73
73
|
|
|
74
74
|
export type DownloadStatus = 'idle' | 'downloading' | 'completed' | 'failed';
|
|
75
75
|
|
|
76
|
+
export type DownloadOverlayMode = 'none' | 'static' | 'deterministic';
|
|
77
|
+
|
|
78
|
+
export type DownloadPhase = 'downloading' | 'compositing' | 'finalizing';
|
|
79
|
+
|
|
76
80
|
export interface DownloadState {
|
|
77
81
|
status: DownloadStatus;
|
|
78
82
|
/** ID of the content item being downloaded, if any. */
|
|
@@ -484,6 +488,17 @@ export interface ShortKitWidgetProps {
|
|
|
484
488
|
* how `<ShortKitFeed feedItems={...} />` works.
|
|
485
489
|
*/
|
|
486
490
|
items?: WidgetInput[];
|
|
491
|
+
/**
|
|
492
|
+
* Per-card tap callback. When set, the SDK skips the built-in
|
|
493
|
+
* `clickAction: 'feed'` modal-presentation path and hands the tap to the
|
|
494
|
+
* host — typically used to push a feed screen via the host's navigator
|
|
495
|
+
* instead of letting the SDK present a UIKit modal that escapes the host's
|
|
496
|
+
* navigation hierarchy.
|
|
497
|
+
*
|
|
498
|
+
* Mirrors `ShortKitPlayer.onTap`. `clickAction: 'mute'` continues to work
|
|
499
|
+
* regardless (per-cell mute toggle runs alongside `onCardTap`).
|
|
500
|
+
*/
|
|
501
|
+
onCardTap?: (playbackId: string, index: number) => void;
|
|
487
502
|
style?: ViewStyle;
|
|
488
503
|
}
|
|
489
504
|
|
package/ios/DebugPanelView.swift
DELETED
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import UIKit
|
|
2
|
-
import Combine
|
|
3
|
-
import ShortKitSDK
|
|
4
|
-
|
|
5
|
-
private enum AdjacentSlot {
|
|
6
|
-
case previous, next
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
final class DebugPanelView: UIView {
|
|
10
|
-
static let panelWidth: CGFloat = 310
|
|
11
|
-
static let panelHeight: CGFloat = 296 // +20 for collapsed header row
|
|
12
|
-
static let expandedExtraHeight: CGFloat = 80 // 4 labels × ~20pt
|
|
13
|
-
|
|
14
|
-
/// Currently selected swipe curve — FeedViewController reads SwipeCurve.selected.
|
|
15
|
-
static var selectedSwipeCurve: SwipeCurve {
|
|
16
|
-
get { SwipeCurve.selected }
|
|
17
|
-
set { SwipeCurve.selected = newValue }
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
private let stackView = UIStackView()
|
|
21
|
-
private let headerLabel = UILabel()
|
|
22
|
-
private let curveSegment = UISegmentedControl()
|
|
23
|
-
private let positionLabel = UILabel()
|
|
24
|
-
private let intentTtffLabel = UILabel()
|
|
25
|
-
private let transitionTtffLabel = UILabel()
|
|
26
|
-
private let prerollBufferLabel = UILabel()
|
|
27
|
-
private let videoQualityLabel = UILabel()
|
|
28
|
-
private let thumbnailLabel = UILabel()
|
|
29
|
-
private let prefetchLabel = UILabel()
|
|
30
|
-
private let bitrateLabel = UILabel()
|
|
31
|
-
private let rebufferLabel = UILabel()
|
|
32
|
-
private let loopWatchLabel = UILabel()
|
|
33
|
-
|
|
34
|
-
// Adjacent players section
|
|
35
|
-
private let adjacentHeader = UILabel()
|
|
36
|
-
private let prevHeaderLabel = UILabel()
|
|
37
|
-
private let prevDetailLabel = UILabel()
|
|
38
|
-
private let nextHeaderLabel = UILabel()
|
|
39
|
-
private let nextDetailLabel = UILabel()
|
|
40
|
-
private let separatorView = UIView()
|
|
41
|
-
private var adjacentLabels: [UIView] = []
|
|
42
|
-
private var isAdjacentExpanded = false
|
|
43
|
-
private var prevCancellable: AnyCancellable?
|
|
44
|
-
private var nextCancellable: AnyCancellable?
|
|
45
|
-
|
|
46
|
-
private var cancellable: AnyCancellable?
|
|
47
|
-
private var dragOffset: CGPoint = .zero
|
|
48
|
-
|
|
49
|
-
override init(frame: CGRect) {
|
|
50
|
-
super.init(frame: frame)
|
|
51
|
-
setupViews()
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
required init?(coder: NSCoder) {
|
|
55
|
-
fatalError("init(coder:) has not been implemented")
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
private func setupViews() {
|
|
59
|
-
backgroundColor = UIColor.black.withAlphaComponent(0.75)
|
|
60
|
-
layer.cornerRadius = 10
|
|
61
|
-
clipsToBounds = true
|
|
62
|
-
|
|
63
|
-
let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:)))
|
|
64
|
-
addGestureRecognizer(pan)
|
|
65
|
-
|
|
66
|
-
let labels = [headerLabel, positionLabel, intentTtffLabel, transitionTtffLabel, prerollBufferLabel, videoQualityLabel, thumbnailLabel, prefetchLabel, bitrateLabel, rebufferLabel, loopWatchLabel]
|
|
67
|
-
for label in labels {
|
|
68
|
-
label.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)
|
|
69
|
-
label.textColor = .white
|
|
70
|
-
label.numberOfLines = 1
|
|
71
|
-
}
|
|
72
|
-
headerLabel.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .bold)
|
|
73
|
-
|
|
74
|
-
stackView.axis = .vertical
|
|
75
|
-
stackView.spacing = 4
|
|
76
|
-
stackView.alignment = .leading
|
|
77
|
-
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
78
|
-
addSubview(stackView)
|
|
79
|
-
|
|
80
|
-
for label in labels {
|
|
81
|
-
stackView.addArrangedSubview(label)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Swipe curve picker
|
|
85
|
-
for (i, curve) in SwipeCurve.allCases.enumerated() {
|
|
86
|
-
curveSegment.insertSegment(withTitle: curve.label, at: i, animated: false)
|
|
87
|
-
}
|
|
88
|
-
curveSegment.selectedSegmentIndex = SwipeCurve.quarticOut.rawValue
|
|
89
|
-
curveSegment.setTitleTextAttributes([.foregroundColor: UIColor.white, .font: UIFont.monospacedSystemFont(ofSize: 10, weight: .medium)], for: .normal)
|
|
90
|
-
curveSegment.setTitleTextAttributes([.foregroundColor: UIColor.black], for: .selected)
|
|
91
|
-
curveSegment.backgroundColor = UIColor.white.withAlphaComponent(0.15)
|
|
92
|
-
curveSegment.selectedSegmentTintColor = UIColor.white.withAlphaComponent(0.9)
|
|
93
|
-
curveSegment.addTarget(self, action: #selector(curveChanged), for: .valueChanged)
|
|
94
|
-
stackView.addArrangedSubview(curveSegment)
|
|
95
|
-
|
|
96
|
-
// Separator
|
|
97
|
-
separatorView.backgroundColor = UIColor.white.withAlphaComponent(0.3)
|
|
98
|
-
separatorView.translatesAutoresizingMaskIntoConstraints = false
|
|
99
|
-
separatorView.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
|
|
100
|
-
stackView.addArrangedSubview(separatorView)
|
|
101
|
-
|
|
102
|
-
// Adjacent header (tap target)
|
|
103
|
-
adjacentHeader.font = UIFont.monospacedSystemFont(ofSize: 12, weight: .bold)
|
|
104
|
-
adjacentHeader.textColor = .white
|
|
105
|
-
adjacentHeader.text = "▶ Adjacent Players"
|
|
106
|
-
adjacentHeader.isUserInteractionEnabled = true
|
|
107
|
-
adjacentHeader.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(toggleAdjacent)))
|
|
108
|
-
stackView.addArrangedSubview(adjacentHeader)
|
|
109
|
-
|
|
110
|
-
// Adjacent player labels (initially hidden)
|
|
111
|
-
let adjLabels = [prevHeaderLabel, prevDetailLabel, nextHeaderLabel, nextDetailLabel]
|
|
112
|
-
for label in adjLabels {
|
|
113
|
-
label.font = UIFont.monospacedSystemFont(ofSize: 11, weight: .regular)
|
|
114
|
-
label.textColor = UIColor.white.withAlphaComponent(0.85)
|
|
115
|
-
label.numberOfLines = 1
|
|
116
|
-
label.isHidden = true
|
|
117
|
-
stackView.addArrangedSubview(label)
|
|
118
|
-
}
|
|
119
|
-
adjacentLabels = adjLabels
|
|
120
|
-
|
|
121
|
-
prevHeaderLabel.text = "▲ Prev — not assigned"
|
|
122
|
-
prevDetailLabel.text = ""
|
|
123
|
-
nextHeaderLabel.text = "▼ Next — not assigned"
|
|
124
|
-
nextDetailLabel.text = ""
|
|
125
|
-
|
|
126
|
-
NSLayoutConstraint.activate([
|
|
127
|
-
stackView.topAnchor.constraint(equalTo: topAnchor, constant: 10),
|
|
128
|
-
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
|
|
129
|
-
stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
|
|
130
|
-
])
|
|
131
|
-
|
|
132
|
-
// Initial state
|
|
133
|
-
update(DebugMetrics())
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) {
|
|
137
|
-
guard let parent = superview else { return }
|
|
138
|
-
switch gesture.state {
|
|
139
|
-
case .began:
|
|
140
|
-
dragOffset = gesture.location(in: self)
|
|
141
|
-
case .changed:
|
|
142
|
-
let location = gesture.location(in: parent)
|
|
143
|
-
var newCenter = CGPoint(
|
|
144
|
-
x: location.x - dragOffset.x + bounds.width / 2,
|
|
145
|
-
y: location.y - dragOffset.y + bounds.height / 2
|
|
146
|
-
)
|
|
147
|
-
// Keep panel within parent bounds
|
|
148
|
-
let hw = bounds.width / 2, hh = bounds.height / 2
|
|
149
|
-
newCenter.x = max(hw, min(parent.bounds.width - hw, newCenter.x))
|
|
150
|
-
newCenter.y = max(hh, min(parent.bounds.height - hh, newCenter.y))
|
|
151
|
-
center = newCenter
|
|
152
|
-
default:
|
|
153
|
-
break
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
@objc private func curveChanged(_ sender: UISegmentedControl) {
|
|
158
|
-
guard let curve = SwipeCurve(rawValue: sender.selectedSegmentIndex) else { return }
|
|
159
|
-
Self.selectedSwipeCurve = curve
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
@objc private func toggleAdjacent() {
|
|
163
|
-
isAdjacentExpanded.toggle()
|
|
164
|
-
adjacentHeader.text = isAdjacentExpanded ? "▼ Adjacent Players" : "▶ Adjacent Players"
|
|
165
|
-
|
|
166
|
-
UIView.animate(withDuration: 0.25) {
|
|
167
|
-
for label in self.adjacentLabels {
|
|
168
|
-
label.isHidden = !self.isAdjacentExpanded
|
|
169
|
-
label.alpha = self.isAdjacentExpanded ? 1 : 0
|
|
170
|
-
}
|
|
171
|
-
// Resize panel
|
|
172
|
-
var newFrame = self.frame
|
|
173
|
-
let baseHeight = Self.panelHeight
|
|
174
|
-
newFrame.size.height = self.isAdjacentExpanded
|
|
175
|
-
? baseHeight + Self.expandedExtraHeight
|
|
176
|
-
: baseHeight
|
|
177
|
-
self.frame = newFrame
|
|
178
|
-
self.layoutIfNeeded()
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
func subscribe(to subject: CurrentValueSubject<DebugMetrics, Never>) {
|
|
183
|
-
cancellable = subject
|
|
184
|
-
.receive(on: DispatchQueue.main)
|
|
185
|
-
.sink { [weak self] metrics in
|
|
186
|
-
self?.update(metrics)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
func subscribeAdjacent(
|
|
191
|
-
prev: CurrentValueSubject<DebugMetrics, Never>,
|
|
192
|
-
next: CurrentValueSubject<DebugMetrics, Never>
|
|
193
|
-
) {
|
|
194
|
-
prevCancellable = prev
|
|
195
|
-
.receive(on: DispatchQueue.main)
|
|
196
|
-
.sink { [weak self] metrics in
|
|
197
|
-
self?.updateAdjacent(metrics, slot: .previous)
|
|
198
|
-
}
|
|
199
|
-
nextCancellable = next
|
|
200
|
-
.receive(on: DispatchQueue.main)
|
|
201
|
-
.sink { [weak self] metrics in
|
|
202
|
-
self?.updateAdjacent(metrics, slot: .next)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private func updateAdjacent(_ m: DebugMetrics, slot: AdjacentSlot) {
|
|
207
|
-
let headerLabel = slot == .previous ? prevHeaderLabel : nextHeaderLabel
|
|
208
|
-
let detailLabel = slot == .previous ? prevDetailLabel : nextDetailLabel
|
|
209
|
-
let arrow = slot == .previous ? "▲ Prev" : "▼ Next"
|
|
210
|
-
|
|
211
|
-
guard m.contentId != nil else {
|
|
212
|
-
headerLabel.text = "\(arrow) — not assigned"
|
|
213
|
-
detailLabel.text = ""
|
|
214
|
-
return
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
let id = m.contentId.map { String($0.prefix(8)) } ?? "—"
|
|
218
|
-
let stateIcon: String
|
|
219
|
-
switch m.playerState {
|
|
220
|
-
case .playing: stateIcon = "▶"
|
|
221
|
-
case .paused: stateIcon = "⏸"
|
|
222
|
-
case .buffering: stateIcon = "⏳"
|
|
223
|
-
default: stateIcon = "○"
|
|
224
|
-
}
|
|
225
|
-
headerLabel.text = "\(arrow) #\(m.feedIndex) \(id) \(stateIcon) \(m.playerState)"
|
|
226
|
-
|
|
227
|
-
let buf = String(format: "%.1f", m.bufferAhead)
|
|
228
|
-
let preroll = m.prerollCompleted ? "✓" : "✗"
|
|
229
|
-
let mbps = m.observedBitrateBps / 1_000_000
|
|
230
|
-
var detail = " buf: \(buf)s preroll: \(preroll)"
|
|
231
|
-
if mbps > 0 {
|
|
232
|
-
detail += String(format: " %.1f Mbps", mbps)
|
|
233
|
-
}
|
|
234
|
-
if m.videoWidth > 0 {
|
|
235
|
-
let codec = m.codec ?? ""
|
|
236
|
-
detail += " \(m.videoWidth)×\(m.videoHeight) \(codec)"
|
|
237
|
-
}
|
|
238
|
-
detailLabel.text = detail
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private func update(_ m: DebugMetrics) {
|
|
242
|
-
let id = m.contentId.map { String($0.prefix(8)) } ?? "—"
|
|
243
|
-
let stateIcon: String
|
|
244
|
-
switch m.playerState {
|
|
245
|
-
case .playing: stateIcon = "▶"
|
|
246
|
-
case .paused: stateIcon = "⏸"
|
|
247
|
-
case .buffering: stateIcon = "⏳"
|
|
248
|
-
case .seeking: stateIcon = "⏩"
|
|
249
|
-
case .error: stateIcon = "⚠️"
|
|
250
|
-
default: stateIcon = "○"
|
|
251
|
-
}
|
|
252
|
-
headerLabel.text = "#\(m.feedIndex) \(id) \(stateIcon) \(m.playerState)"
|
|
253
|
-
|
|
254
|
-
let cur = String(format: "%.1f", m.currentTime)
|
|
255
|
-
let dur = String(format: "%.1f", m.duration)
|
|
256
|
-
positionLabel.text = "Position: \(cur)s / \(dur)s"
|
|
257
|
-
|
|
258
|
-
if let intent = m.intentToFrameMs {
|
|
259
|
-
intentTtffLabel.text = "Intent: \(intent)ms"
|
|
260
|
-
} else {
|
|
261
|
-
intentTtffLabel.text = "Intent: N/A"
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if let transition = m.transitionToFrameMs {
|
|
265
|
-
let net = m.networkMs.map { "\($0)" } ?? "?"
|
|
266
|
-
let dec = m.decodeMs.map { "\($0)" } ?? "?"
|
|
267
|
-
transitionTtffLabel.text = "Transition: \(transition)ms (net: \(net) + dec: \(dec))"
|
|
268
|
-
} else {
|
|
269
|
-
transitionTtffLabel.text = "Transition: measuring..."
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
let preroll = m.prerollCompleted ? "✓" : "✗"
|
|
273
|
-
let buffer = String(format: "%.1f", m.bufferAhead)
|
|
274
|
-
prerollBufferLabel.text = "Preroll: \(preroll) Buffer: \(buffer)s"
|
|
275
|
-
|
|
276
|
-
let codec = m.codec ?? "—"
|
|
277
|
-
if m.videoWidth > 0 {
|
|
278
|
-
videoQualityLabel.text = "Video: \(m.videoWidth)×\(m.videoHeight) \(codec)"
|
|
279
|
-
} else {
|
|
280
|
-
videoQualityLabel.text = "Video: —"
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
thumbnailLabel.text = "Thumbnail: \(m.thumbnailVisible ? "visible" : "removed")"
|
|
284
|
-
prefetchLabel.text = "Prefetch: \(m.thumbsCachedAhead)/\(m.metadataAhead) thumbs \(m.metadataAhead) meta"
|
|
285
|
-
|
|
286
|
-
let obsMbps = m.observedBitrateBps / 1_000_000
|
|
287
|
-
let indMbps = m.indicatedBitrateBps / 1_000_000
|
|
288
|
-
if obsMbps > 0 {
|
|
289
|
-
bitrateLabel.text = String(format: "Bitrate: %.1f Mbps (ind: %.1f)", obsMbps, indMbps)
|
|
290
|
-
} else {
|
|
291
|
-
bitrateLabel.text = "Bitrate: —"
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if let lastMs = m.lastRebufferDurationMs {
|
|
295
|
-
rebufferLabel.text = "Rebuffers: \(m.rebufferCount) (last: \(lastMs)ms)"
|
|
296
|
-
} else {
|
|
297
|
-
rebufferLabel.text = "Rebuffers: \(m.rebufferCount)"
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
loopWatchLabel.text = "Loops: \(m.loopCount)"
|
|
301
|
-
}
|
|
302
|
-
}
|