@shopify/react-native-skia 1.3.1 → 1.3.3
Sign up to get free protection for your applications and to get access to all the features.
- package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp +72 -1
- package/android/cpp/rnskia-android/RNSkAndroidVideo.h +5 -0
- package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java +89 -8
- package/cpp/api/JsiVideo.h +37 -5
- package/cpp/rnskia/RNSkVideo.h +5 -0
- package/ios/RNSkia-iOS/RNSkiOSVideo.h +14 -3
- package/ios/RNSkia-iOS/RNSkiOSVideo.mm +78 -60
- package/lib/commonjs/dom/nodes/datatypes/Fitting.js +42 -30
- package/lib/commonjs/dom/nodes/datatypes/Fitting.js.map +1 -1
- package/lib/commonjs/external/reanimated/useVideo.d.ts +14 -5
- package/lib/commonjs/external/reanimated/useVideo.js +90 -58
- package/lib/commonjs/external/reanimated/useVideo.js.map +1 -1
- package/lib/commonjs/renderer/components/shapes/FitBox.d.ts +2 -10
- package/lib/commonjs/renderer/components/shapes/FitBox.js +32 -3
- package/lib/commonjs/renderer/components/shapes/FitBox.js.map +1 -1
- package/lib/commonjs/skia/core/Matrix.js +5 -1
- package/lib/commonjs/skia/core/Matrix.js.map +1 -1
- package/lib/commonjs/skia/types/Matrix.js +2 -0
- package/lib/commonjs/skia/types/Matrix.js.map +1 -1
- package/lib/commonjs/skia/types/Video/Video.d.ts +9 -0
- package/lib/commonjs/skia/types/Video/Video.js.map +1 -1
- package/lib/commonjs/skia/types/index.d.ts +1 -0
- package/lib/commonjs/skia/types/index.js +11 -0
- package/lib/commonjs/skia/types/index.js.map +1 -1
- package/lib/module/dom/nodes/datatypes/Fitting.js +41 -29
- package/lib/module/dom/nodes/datatypes/Fitting.js.map +1 -1
- package/lib/module/external/reanimated/useVideo.d.ts +14 -5
- package/lib/module/external/reanimated/useVideo.js +91 -59
- package/lib/module/external/reanimated/useVideo.js.map +1 -1
- package/lib/module/renderer/components/shapes/FitBox.d.ts +2 -10
- package/lib/module/renderer/components/shapes/FitBox.js +32 -3
- package/lib/module/renderer/components/shapes/FitBox.js.map +1 -1
- package/lib/module/skia/core/Matrix.js +5 -1
- package/lib/module/skia/core/Matrix.js.map +1 -1
- package/lib/module/skia/types/Matrix.js +2 -0
- package/lib/module/skia/types/Matrix.js.map +1 -1
- package/lib/module/skia/types/Video/Video.d.ts +9 -0
- package/lib/module/skia/types/Video/Video.js.map +1 -1
- package/lib/module/skia/types/index.d.ts +1 -0
- package/lib/module/skia/types/index.js +1 -0
- package/lib/module/skia/types/index.js.map +1 -1
- package/lib/typescript/src/external/reanimated/useVideo.d.ts +14 -5
- package/lib/typescript/src/renderer/components/shapes/FitBox.d.ts +2 -10
- package/lib/typescript/src/skia/types/Video/Video.d.ts +9 -0
- package/lib/typescript/src/skia/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/dom/nodes/datatypes/Fitting.ts +28 -21
- package/src/external/reanimated/useVideo.ts +86 -73
- package/src/renderer/components/shapes/FitBox.tsx +38 -4
- package/src/skia/core/Matrix.ts +4 -2
- package/src/skia/types/Matrix.ts +1 -0
- package/src/skia/types/Video/Video.ts +7 -0
- package/src/skia/types/index.ts +1 -0
@@ -1,21 +1,13 @@
|
|
1
1
|
import type { ReactNode } from "react";
|
2
2
|
import React from "react";
|
3
3
|
import type { Fit } from "../../../dom/nodes";
|
4
|
-
import type { SkRect } from "../../../skia/types";
|
4
|
+
import type { SkRect, Transforms3d } from "../../../skia/types";
|
5
5
|
interface FitProps {
|
6
6
|
fit?: Fit;
|
7
7
|
src: SkRect;
|
8
8
|
dst: SkRect;
|
9
9
|
children: ReactNode | ReactNode[];
|
10
10
|
}
|
11
|
-
export declare const fitbox: (fit: Fit, src: SkRect, dst: SkRect) =>
|
12
|
-
translateX: number;
|
13
|
-
}, {
|
14
|
-
translateY: number;
|
15
|
-
}, {
|
16
|
-
scaleX: number;
|
17
|
-
}, {
|
18
|
-
scaleY: number;
|
19
|
-
}];
|
11
|
+
export declare const fitbox: (fit: Fit, src: SkRect, dst: SkRect, rotation?: 0 | 90 | 180 | 270) => Transforms3d;
|
20
12
|
export declare const FitBox: ({ fit, src, dst, children }: FitProps) => React.JSX.Element;
|
21
13
|
export {};
|
@@ -1,8 +1,17 @@
|
|
1
1
|
import type { SkImage } from "../Image";
|
2
2
|
import type { SkJSIInstance } from "../JsiInstance";
|
3
|
+
export type VideoRotation = 0 | 90 | 180 | 270;
|
3
4
|
export interface Video extends SkJSIInstance<"Video"> {
|
4
5
|
duration(): number;
|
5
6
|
framerate(): number;
|
6
7
|
nextImage(): SkImage | null;
|
7
8
|
seek(time: number): void;
|
9
|
+
rotation(): VideoRotation;
|
10
|
+
size(): {
|
11
|
+
width: number;
|
12
|
+
height: number;
|
13
|
+
};
|
14
|
+
pause(): void;
|
15
|
+
play(): void;
|
16
|
+
setVolume(volume: number): void;
|
8
17
|
}
|
package/package.json
CHANGED
@@ -7,7 +7,7 @@
|
|
7
7
|
"setup-skia-web": "./scripts/setup-canvaskit.js"
|
8
8
|
},
|
9
9
|
"title": "React Native Skia",
|
10
|
-
"version": "1.3.
|
10
|
+
"version": "1.3.3",
|
11
11
|
"description": "High-performance React Native Graphics using Skia",
|
12
12
|
"main": "lib/module/index.js",
|
13
13
|
"react-native": "src/index.ts",
|
@@ -7,7 +7,10 @@ export interface Size {
|
|
7
7
|
height: number;
|
8
8
|
}
|
9
9
|
|
10
|
-
export const size = (width = 0, height = 0) =>
|
10
|
+
export const size = (width = 0, height = 0) => {
|
11
|
+
"worklet";
|
12
|
+
return { width, height };
|
13
|
+
};
|
11
14
|
|
12
15
|
export const rect2rect = (
|
13
16
|
src: SkRect,
|
@@ -18,6 +21,7 @@ export const rect2rect = (
|
|
18
21
|
{ scaleX: number },
|
19
22
|
{ scaleY: number }
|
20
23
|
] => {
|
24
|
+
"worklet";
|
21
25
|
const scaleX = dst.width / src.width;
|
22
26
|
const scaleY = dst.height / src.height;
|
23
27
|
const translateX = dst.x - src.x * scaleX;
|
@@ -25,30 +29,11 @@ export const rect2rect = (
|
|
25
29
|
return [{ translateX }, { translateY }, { scaleX }, { scaleY }];
|
26
30
|
};
|
27
31
|
|
28
|
-
export const fitRects = (
|
29
|
-
fit: Fit,
|
30
|
-
rect: SkRect,
|
31
|
-
{ x, y, width, height }: SkRect
|
32
|
-
) => {
|
33
|
-
const sizes = applyBoxFit(
|
34
|
-
fit,
|
35
|
-
{ width: rect.width, height: rect.height },
|
36
|
-
{ width, height }
|
37
|
-
);
|
38
|
-
const src = inscribe(sizes.src, rect);
|
39
|
-
const dst = inscribe(sizes.dst, {
|
40
|
-
x,
|
41
|
-
y,
|
42
|
-
width,
|
43
|
-
height,
|
44
|
-
});
|
45
|
-
return { src, dst };
|
46
|
-
};
|
47
|
-
|
48
32
|
const inscribe = (
|
49
33
|
{ width, height }: Size,
|
50
34
|
rect: { x: number; y: number; width: number; height: number }
|
51
35
|
) => {
|
36
|
+
"worklet";
|
52
37
|
const halfWidthDelta = (rect.width - width) / 2.0;
|
53
38
|
const halfHeightDelta = (rect.height - height) / 2.0;
|
54
39
|
return {
|
@@ -60,6 +45,7 @@ const inscribe = (
|
|
60
45
|
};
|
61
46
|
|
62
47
|
const applyBoxFit = (fit: Fit, input: Size, output: Size) => {
|
48
|
+
"worklet";
|
63
49
|
let src = size(),
|
64
50
|
dst = size();
|
65
51
|
if (
|
@@ -122,3 +108,24 @@ const applyBoxFit = (fit: Fit, input: Size, output: Size) => {
|
|
122
108
|
}
|
123
109
|
return { src, dst };
|
124
110
|
};
|
111
|
+
|
112
|
+
export const fitRects = (
|
113
|
+
fit: Fit,
|
114
|
+
rect: SkRect,
|
115
|
+
{ x, y, width, height }: SkRect
|
116
|
+
) => {
|
117
|
+
"worklet";
|
118
|
+
const sizes = applyBoxFit(
|
119
|
+
fit,
|
120
|
+
{ width: rect.width, height: rect.height },
|
121
|
+
{ width, height }
|
122
|
+
);
|
123
|
+
const src = inscribe(sizes.src, rect);
|
124
|
+
const dst = inscribe(sizes.dst, {
|
125
|
+
x,
|
126
|
+
y,
|
127
|
+
width,
|
128
|
+
height,
|
129
|
+
});
|
130
|
+
return { src, dst };
|
131
|
+
};
|
@@ -1,44 +1,58 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
useSharedValue,
|
4
|
-
type FrameInfo,
|
5
|
-
type SharedValue,
|
6
|
-
} from "react-native-reanimated";
|
7
|
-
import { useCallback, useEffect, useMemo } from "react";
|
1
|
+
import type { SharedValue, FrameInfo } from "react-native-reanimated";
|
2
|
+
import { useEffect, useMemo } from "react";
|
8
3
|
|
9
4
|
import { Skia } from "../../skia/Skia";
|
10
|
-
import type { SkImage } from "../../skia/types";
|
5
|
+
import type { SkImage, Video } from "../../skia/types";
|
11
6
|
import { Platform } from "../../Platform";
|
12
7
|
|
13
8
|
import Rea from "./ReanimatedProxy";
|
14
9
|
|
15
10
|
type Animated<T> = SharedValue<T> | T;
|
16
11
|
|
17
|
-
|
18
|
-
playbackSpeed: Animated<number>;
|
12
|
+
interface PlaybackOptions {
|
19
13
|
looping: Animated<boolean>;
|
20
14
|
paused: Animated<boolean>;
|
21
15
|
seek: Animated<number | null>;
|
22
|
-
|
16
|
+
volume: Animated<number>;
|
23
17
|
}
|
24
18
|
|
19
|
+
const setFrame = (video: Video, currentFrame: SharedValue<SkImage | null>) => {
|
20
|
+
"worklet";
|
21
|
+
const img = video.nextImage();
|
22
|
+
if (img) {
|
23
|
+
if (currentFrame.value) {
|
24
|
+
currentFrame.value.dispose();
|
25
|
+
}
|
26
|
+
if (Platform.OS === "android") {
|
27
|
+
currentFrame.value = img.makeNonTextureImage();
|
28
|
+
} else {
|
29
|
+
currentFrame.value = img;
|
30
|
+
}
|
31
|
+
}
|
32
|
+
};
|
33
|
+
|
25
34
|
const defaultOptions = {
|
26
|
-
playbackSpeed: 1,
|
27
35
|
looping: true,
|
28
36
|
paused: false,
|
29
37
|
seek: null,
|
30
38
|
currentTime: 0,
|
39
|
+
volume: 0,
|
31
40
|
};
|
32
41
|
|
33
42
|
const useOption = <T>(value: Animated<T>) => {
|
34
43
|
"worklet";
|
35
44
|
// TODO: only create defaultValue is needed (via makeMutable)
|
36
|
-
const defaultValue = useSharedValue(
|
45
|
+
const defaultValue = Rea.useSharedValue(
|
37
46
|
Rea.isSharedValue(value) ? value.value : value
|
38
47
|
);
|
39
48
|
return Rea.isSharedValue(value) ? value : defaultValue;
|
40
49
|
};
|
41
50
|
|
51
|
+
const disposeVideo = (video: Video | null) => {
|
52
|
+
"worklet";
|
53
|
+
video?.dispose();
|
54
|
+
};
|
55
|
+
|
42
56
|
export const useVideo = (
|
43
57
|
source: string | null,
|
44
58
|
userOptions?: Partial<PlaybackOptions>
|
@@ -47,84 +61,83 @@ export const useVideo = (
|
|
47
61
|
const isPaused = useOption(userOptions?.paused ?? defaultOptions.paused);
|
48
62
|
const looping = useOption(userOptions?.looping ?? defaultOptions.looping);
|
49
63
|
const seek = useOption(userOptions?.seek ?? defaultOptions.seek);
|
50
|
-
const
|
51
|
-
userOptions?.currentTime ?? defaultOptions.currentTime
|
52
|
-
);
|
53
|
-
const playbackSpeed = useOption(
|
54
|
-
userOptions?.playbackSpeed ?? defaultOptions.playbackSpeed
|
55
|
-
);
|
64
|
+
const volume = useOption(userOptions?.volume ?? defaultOptions.volume);
|
56
65
|
const currentFrame = Rea.useSharedValue<null | SkImage>(null);
|
66
|
+
const currentTime = Rea.useSharedValue(0);
|
57
67
|
const lastTimestamp = Rea.useSharedValue(-1);
|
58
|
-
const
|
59
|
-
|
60
|
-
const
|
61
|
-
const
|
62
|
-
const frameDuration =
|
63
|
-
|
64
|
-
|
68
|
+
const duration = useMemo(() => video?.duration() ?? 0, [video]);
|
69
|
+
const framerate = useMemo(() => video?.framerate() ?? 0, [video]);
|
70
|
+
const size = useMemo(() => video?.size() ?? { width: 0, height: 0 }, [video]);
|
71
|
+
const rotation = useMemo(() => video?.rotation() ?? 0, [video]);
|
72
|
+
const frameDuration = 1000 / framerate;
|
73
|
+
const currentFrameDuration = Math.floor(frameDuration);
|
74
|
+
Rea.useAnimatedReaction(
|
75
|
+
() => isPaused.value,
|
76
|
+
(paused) => {
|
77
|
+
if (paused) {
|
78
|
+
video?.pause();
|
79
|
+
} else {
|
80
|
+
lastTimestamp.value = -1;
|
81
|
+
video?.play();
|
82
|
+
}
|
83
|
+
}
|
84
|
+
);
|
85
|
+
Rea.useAnimatedReaction(
|
86
|
+
() => seek.value,
|
87
|
+
(value) => {
|
88
|
+
if (value !== null) {
|
89
|
+
video?.seek(value);
|
90
|
+
currentTime.value = value;
|
91
|
+
seek.value = null;
|
92
|
+
}
|
93
|
+
}
|
94
|
+
);
|
95
|
+
Rea.useAnimatedReaction(
|
96
|
+
() => volume.value,
|
97
|
+
(value) => {
|
98
|
+
video?.setVolume(value);
|
99
|
+
}
|
65
100
|
);
|
66
|
-
const disposeVideo = useCallback(() => {
|
67
|
-
"worklet";
|
68
|
-
video?.dispose();
|
69
|
-
}, [video]);
|
70
|
-
|
71
101
|
Rea.useFrameCallback((frameInfo: FrameInfo) => {
|
102
|
+
"worklet";
|
72
103
|
if (!video) {
|
73
104
|
return;
|
74
105
|
}
|
75
|
-
if (
|
76
|
-
video.seek(seek.value);
|
77
|
-
seek.value = null;
|
78
|
-
lastTimestamp.value = -1;
|
79
|
-
startTimestamp.value = -1;
|
80
|
-
}
|
81
|
-
if (isPaused.value && lastTimestamp.value !== -1) {
|
106
|
+
if (isPaused.value) {
|
82
107
|
return;
|
83
108
|
}
|
84
|
-
const
|
85
|
-
|
86
|
-
|
87
|
-
if (startTimestamp.value === -1) {
|
88
|
-
startTimestamp.value = timestamp;
|
109
|
+
const currentTimestamp = frameInfo.timestamp;
|
110
|
+
if (lastTimestamp.value === -1) {
|
111
|
+
lastTimestamp.value = currentTimestamp;
|
89
112
|
}
|
113
|
+
const delta = currentTimestamp - lastTimestamp.value;
|
90
114
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
if (currentTimestamp > duration && looping.value) {
|
97
|
-
video.seek(0);
|
98
|
-
startTimestamp.value = timestamp;
|
115
|
+
const isOver = currentTime.value + delta > duration;
|
116
|
+
if (isOver && looping.value) {
|
117
|
+
seek.value = 0;
|
118
|
+
currentTime.value = seek.value;
|
119
|
+
lastTimestamp.value = currentTimestamp;
|
99
120
|
}
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
);
|
105
|
-
const delta = Math.floor(timestamp - lastTimestamp.value);
|
106
|
-
if (lastTimestamp.value === -1 || delta >= currentFrameDuration) {
|
107
|
-
const img = video.nextImage();
|
108
|
-
if (img) {
|
109
|
-
if (currentFrame.value) {
|
110
|
-
currentFrame.value.dispose();
|
111
|
-
}
|
112
|
-
if (Platform.OS === "android") {
|
113
|
-
currentFrame.value = img.makeNonTextureImage();
|
114
|
-
} else {
|
115
|
-
currentFrame.value = img;
|
116
|
-
}
|
117
|
-
}
|
118
|
-
lastTimestamp.value = timestamp;
|
121
|
+
if (delta >= currentFrameDuration && !isOver) {
|
122
|
+
setFrame(video, currentFrame);
|
123
|
+
currentTime.value += delta;
|
124
|
+
lastTimestamp.value = currentTimestamp;
|
119
125
|
}
|
120
126
|
});
|
121
127
|
|
122
128
|
useEffect(() => {
|
123
129
|
return () => {
|
124
130
|
// TODO: should video simply be a shared value instead?
|
125
|
-
runOnUI(disposeVideo)();
|
131
|
+
Rea.runOnUI(disposeVideo)(video);
|
126
132
|
};
|
127
|
-
}, [
|
133
|
+
}, [video]);
|
128
134
|
|
129
|
-
return
|
135
|
+
return {
|
136
|
+
currentFrame,
|
137
|
+
currentTime,
|
138
|
+
duration,
|
139
|
+
framerate,
|
140
|
+
rotation,
|
141
|
+
size,
|
142
|
+
};
|
130
143
|
};
|
@@ -3,7 +3,7 @@ import React, { useMemo } from "react";
|
|
3
3
|
|
4
4
|
import type { Fit } from "../../../dom/nodes";
|
5
5
|
import { fitRects, rect2rect } from "../../../dom/nodes";
|
6
|
-
import type { SkRect } from "../../../skia/types";
|
6
|
+
import type { SkRect, Transforms3d } from "../../../skia/types";
|
7
7
|
import { Group } from "../Group";
|
8
8
|
|
9
9
|
interface FitProps {
|
@@ -13,9 +13,43 @@ interface FitProps {
|
|
13
13
|
children: ReactNode | ReactNode[];
|
14
14
|
}
|
15
15
|
|
16
|
-
export const fitbox = (
|
17
|
-
|
18
|
-
|
16
|
+
export const fitbox = (
|
17
|
+
fit: Fit,
|
18
|
+
src: SkRect,
|
19
|
+
dst: SkRect,
|
20
|
+
rotation: 0 | 90 | 180 | 270 = 0
|
21
|
+
) => {
|
22
|
+
"worklet";
|
23
|
+
const rects = fitRects(
|
24
|
+
fit,
|
25
|
+
rotation === 90 || rotation === 270
|
26
|
+
? { x: 0, y: 0, width: src.height, height: src.width }
|
27
|
+
: src,
|
28
|
+
dst
|
29
|
+
);
|
30
|
+
const result = rect2rect(rects.src, rects.dst);
|
31
|
+
if (rotation === 90) {
|
32
|
+
return [
|
33
|
+
...result,
|
34
|
+
{ translate: [src.height, 0] },
|
35
|
+
{ rotate: Math.PI / 2 },
|
36
|
+
] as Transforms3d;
|
37
|
+
}
|
38
|
+
if (rotation === 180) {
|
39
|
+
return [
|
40
|
+
...result,
|
41
|
+
{ translate: [src.width, src.height] },
|
42
|
+
{ rotate: Math.PI },
|
43
|
+
] as Transforms3d;
|
44
|
+
}
|
45
|
+
if (rotation === 270) {
|
46
|
+
return [
|
47
|
+
...result,
|
48
|
+
{ translate: [0, src.width] },
|
49
|
+
{ rotate: -Math.PI / 2 },
|
50
|
+
] as Transforms3d;
|
51
|
+
}
|
52
|
+
return result;
|
19
53
|
};
|
20
54
|
|
21
55
|
export const FitBox = ({ fit = "contain", src, dst, children }: FitProps) => {
|
package/src/skia/core/Matrix.ts
CHANGED
@@ -2,5 +2,7 @@ import { Skia } from "../Skia";
|
|
2
2
|
import type { Transforms3d } from "../types";
|
3
3
|
import { processTransform } from "../types";
|
4
4
|
|
5
|
-
export const processTransform2d = (transforms: Transforms3d) =>
|
6
|
-
|
5
|
+
export const processTransform2d = (transforms: Transforms3d) => {
|
6
|
+
"worklet";
|
7
|
+
return processTransform(Skia.Matrix(), transforms);
|
8
|
+
};
|
package/src/skia/types/Matrix.ts
CHANGED
@@ -1,9 +1,16 @@
|
|
1
1
|
import type { SkImage } from "../Image";
|
2
2
|
import type { SkJSIInstance } from "../JsiInstance";
|
3
3
|
|
4
|
+
export type VideoRotation = 0 | 90 | 180 | 270;
|
5
|
+
|
4
6
|
export interface Video extends SkJSIInstance<"Video"> {
|
5
7
|
duration(): number;
|
6
8
|
framerate(): number;
|
7
9
|
nextImage(): SkImage | null;
|
8
10
|
seek(time: number): void;
|
11
|
+
rotation(): VideoRotation;
|
12
|
+
size(): { width: number; height: number };
|
13
|
+
pause(): void;
|
14
|
+
play(): void;
|
15
|
+
setVolume(volume: number): void;
|
9
16
|
}
|
package/src/skia/types/index.ts
CHANGED