@shopify/react-native-skia 1.3.1 → 1.3.3

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 (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";