@movementinfra/expo-twostep-video 0.1.11 → 0.1.12
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/README.md +221 -0
- package/ios/ExpoTwoStepVideoView.swift +58 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,7 @@ function VideoEditor() {
|
|
|
126
126
|
| `adjustSpeed({ assetId, speed, startTime?, endTime? })` | Change speed | `VideoComposition` |
|
|
127
127
|
| `loopSegment({ assetId, startTime, endTime, loopCount })` | Loop a segment | `LoopResult` |
|
|
128
128
|
| `transformVideo({ assetId, speed?, mirrorAxis?, startTime?, endTime? })` | Combined transform | `VideoComposition` |
|
|
129
|
+
| `panZoomVideo({ assetId, panX, panY, zoomLevel })` | Apply pan/zoom crop | `VideoComposition` |
|
|
129
130
|
| `generateThumbnails({ assetId, times, size? })` | Extract frames | `string[]` (base64) |
|
|
130
131
|
| `exportVideo({ compositionId, quality? })` | Export composition | `ExportResult` |
|
|
131
132
|
| `exportAsset({ assetId, quality? })` | Export asset directly | `ExportResult` |
|
|
@@ -420,6 +421,7 @@ function VideoPlayer({ compositionId }: { compositionId: string }) {
|
|
|
420
421
|
| `onProgress` | `function` | Called periodically with `{ currentTime, duration, progress }` |
|
|
421
422
|
| `onEnd` | `function` | Called when video ends (not called if `loop` is true) |
|
|
422
423
|
| `onError` | `function` | Called on playback error |
|
|
424
|
+
| `onPanZoomChange` | `function` | Called when user pinches/pans with `{ panX, panY, zoomLevel }` |
|
|
423
425
|
|
|
424
426
|
#### Player Methods (via ref)
|
|
425
427
|
|
|
@@ -439,6 +441,205 @@ await playerRef.current?.seek(10.5);
|
|
|
439
441
|
await playerRef.current?.replay();
|
|
440
442
|
```
|
|
441
443
|
|
|
444
|
+
### Pan & Zoom
|
|
445
|
+
|
|
446
|
+
The `TwoStepVideoView` component supports interactive pan and zoom gestures, allowing users to zoom into video content and pan around while zoomed.
|
|
447
|
+
|
|
448
|
+
#### Gesture Controls
|
|
449
|
+
|
|
450
|
+
| Gesture | Action |
|
|
451
|
+
|---------|--------|
|
|
452
|
+
| Pinch (2 fingers) | Zoom in/out (1x to 5x) |
|
|
453
|
+
| Drag (2 fingers) | Pan around when zoomed in |
|
|
454
|
+
|
|
455
|
+
#### Pan/Zoom Events
|
|
456
|
+
|
|
457
|
+
Listen for pan/zoom state changes with the `onPanZoomChange` prop:
|
|
458
|
+
|
|
459
|
+
```tsx
|
|
460
|
+
import { TwoStepVideoView, PanZoomState } from 'expo-twostep-video';
|
|
461
|
+
|
|
462
|
+
function VideoWithZoom() {
|
|
463
|
+
const [panZoom, setPanZoom] = useState<PanZoomState>({
|
|
464
|
+
panX: 0,
|
|
465
|
+
panY: 0,
|
|
466
|
+
zoomLevel: 1,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
<View>
|
|
471
|
+
<TwoStepVideoView
|
|
472
|
+
assetId={asset.id}
|
|
473
|
+
onPanZoomChange={(e) => setPanZoom(e.nativeEvent)}
|
|
474
|
+
style={{ width: '100%', height: 300 }}
|
|
475
|
+
/>
|
|
476
|
+
|
|
477
|
+
{panZoom.zoomLevel > 1 && (
|
|
478
|
+
<Text>
|
|
479
|
+
Zoom: {panZoom.zoomLevel.toFixed(1)}x |
|
|
480
|
+
Pan: ({panZoom.panX.toFixed(2)}, {panZoom.panY.toFixed(2)})
|
|
481
|
+
</Text>
|
|
482
|
+
)}
|
|
483
|
+
</View>
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### Pan/Zoom Methods (via ref)
|
|
489
|
+
|
|
490
|
+
Control pan/zoom programmatically using the player ref:
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
const playerRef = useRef<TwoStepVideoViewRef>(null);
|
|
494
|
+
|
|
495
|
+
// Get current pan/zoom state
|
|
496
|
+
const state = await playerRef.current?.getPanZoomState();
|
|
497
|
+
// Returns: { panX: number, panY: number, zoomLevel: number }
|
|
498
|
+
|
|
499
|
+
// Set pan/zoom programmatically
|
|
500
|
+
await playerRef.current?.setPanZoomState({
|
|
501
|
+
panX: 0.5, // Pan right (range: -1 to 1)
|
|
502
|
+
panY: -0.3, // Pan up (range: -1 to 1)
|
|
503
|
+
zoomLevel: 2.0 // 2x zoom (range: 1 to 5)
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Reset to default (no zoom, centered)
|
|
507
|
+
await playerRef.current?.resetPanZoom();
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### Baking Pan/Zoom into Export
|
|
511
|
+
|
|
512
|
+
To permanently apply the pan/zoom transform to a video for export, use `panZoomVideo`:
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
import * as TwoStepVideo from 'expo-twostep-video';
|
|
516
|
+
|
|
517
|
+
// Apply pan/zoom as a permanent transformation
|
|
518
|
+
const composition = await TwoStepVideo.panZoomVideo({
|
|
519
|
+
assetId: asset.id,
|
|
520
|
+
panX: 0.3, // Pan position (-1 to 1)
|
|
521
|
+
panY: -0.2,
|
|
522
|
+
zoomLevel: 1.5, // Zoom level (1 to 5)
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// Export the cropped/zoomed video
|
|
526
|
+
const result = await TwoStepVideo.exportVideo({
|
|
527
|
+
compositionId: composition.id,
|
|
528
|
+
quality: 'high',
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
#### Complete Pan/Zoom Example
|
|
533
|
+
|
|
534
|
+
A full implementation with gesture preview and export:
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
import React, { useState, useRef } from 'react';
|
|
538
|
+
import { View, Button, Text, StyleSheet } from 'react-native';
|
|
539
|
+
import * as TwoStepVideo from 'expo-twostep-video';
|
|
540
|
+
import {
|
|
541
|
+
TwoStepVideoView,
|
|
542
|
+
TwoStepVideoViewRef,
|
|
543
|
+
PanZoomState,
|
|
544
|
+
} from 'expo-twostep-video';
|
|
545
|
+
|
|
546
|
+
function PanZoomEditor({ assetId }: { assetId: string }) {
|
|
547
|
+
const playerRef = useRef<TwoStepVideoViewRef>(null);
|
|
548
|
+
const [panZoom, setPanZoom] = useState<PanZoomState>({
|
|
549
|
+
panX: 0,
|
|
550
|
+
panY: 0,
|
|
551
|
+
zoomLevel: 1,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
const hasTransform = panZoom.zoomLevel > 1 ||
|
|
555
|
+
panZoom.panX !== 0 ||
|
|
556
|
+
panZoom.panY !== 0;
|
|
557
|
+
|
|
558
|
+
const handleReset = async () => {
|
|
559
|
+
await playerRef.current?.resetPanZoom();
|
|
560
|
+
setPanZoom({ panX: 0, panY: 0, zoomLevel: 1 });
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
const handleApplyAndExport = async () => {
|
|
564
|
+
// Bake the pan/zoom into a composition
|
|
565
|
+
const composition = await TwoStepVideo.panZoomVideo({
|
|
566
|
+
assetId,
|
|
567
|
+
panX: panZoom.panX,
|
|
568
|
+
panY: panZoom.panY,
|
|
569
|
+
zoomLevel: panZoom.zoomLevel,
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// Export the result
|
|
573
|
+
const result = await TwoStepVideo.exportVideo({
|
|
574
|
+
compositionId: composition.id,
|
|
575
|
+
quality: 'high',
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Reset the preview since transform is now baked in
|
|
579
|
+
await handleReset();
|
|
580
|
+
|
|
581
|
+
console.log('Exported to:', result.uri);
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
return (
|
|
585
|
+
<View style={styles.container}>
|
|
586
|
+
{/* Video Preview with Gestures */}
|
|
587
|
+
<TwoStepVideoView
|
|
588
|
+
ref={playerRef}
|
|
589
|
+
assetId={assetId}
|
|
590
|
+
onPanZoomChange={(e) => setPanZoom(e.nativeEvent)}
|
|
591
|
+
style={styles.video}
|
|
592
|
+
/>
|
|
593
|
+
|
|
594
|
+
{/* Gesture Hint */}
|
|
595
|
+
<Text style={styles.hint}>
|
|
596
|
+
Pinch to zoom, drag with 2 fingers to pan
|
|
597
|
+
</Text>
|
|
598
|
+
|
|
599
|
+
{/* Transform Info */}
|
|
600
|
+
{hasTransform && (
|
|
601
|
+
<View style={styles.info}>
|
|
602
|
+
<Text>Zoom: {panZoom.zoomLevel.toFixed(2)}x</Text>
|
|
603
|
+
<Text>Pan X: {panZoom.panX.toFixed(3)}</Text>
|
|
604
|
+
<Text>Pan Y: {panZoom.panY.toFixed(3)}</Text>
|
|
605
|
+
</View>
|
|
606
|
+
)}
|
|
607
|
+
|
|
608
|
+
{/* Controls */}
|
|
609
|
+
<View style={styles.buttons}>
|
|
610
|
+
<Button
|
|
611
|
+
title="Reset"
|
|
612
|
+
onPress={handleReset}
|
|
613
|
+
disabled={!hasTransform}
|
|
614
|
+
/>
|
|
615
|
+
<Button
|
|
616
|
+
title="Apply & Export"
|
|
617
|
+
onPress={handleApplyAndExport}
|
|
618
|
+
disabled={!hasTransform}
|
|
619
|
+
/>
|
|
620
|
+
</View>
|
|
621
|
+
</View>
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const styles = StyleSheet.create({
|
|
626
|
+
container: { flex: 1, padding: 16 },
|
|
627
|
+
video: { width: '100%', height: 300, backgroundColor: '#000' },
|
|
628
|
+
hint: { textAlign: 'center', color: '#666', marginTop: 8 },
|
|
629
|
+
info: { padding: 12, backgroundColor: '#f0f0f0', borderRadius: 8, marginTop: 12 },
|
|
630
|
+
buttons: { flexDirection: 'row', justifyContent: 'space-around', marginTop: 16 },
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
export default PanZoomEditor;
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
#### Pan/Zoom Notes
|
|
637
|
+
|
|
638
|
+
- **Zoom range**: 1x (no zoom) to 5x by default
|
|
639
|
+
- **Pan range**: -1 to 1 on both axes (only effective when zoomed in)
|
|
640
|
+
- **Pan constraint**: Pan is automatically constrained to keep video content visible at the current zoom level
|
|
641
|
+
- **Simulator testing**: Multi-touch gestures work best on real devices. The iOS Simulator uses Option+drag for pinch, which can be unreliable
|
|
642
|
+
|
|
442
643
|
### Events
|
|
443
644
|
|
|
444
645
|
#### `addExportProgressListener(callback)`
|
|
@@ -570,6 +771,7 @@ interface TwoStepVideoViewProps {
|
|
|
570
771
|
onProgress?: (event: { nativeEvent: ProgressEvent }) => void;
|
|
571
772
|
onEnd?: (event: { nativeEvent: {} }) => void;
|
|
572
773
|
onError?: (event: { nativeEvent: ErrorEvent }) => void;
|
|
774
|
+
onPanZoomChange?: (event: { nativeEvent: PanZoomState }) => void;
|
|
573
775
|
style?: ViewStyle;
|
|
574
776
|
}
|
|
575
777
|
|
|
@@ -578,6 +780,25 @@ interface TwoStepVideoViewRef {
|
|
|
578
780
|
pause: () => Promise<void>;
|
|
579
781
|
seek: (time: number) => Promise<void>;
|
|
580
782
|
replay: () => Promise<void>;
|
|
783
|
+
getPanZoomState: () => Promise<PanZoomState>;
|
|
784
|
+
setPanZoomState: (state: Partial<PanZoomState>) => Promise<void>;
|
|
785
|
+
resetPanZoom: () => Promise<void>;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Pan/Zoom types
|
|
789
|
+
interface PanZoomState {
|
|
790
|
+
panX: number; // Horizontal pan (-1 to 1, 0 = center)
|
|
791
|
+
panY: number; // Vertical pan (-1 to 1, 0 = center)
|
|
792
|
+
zoomLevel: number; // Zoom level (1 to 5, 1 = no zoom)
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
interface PanZoomVideoOptions {
|
|
796
|
+
assetId: string;
|
|
797
|
+
panX: number;
|
|
798
|
+
panY: number;
|
|
799
|
+
zoomLevel: number;
|
|
800
|
+
startTime?: number;
|
|
801
|
+
endTime?: number;
|
|
581
802
|
}
|
|
582
803
|
```
|
|
583
804
|
|
|
@@ -42,8 +42,7 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
42
42
|
private var lastPinchScale: CGFloat = 1.0
|
|
43
43
|
|
|
44
44
|
/// Starting pan position when gesture begins
|
|
45
|
-
private var
|
|
46
|
-
private var panStartY: CGFloat = 0.0
|
|
45
|
+
private var panStartPosition: CGPoint = .zero
|
|
47
46
|
|
|
48
47
|
/// Minimum zoom level (configurable)
|
|
49
48
|
var minZoom: CGFloat = 1.0
|
|
@@ -55,6 +54,25 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
55
54
|
private var pinchGesture: UIPinchGestureRecognizer?
|
|
56
55
|
private var panGesture: UIPanGestureRecognizer?
|
|
57
56
|
|
|
57
|
+
// MARK: - Pan/Zoom Helpers
|
|
58
|
+
|
|
59
|
+
/// Maximum pan amount allowed at current zoom level
|
|
60
|
+
/// At zoom 2x, this is 0.5 (can pan halfway). At zoom 1x, this is 0.
|
|
61
|
+
private var maxPanAmount: CGFloat {
|
|
62
|
+
guard currentZoom > 1.0 else { return 0 }
|
|
63
|
+
return (currentZoom - 1.0) / currentZoom
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Clamp a value between min and max bounds
|
|
67
|
+
private func clamp(_ value: CGFloat, min minValue: CGFloat, max maxValue: CGFloat) -> CGFloat {
|
|
68
|
+
return Swift.min(Swift.max(value, minValue), maxValue)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Whether pan/zoom is at the default (untransformed) state
|
|
72
|
+
private var isAtDefaultTransform: Bool {
|
|
73
|
+
return currentZoom == 1.0 && currentPanX == 0 && currentPanY == 0
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
// MARK: - Initialization
|
|
59
77
|
|
|
60
78
|
required init(appContext: AppContext? = nil) {
|
|
@@ -183,19 +201,16 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
183
201
|
case .began:
|
|
184
202
|
lastPinchScale = 1.0
|
|
185
203
|
case .changed:
|
|
186
|
-
// Calculate incremental scale change
|
|
204
|
+
// Calculate incremental scale change from last gesture update
|
|
187
205
|
let scaleChange = gesture.scale / lastPinchScale
|
|
188
206
|
lastPinchScale = gesture.scale
|
|
189
207
|
|
|
190
|
-
// Apply to
|
|
191
|
-
|
|
192
|
-
currentZoom = min(max(newZoom, minZoom), maxZoom)
|
|
208
|
+
// Apply zoom with clamping to valid range
|
|
209
|
+
currentZoom = clamp(currentZoom * scaleChange, min: minZoom, max: maxZoom)
|
|
193
210
|
|
|
194
|
-
// Constrain pan when zoom
|
|
211
|
+
// Constrain pan (may need adjustment when zoom decreases)
|
|
195
212
|
constrainPan()
|
|
196
|
-
|
|
197
|
-
updateLayerTransform()
|
|
198
|
-
emitPanZoomChange()
|
|
213
|
+
applyTransformAndNotify()
|
|
199
214
|
case .ended, .cancelled:
|
|
200
215
|
lastPinchScale = 1.0
|
|
201
216
|
default:
|
|
@@ -209,29 +224,23 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
209
224
|
|
|
210
225
|
switch gesture.state {
|
|
211
226
|
case .began:
|
|
212
|
-
|
|
213
|
-
panStartY = currentPanY
|
|
227
|
+
panStartPosition = CGPoint(x: currentPanX, y: currentPanY)
|
|
214
228
|
case .changed:
|
|
215
229
|
let translation = gesture.translation(in: self)
|
|
216
230
|
|
|
217
|
-
// Convert translation to normalized pan
|
|
218
|
-
//
|
|
219
|
-
let
|
|
220
|
-
|
|
231
|
+
// Convert screen translation to normalized pan delta
|
|
232
|
+
// Dragging full width changes pan by 2x the available pan range
|
|
233
|
+
let panDelta = CGPoint(
|
|
234
|
+
x: (translation.x / bounds.width) * 2 * maxPanAmount,
|
|
235
|
+
y: (translation.y / bounds.height) * 2 * maxPanAmount
|
|
236
|
+
)
|
|
221
237
|
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
// Apply delta from start position (negative because dragging right shows left content)
|
|
239
|
+
currentPanX = panStartPosition.x - panDelta.x
|
|
240
|
+
currentPanY = panStartPosition.y - panDelta.y
|
|
225
241
|
|
|
226
|
-
// Scale by available pan range and apply
|
|
227
|
-
currentPanX = panStartX - normalizedDeltaX * 2 * availablePanX
|
|
228
|
-
currentPanY = panStartY - normalizedDeltaY * 2 * availablePanY
|
|
229
|
-
|
|
230
|
-
// Constrain pan
|
|
231
242
|
constrainPan()
|
|
232
|
-
|
|
233
|
-
updateLayerTransform()
|
|
234
|
-
emitPanZoomChange()
|
|
243
|
+
applyTransformAndNotify()
|
|
235
244
|
case .ended, .cancelled:
|
|
236
245
|
break
|
|
237
246
|
default:
|
|
@@ -239,39 +248,35 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
239
248
|
}
|
|
240
249
|
}
|
|
241
250
|
|
|
242
|
-
/// Constrain pan values
|
|
251
|
+
/// Constrain pan values to keep content visible at current zoom level
|
|
243
252
|
private func constrainPan() {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
currentPanX = min(max(currentPanX, -maxPanAmount), maxPanAmount)
|
|
249
|
-
currentPanY = min(max(currentPanY, -maxPanAmount), maxPanAmount)
|
|
253
|
+
let limit = maxPanAmount
|
|
254
|
+
currentPanX = clamp(currentPanX, min: -limit, max: limit)
|
|
255
|
+
currentPanY = clamp(currentPanY, min: -limit, max: limit)
|
|
256
|
+
}
|
|
250
257
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
258
|
+
/// Apply transform to layer and notify JS of the change
|
|
259
|
+
private func applyTransformAndNotify() {
|
|
260
|
+
updateLayerTransform()
|
|
261
|
+
emitPanZoomChange()
|
|
256
262
|
}
|
|
257
263
|
|
|
258
264
|
/// Apply the current pan/zoom transform to the player layer
|
|
259
265
|
private func updateLayerTransform() {
|
|
260
266
|
guard let layer = playerLayer else { return }
|
|
261
267
|
|
|
262
|
-
|
|
268
|
+
// Start with identity and apply zoom
|
|
269
|
+
var transform = CATransform3DScale(CATransform3DIdentity, currentZoom, currentZoom, 1.0)
|
|
263
270
|
|
|
264
|
-
// Apply
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
let translateX = -currentPanX * bounds.width * (currentZoom - 1.0) / (2.0 * currentZoom)
|
|
269
|
-
let translateY = -currentPanY * bounds.height * (currentZoom - 1.0) / (2.0 * currentZoom)
|
|
271
|
+
// Apply pan translation (scaled for zoomed coordinate space)
|
|
272
|
+
// Formula: translate by half the extra visible area in the zoom direction
|
|
273
|
+
let translateX = -currentPanX * bounds.width * maxPanAmount / 2.0
|
|
274
|
+
let translateY = -currentPanY * bounds.height * maxPanAmount / 2.0
|
|
270
275
|
transform = CATransform3DTranslate(transform, translateX, translateY, 0)
|
|
271
276
|
|
|
272
|
-
// Apply
|
|
277
|
+
// Apply without implicit animations for responsive feel
|
|
273
278
|
CATransaction.begin()
|
|
274
|
-
CATransaction.setDisableActions(true)
|
|
279
|
+
CATransaction.setDisableActions(true)
|
|
275
280
|
layer.transform = transform
|
|
276
281
|
CATransaction.commit()
|
|
277
282
|
}
|
|
@@ -299,7 +304,7 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
299
304
|
/// Set the pan/zoom state programmatically
|
|
300
305
|
func setPanZoomState(panX: CGFloat?, panY: CGFloat?, zoomLevel: CGFloat?) {
|
|
301
306
|
if let zoom = zoomLevel {
|
|
302
|
-
currentZoom =
|
|
307
|
+
currentZoom = clamp(zoom, min: minZoom, max: maxZoom)
|
|
303
308
|
}
|
|
304
309
|
if let x = panX {
|
|
305
310
|
currentPanX = x
|
|
@@ -309,8 +314,7 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
309
314
|
}
|
|
310
315
|
|
|
311
316
|
constrainPan()
|
|
312
|
-
|
|
313
|
-
emitPanZoomChange()
|
|
317
|
+
applyTransformAndNotify()
|
|
314
318
|
}
|
|
315
319
|
|
|
316
320
|
/// Reset pan/zoom to default state
|
|
@@ -318,15 +322,14 @@ class ExpoTwoStepVideoView: ExpoView {
|
|
|
318
322
|
currentZoom = 1.0
|
|
319
323
|
currentPanX = 0.0
|
|
320
324
|
currentPanY = 0.0
|
|
321
|
-
|
|
322
|
-
emitPanZoomChange()
|
|
325
|
+
applyTransformAndNotify()
|
|
323
326
|
}
|
|
324
327
|
|
|
325
328
|
override func layoutSubviews() {
|
|
326
329
|
super.layoutSubviews()
|
|
327
330
|
playerLayer?.frame = bounds
|
|
328
|
-
// Reapply transform after frame changes
|
|
329
|
-
if
|
|
331
|
+
// Reapply transform after frame changes (only if not at default)
|
|
332
|
+
if !isAtDefaultTransform {
|
|
330
333
|
updateLayerTransform()
|
|
331
334
|
}
|
|
332
335
|
}
|