@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.
Files changed (53) hide show
  1. package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp +72 -1
  2. package/android/cpp/rnskia-android/RNSkAndroidVideo.h +5 -0
  3. package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java +89 -8
  4. package/cpp/api/JsiVideo.h +37 -5
  5. package/cpp/rnskia/RNSkVideo.h +5 -0
  6. package/ios/RNSkia-iOS/RNSkiOSVideo.h +14 -3
  7. package/ios/RNSkia-iOS/RNSkiOSVideo.mm +78 -60
  8. package/lib/commonjs/dom/nodes/datatypes/Fitting.js +42 -30
  9. package/lib/commonjs/dom/nodes/datatypes/Fitting.js.map +1 -1
  10. package/lib/commonjs/external/reanimated/useVideo.d.ts +14 -5
  11. package/lib/commonjs/external/reanimated/useVideo.js +90 -58
  12. package/lib/commonjs/external/reanimated/useVideo.js.map +1 -1
  13. package/lib/commonjs/renderer/components/shapes/FitBox.d.ts +2 -10
  14. package/lib/commonjs/renderer/components/shapes/FitBox.js +32 -3
  15. package/lib/commonjs/renderer/components/shapes/FitBox.js.map +1 -1
  16. package/lib/commonjs/skia/core/Matrix.js +5 -1
  17. package/lib/commonjs/skia/core/Matrix.js.map +1 -1
  18. package/lib/commonjs/skia/types/Matrix.js +2 -0
  19. package/lib/commonjs/skia/types/Matrix.js.map +1 -1
  20. package/lib/commonjs/skia/types/Video/Video.d.ts +9 -0
  21. package/lib/commonjs/skia/types/Video/Video.js.map +1 -1
  22. package/lib/commonjs/skia/types/index.d.ts +1 -0
  23. package/lib/commonjs/skia/types/index.js +11 -0
  24. package/lib/commonjs/skia/types/index.js.map +1 -1
  25. package/lib/module/dom/nodes/datatypes/Fitting.js +41 -29
  26. package/lib/module/dom/nodes/datatypes/Fitting.js.map +1 -1
  27. package/lib/module/external/reanimated/useVideo.d.ts +14 -5
  28. package/lib/module/external/reanimated/useVideo.js +91 -59
  29. package/lib/module/external/reanimated/useVideo.js.map +1 -1
  30. package/lib/module/renderer/components/shapes/FitBox.d.ts +2 -10
  31. package/lib/module/renderer/components/shapes/FitBox.js +32 -3
  32. package/lib/module/renderer/components/shapes/FitBox.js.map +1 -1
  33. package/lib/module/skia/core/Matrix.js +5 -1
  34. package/lib/module/skia/core/Matrix.js.map +1 -1
  35. package/lib/module/skia/types/Matrix.js +2 -0
  36. package/lib/module/skia/types/Matrix.js.map +1 -1
  37. package/lib/module/skia/types/Video/Video.d.ts +9 -0
  38. package/lib/module/skia/types/Video/Video.js.map +1 -1
  39. package/lib/module/skia/types/index.d.ts +1 -0
  40. package/lib/module/skia/types/index.js +1 -0
  41. package/lib/module/skia/types/index.js.map +1 -1
  42. package/lib/typescript/src/external/reanimated/useVideo.d.ts +14 -5
  43. package/lib/typescript/src/renderer/components/shapes/FitBox.d.ts +2 -10
  44. package/lib/typescript/src/skia/types/Video/Video.d.ts +9 -0
  45. package/lib/typescript/src/skia/types/index.d.ts +1 -0
  46. package/package.json +1 -1
  47. package/src/dom/nodes/datatypes/Fitting.ts +28 -21
  48. package/src/external/reanimated/useVideo.ts +86 -73
  49. package/src/renderer/components/shapes/FitBox.tsx +38 -4
  50. package/src/skia/core/Matrix.ts +4 -2
  51. package/src/skia/types/Matrix.ts +1 -0
  52. package/src/skia/types/Video/Video.ts +7 -0
  53. 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
  }
@@ -30,3 +30,4 @@ export * from "./Size";
30
30
  export * from "./Paragraph";
31
31
  export * from "./Matrix4";
32
32
  export * from "./NativeBuffer";
33
+ export * from "./Video";
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.1",
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) => ({ width, height });
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
- runOnUI,
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
- export interface PlaybackOptions {
18
- playbackSpeed: Animated<number>;
12
+ interface PlaybackOptions {
19
13
  looping: Animated<boolean>;
20
14
  paused: Animated<boolean>;
21
15
  seek: Animated<number | null>;
22
- currentTime: Animated<number>;
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 currentTime = useOption(
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 startTimestamp = Rea.useSharedValue(-1);
59
-
60
- const framerate = useMemo(() => (video ? video.framerate() : -1), [video]);
61
- const duration = useMemo(() => (video ? video.duration() : -1), [video]);
62
- const frameDuration = useMemo(
63
- () => (framerate > 0 ? 1000 / framerate : -1),
64
- [framerate]
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 (seek.value !== null) {
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 { timestamp } = frameInfo;
85
-
86
- // Initialize start timestamp
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
- // Calculate the current time in the video
92
- const currentTimestamp = timestamp - startTimestamp.value;
93
- currentTime.value = currentTimestamp;
94
-
95
- // Handle looping
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
- // Update frame only if the elapsed time since last update is greater than the frame duration
102
- const currentFrameDuration = Math.floor(
103
- frameDuration / playbackSpeed.value
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
- }, [disposeVideo, video]);
133
+ }, [video]);
128
134
 
129
- return currentFrame;
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 = (fit: Fit, src: SkRect, dst: SkRect) => {
17
- const rects = fitRects(fit, src, dst);
18
- return rect2rect(rects.src, rects.dst);
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) => {
@@ -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
- processTransform(Skia.Matrix(), transforms);
5
+ export const processTransform2d = (transforms: Transforms3d) => {
6
+ "worklet";
7
+ return processTransform(Skia.Matrix(), transforms);
8
+ };
@@ -30,6 +30,7 @@ export const processTransform = <T extends SkMatrix | SkCanvas>(
30
30
  m: T,
31
31
  transforms: Transforms3d
32
32
  ) => {
33
+ "worklet";
33
34
  const m3 = processTransform3d(transforms);
34
35
  m.concat(m3);
35
36
  return m;
@@ -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
  }
@@ -30,3 +30,4 @@ export * from "./Size";
30
30
  export * from "./Paragraph";
31
31
  export * from "./Matrix4";
32
32
  export * from "./NativeBuffer";
33
+ export * from "./Video";