@shopify/react-native-skia 1.3.2 → 1.3.4

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 (125) hide show
  1. package/android/cpp/jni/JniPlatformContext.cpp +4 -3
  2. package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp +59 -1
  3. package/android/cpp/rnskia-android/RNSkAndroidVideo.h +4 -0
  4. package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java +80 -7
  5. package/cpp/api/JsiVideo.h +32 -7
  6. package/cpp/rnskia/RNSkVideo.h +4 -0
  7. package/ios/RNSkia-iOS/RNSkiOSVideo.h +13 -4
  8. package/ios/RNSkia-iOS/RNSkiOSVideo.mm +65 -67
  9. package/lib/commonjs/dom/nodes/datatypes/Fitting.js +42 -30
  10. package/lib/commonjs/dom/nodes/datatypes/Fitting.js.map +1 -1
  11. package/lib/commonjs/external/reanimated/useVideo.d.ts +16 -4
  12. package/lib/commonjs/external/reanimated/useVideo.js +92 -17
  13. package/lib/commonjs/external/reanimated/useVideo.js.map +1 -1
  14. package/lib/commonjs/external/reanimated/useVideoLoading.d.ts +4 -0
  15. package/lib/commonjs/external/reanimated/useVideoLoading.js +27 -0
  16. package/lib/commonjs/external/reanimated/useVideoLoading.js.map +1 -0
  17. package/lib/commonjs/external/reanimated/useVideoLoading.web.d.ts +4 -0
  18. package/lib/commonjs/external/reanimated/useVideoLoading.web.js +20 -0
  19. package/lib/commonjs/external/reanimated/useVideoLoading.web.js.map +1 -0
  20. package/lib/commonjs/renderer/components/shapes/FitBox.d.ts +2 -10
  21. package/lib/commonjs/renderer/components/shapes/FitBox.js +32 -3
  22. package/lib/commonjs/renderer/components/shapes/FitBox.js.map +1 -1
  23. package/lib/commonjs/skia/core/Matrix.js +5 -1
  24. package/lib/commonjs/skia/core/Matrix.js.map +1 -1
  25. package/lib/commonjs/skia/types/Matrix.js +2 -0
  26. package/lib/commonjs/skia/types/Matrix.js.map +1 -1
  27. package/lib/commonjs/skia/types/NativeBuffer/NativeBufferFactory.d.ts +3 -1
  28. package/lib/commonjs/skia/types/NativeBuffer/NativeBufferFactory.js +4 -2
  29. package/lib/commonjs/skia/types/NativeBuffer/NativeBufferFactory.js.map +1 -1
  30. package/lib/commonjs/skia/types/Skia.d.ts +1 -1
  31. package/lib/commonjs/skia/types/Skia.js.map +1 -1
  32. package/lib/commonjs/skia/types/Video/Video.d.ts +9 -1
  33. package/lib/commonjs/skia/types/Video/Video.js.map +1 -1
  34. package/lib/commonjs/skia/web/CanvasKitWebGLBufferImpl.d.ts +9 -0
  35. package/lib/commonjs/skia/web/CanvasKitWebGLBufferImpl.js +30 -0
  36. package/lib/commonjs/skia/web/CanvasKitWebGLBufferImpl.js.map +1 -0
  37. package/lib/commonjs/skia/web/JsiSkImageFactory.js +8 -2
  38. package/lib/commonjs/skia/web/JsiSkImageFactory.js.map +1 -1
  39. package/lib/commonjs/skia/web/JsiSkia.js +2 -3
  40. package/lib/commonjs/skia/web/JsiSkia.js.map +1 -1
  41. package/lib/commonjs/skia/web/JsiVideo.d.ts +24 -0
  42. package/lib/commonjs/skia/web/JsiVideo.js +83 -0
  43. package/lib/commonjs/skia/web/JsiVideo.js.map +1 -0
  44. package/lib/commonjs/views/SkiaDomView.js +2 -0
  45. package/lib/commonjs/views/SkiaDomView.js.map +1 -1
  46. package/lib/commonjs/views/SkiaDomView.web.js +2 -0
  47. package/lib/commonjs/views/SkiaDomView.web.js.map +1 -1
  48. package/lib/commonjs/views/SkiaJSDomView.js +2 -0
  49. package/lib/commonjs/views/SkiaJSDomView.js.map +1 -1
  50. package/lib/module/dom/nodes/datatypes/Fitting.js +41 -29
  51. package/lib/module/dom/nodes/datatypes/Fitting.js.map +1 -1
  52. package/lib/module/external/reanimated/useVideo.d.ts +16 -4
  53. package/lib/module/external/reanimated/useVideo.js +92 -17
  54. package/lib/module/external/reanimated/useVideo.js.map +1 -1
  55. package/lib/module/external/reanimated/useVideoLoading.d.ts +4 -0
  56. package/lib/module/external/reanimated/useVideoLoading.js +20 -0
  57. package/lib/module/external/reanimated/useVideoLoading.js.map +1 -0
  58. package/lib/module/external/reanimated/useVideoLoading.web.d.ts +4 -0
  59. package/lib/module/external/reanimated/useVideoLoading.web.js +13 -0
  60. package/lib/module/external/reanimated/useVideoLoading.web.js.map +1 -0
  61. package/lib/module/renderer/components/shapes/FitBox.d.ts +2 -10
  62. package/lib/module/renderer/components/shapes/FitBox.js +32 -3
  63. package/lib/module/renderer/components/shapes/FitBox.js.map +1 -1
  64. package/lib/module/skia/core/Matrix.js +5 -1
  65. package/lib/module/skia/core/Matrix.js.map +1 -1
  66. package/lib/module/skia/types/Matrix.js +2 -0
  67. package/lib/module/skia/types/Matrix.js.map +1 -1
  68. package/lib/module/skia/types/NativeBuffer/NativeBufferFactory.d.ts +3 -1
  69. package/lib/module/skia/types/NativeBuffer/NativeBufferFactory.js +2 -1
  70. package/lib/module/skia/types/NativeBuffer/NativeBufferFactory.js.map +1 -1
  71. package/lib/module/skia/types/Skia.d.ts +1 -1
  72. package/lib/module/skia/types/Skia.js.map +1 -1
  73. package/lib/module/skia/types/Video/Video.d.ts +9 -1
  74. package/lib/module/skia/types/Video/Video.js.map +1 -1
  75. package/lib/module/skia/web/CanvasKitWebGLBufferImpl.d.ts +9 -0
  76. package/lib/module/skia/web/CanvasKitWebGLBufferImpl.js +23 -0
  77. package/lib/module/skia/web/CanvasKitWebGLBufferImpl.js.map +1 -0
  78. package/lib/module/skia/web/JsiSkImageFactory.js +9 -3
  79. package/lib/module/skia/web/JsiSkImageFactory.js.map +1 -1
  80. package/lib/module/skia/web/JsiSkia.js +2 -3
  81. package/lib/module/skia/web/JsiSkia.js.map +1 -1
  82. package/lib/module/skia/web/JsiVideo.d.ts +24 -0
  83. package/lib/module/skia/web/JsiVideo.js +75 -0
  84. package/lib/module/skia/web/JsiVideo.js.map +1 -0
  85. package/lib/module/views/SkiaDomView.js +2 -0
  86. package/lib/module/views/SkiaDomView.js.map +1 -1
  87. package/lib/module/views/SkiaDomView.web.js +2 -0
  88. package/lib/module/views/SkiaDomView.web.js.map +1 -1
  89. package/lib/module/views/SkiaJSDomView.js +2 -0
  90. package/lib/module/views/SkiaJSDomView.js.map +1 -1
  91. package/lib/typescript/src/external/reanimated/useVideo.d.ts +16 -4
  92. package/lib/typescript/src/external/reanimated/useVideoLoading.d.ts +4 -0
  93. package/lib/typescript/src/external/reanimated/useVideoLoading.web.d.ts +4 -0
  94. package/lib/typescript/src/renderer/components/shapes/FitBox.d.ts +2 -10
  95. package/lib/typescript/src/skia/types/NativeBuffer/NativeBufferFactory.d.ts +3 -1
  96. package/lib/typescript/src/skia/types/Skia.d.ts +1 -1
  97. package/lib/typescript/src/skia/types/Video/Video.d.ts +9 -1
  98. package/lib/typescript/src/skia/web/CanvasKitWebGLBufferImpl.d.ts +9 -0
  99. package/lib/typescript/src/skia/web/JsiVideo.d.ts +24 -0
  100. package/package.json +1 -1
  101. package/src/dom/nodes/datatypes/Fitting.ts +28 -21
  102. package/src/external/reanimated/useVideo.ts +108 -31
  103. package/src/external/reanimated/useVideoLoading.ts +28 -0
  104. package/src/external/reanimated/useVideoLoading.web.ts +17 -0
  105. package/src/renderer/components/shapes/FitBox.tsx +38 -4
  106. package/src/skia/core/Matrix.ts +4 -2
  107. package/src/skia/types/Matrix.ts +1 -0
  108. package/src/skia/types/NativeBuffer/NativeBufferFactory.ts +10 -2
  109. package/src/skia/types/Skia.ts +1 -1
  110. package/src/skia/types/Video/Video.ts +7 -1
  111. package/src/skia/web/CanvasKitWebGLBufferImpl.ts +22 -0
  112. package/src/skia/web/JsiSkImageFactory.ts +16 -3
  113. package/src/skia/web/JsiSkia.ts +2 -3
  114. package/src/skia/web/JsiVideo.ts +96 -0
  115. package/src/views/SkiaDomView.tsx +4 -0
  116. package/src/views/SkiaDomView.web.tsx +4 -0
  117. package/src/views/SkiaJSDomView.tsx +4 -0
  118. package/lib/commonjs/external/reanimated/video.d.ts +0 -16
  119. package/lib/commonjs/external/reanimated/video.js +0 -54
  120. package/lib/commonjs/external/reanimated/video.js.map +0 -1
  121. package/lib/module/external/reanimated/video.d.ts +0 -16
  122. package/lib/module/external/reanimated/video.js +0 -46
  123. package/lib/module/external/reanimated/video.js.map +0 -1
  124. package/lib/typescript/src/external/reanimated/video.d.ts +0 -16
  125. package/src/external/reanimated/video.ts +0 -82
@@ -1,22 +1,52 @@
1
- import { type FrameInfo } from "react-native-reanimated";
1
+ import type { SharedValue, FrameInfo } from "react-native-reanimated";
2
2
  import { useEffect, useMemo } from "react";
3
3
 
4
- import { Skia } from "../../skia/Skia";
5
4
  import type { SkImage, Video } from "../../skia/types";
5
+ import { Platform } from "../../Platform";
6
6
 
7
7
  import Rea from "./ReanimatedProxy";
8
- import {
9
- processVideoState,
10
- type Animated,
11
- type PlaybackOptions,
12
- } from "./video";
8
+ import { useVideoLoading } from "./useVideoLoading";
9
+
10
+ type Animated<T> = SharedValue<T> | T;
11
+
12
+ interface PlaybackOptions {
13
+ looping: Animated<boolean>;
14
+ paused: Animated<boolean>;
15
+ seek: Animated<number | null>;
16
+ volume: Animated<number>;
17
+ }
18
+
19
+ const copyFrameOnAndroid = (currentFrame: SharedValue<SkImage | null>) => {
20
+ "worklet";
21
+ // on android we need to copy the texture before it's invalidated
22
+ if (Platform.OS === "android") {
23
+ const tex = currentFrame.value;
24
+ if (tex) {
25
+ currentFrame.value = tex.makeNonTextureImage();
26
+ tex.dispose();
27
+ }
28
+ }
29
+ };
30
+
31
+ const setFrame = (video: Video, currentFrame: SharedValue<SkImage | null>) => {
32
+ "worklet";
33
+ const img = video.nextImage();
34
+ if (img) {
35
+ if (currentFrame.value) {
36
+ currentFrame.value.dispose();
37
+ }
38
+ currentFrame.value = img;
39
+ } else {
40
+ copyFrameOnAndroid(currentFrame);
41
+ }
42
+ };
13
43
 
14
44
  const defaultOptions = {
15
- playbackSpeed: 1,
16
45
  looping: true,
17
46
  paused: false,
18
47
  seek: null,
19
48
  currentTime: 0,
49
+ volume: 0,
20
50
  };
21
51
 
22
52
  const useOption = <T>(value: Animated<T>) => {
@@ -37,38 +67,78 @@ export const useVideo = (
37
67
  source: string | null,
38
68
  userOptions?: Partial<PlaybackOptions>
39
69
  ) => {
40
- const video = useMemo(() => (source ? Skia.Video(source) : null), [source]);
70
+ const video = useVideoLoading(source);
41
71
  const isPaused = useOption(userOptions?.paused ?? defaultOptions.paused);
42
72
  const looping = useOption(userOptions?.looping ?? defaultOptions.looping);
43
73
  const seek = useOption(userOptions?.seek ?? defaultOptions.seek);
44
- const playbackSpeed = useOption(
45
- userOptions?.playbackSpeed ?? defaultOptions.playbackSpeed
46
- );
74
+ const volume = useOption(userOptions?.volume ?? defaultOptions.volume);
47
75
  const currentFrame = Rea.useSharedValue<null | SkImage>(null);
48
76
  const currentTime = Rea.useSharedValue(0);
49
77
  const lastTimestamp = Rea.useSharedValue(-1);
50
78
  const duration = useMemo(() => video?.duration() ?? 0, [video]);
51
- const framerate = useMemo(() => video?.framerate() ?? 0, [video]);
52
- const rotationInDegrees = useMemo(
53
- () => video?.getRotationInDegrees() ?? 0,
79
+ const framerate = useMemo(
80
+ () => (Platform.OS === "web" ? -1 : video?.framerate() ?? 0),
54
81
  [video]
55
82
  );
83
+ const size = useMemo(() => video?.size() ?? { width: 0, height: 0 }, [video]);
84
+ const rotation = useMemo(() => video?.rotation() ?? 0, [video]);
85
+ const frameDuration = 1000 / framerate;
86
+ const currentFrameDuration = Math.floor(frameDuration);
87
+ Rea.useAnimatedReaction(
88
+ () => isPaused.value,
89
+ (paused) => {
90
+ if (paused) {
91
+ video?.pause();
92
+ } else {
93
+ lastTimestamp.value = -1;
94
+ video?.play();
95
+ }
96
+ }
97
+ );
98
+ Rea.useAnimatedReaction(
99
+ () => seek.value,
100
+ (value) => {
101
+ if (value !== null) {
102
+ copyFrameOnAndroid(currentFrame);
103
+ video?.seek(value);
104
+ currentTime.value = value;
105
+ seek.value = null;
106
+ }
107
+ }
108
+ );
109
+ Rea.useAnimatedReaction(
110
+ () => volume.value,
111
+ (value) => {
112
+ video?.setVolume(value);
113
+ }
114
+ );
56
115
  Rea.useFrameCallback((frameInfo: FrameInfo) => {
57
- processVideoState(
58
- video,
59
- duration,
60
- framerate,
61
- frameInfo.timestamp,
62
- {
63
- paused: isPaused.value,
64
- looping: looping.value,
65
- playbackSpeed: playbackSpeed.value,
66
- },
67
- currentTime,
68
- currentFrame,
69
- lastTimestamp,
70
- seek
71
- );
116
+ "worklet";
117
+ if (!video) {
118
+ return;
119
+ }
120
+ if (isPaused.value) {
121
+ return;
122
+ }
123
+ const currentTimestamp = frameInfo.timestamp;
124
+ if (lastTimestamp.value === -1) {
125
+ lastTimestamp.value = currentTimestamp;
126
+ }
127
+ const delta = currentTimestamp - lastTimestamp.value;
128
+
129
+ const isOver = currentTime.value + delta > duration;
130
+ if (isOver && looping.value) {
131
+ seek.value = 0;
132
+ currentTime.value = seek.value;
133
+ lastTimestamp.value = currentTimestamp;
134
+ }
135
+ // On Web the framerate is uknown.
136
+ // This could be optimized by using requestVideoFrameCallback (Chrome only)
137
+ if ((delta >= currentFrameDuration && !isOver) || Platform.OS === "web") {
138
+ setFrame(video, currentFrame);
139
+ currentTime.value += delta;
140
+ lastTimestamp.value = currentTimestamp;
141
+ }
72
142
  });
73
143
 
74
144
  useEffect(() => {
@@ -78,5 +148,12 @@ export const useVideo = (
78
148
  };
79
149
  }, [video]);
80
150
 
81
- return { currentFrame, currentTime, duration, framerate, rotationInDegrees };
151
+ return {
152
+ currentFrame,
153
+ currentTime,
154
+ duration,
155
+ framerate,
156
+ rotation,
157
+ size,
158
+ };
82
159
  };
@@ -0,0 +1,28 @@
1
+ import { useEffect, useState } from "react";
2
+ import {
3
+ createWorkletRuntime,
4
+ runOnJS,
5
+ runOnRuntime,
6
+ } from "react-native-reanimated";
7
+
8
+ import type { Video } from "../../skia/types";
9
+ import { Skia } from "../../skia";
10
+
11
+ const runtime = createWorkletRuntime("video-metadata-runtime");
12
+
13
+ type VideoSource = string | null;
14
+
15
+ export const useVideoLoading = (source: VideoSource) => {
16
+ const [video, setVideo] = useState<Video | null>(null);
17
+ const cb = (src: string) => {
18
+ "worklet";
19
+ const vid = Skia.Video(src) as Video;
20
+ runOnJS(setVideo)(vid);
21
+ };
22
+ useEffect(() => {
23
+ if (source) {
24
+ runOnRuntime(runtime, cb)(source);
25
+ }
26
+ }, [source]);
27
+ return video;
28
+ };
@@ -0,0 +1,17 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ import type { Video } from "../../skia/types";
4
+ import { Skia } from "../../skia";
5
+
6
+ type VideoSource = string | null;
7
+
8
+ export const useVideoLoading = (source: VideoSource) => {
9
+ const [video, setVideo] = useState<Video | null>(null);
10
+ useEffect(() => {
11
+ if (source) {
12
+ const vid = Skia.Video(source) as Promise<Video>;
13
+ vid.then((v) => setVideo(v));
14
+ }
15
+ }, [source]);
16
+ return video;
17
+ };
@@ -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,7 +1,14 @@
1
1
  import type { SkImage } from "../Image";
2
2
 
3
+ export abstract class CanvasKitWebGLBuffer {}
4
+
3
5
  export type NativeBuffer<
4
- T extends bigint | ArrayBuffer | CanvasImageSource | unknown = unknown
6
+ T extends
7
+ | bigint
8
+ | ArrayBuffer
9
+ | CanvasImageSource
10
+ | CanvasKitWebGLBuffer
11
+ | unknown = unknown
5
12
  > = T;
6
13
 
7
14
  export type NativeBufferAddr = NativeBuffer<bigint>;
@@ -20,7 +27,8 @@ export const isNativeBufferWeb = (
20
27
  buffer instanceof OffscreenCanvas ||
21
28
  buffer instanceof VideoFrame ||
22
29
  buffer instanceof HTMLImageElement ||
23
- buffer instanceof SVGImageElement;
30
+ buffer instanceof SVGImageElement ||
31
+ buffer instanceof CanvasKitWebGLBuffer;
24
32
 
25
33
  export const isNativeBufferNode = (
26
34
  buffer: NativeBuffer
@@ -96,6 +96,6 @@ export interface Skia {
96
96
  TextBlob: TextBlobFactory;
97
97
  Surface: SurfaceFactory;
98
98
  ParagraphBuilder: ParagraphBuilderFactory;
99
- Video: (url: string) => Video;
99
+ Video: (url: string) => Promise<Video> | Video;
100
100
  NativeBuffer: NativeBufferFactory;
101
101
  }
@@ -1,10 +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;
9
- getRotationInDegrees(): number;
11
+ rotation(): VideoRotation;
12
+ size(): { width: number; height: number };
13
+ pause(): void;
14
+ play(): void;
15
+ setVolume(volume: number): void;
10
16
  }
@@ -0,0 +1,22 @@
1
+ import type { Surface, TextureSource, Image } from "canvaskit-wasm";
2
+
3
+ import { CanvasKitWebGLBuffer } from "../types";
4
+
5
+ export class CanvasKitWebGLBufferImpl extends CanvasKitWebGLBuffer {
6
+ public image: Image | null = null;
7
+
8
+ constructor(public surface: Surface, private source: TextureSource) {
9
+ super();
10
+ }
11
+
12
+ toImage() {
13
+ if (this.image === null) {
14
+ this.image = this.surface.makeImageFromTextureSource(this.source);
15
+ }
16
+ if (this.image === null) {
17
+ throw new Error("Failed to create image from texture source");
18
+ }
19
+ this.surface.updateTextureFromSource(this.image, this.source);
20
+ return this.image;
21
+ }
22
+ }
@@ -1,6 +1,6 @@
1
1
  import type { CanvasKit, Image } from "canvaskit-wasm";
2
2
 
3
- import { isNativeBufferWeb } from "../types";
3
+ import { CanvasKitWebGLBuffer, isNativeBufferWeb } from "../types";
4
4
  import type {
5
5
  SkData,
6
6
  ImageInfo,
@@ -13,6 +13,7 @@ import { Host, getEnum } from "./Host";
13
13
  import { JsiSkImage } from "./JsiSkImage";
14
14
  import { JsiSkData } from "./JsiSkData";
15
15
  import type { JsiSkSurface } from "./JsiSkSurface";
16
+ import type { CanvasKitWebGLBufferImpl } from "./CanvasKitWebGLBufferImpl";
16
17
 
17
18
  export class JsiSkImageFactory extends Host implements ImageFactory {
18
19
  constructor(CanvasKit: CanvasKit) {
@@ -35,8 +36,20 @@ export class JsiSkImageFactory extends Host implements ImageFactory {
35
36
  throw new Error("Invalid NativeBuffer");
36
37
  }
37
38
  if (!surface) {
38
- // TODO: this is way to slow
39
- const img = this.CanvasKit.MakeImageFromCanvasImageSource(buffer);
39
+ let img: Image;
40
+ if (
41
+ buffer instanceof HTMLImageElement ||
42
+ buffer instanceof HTMLVideoElement ||
43
+ buffer instanceof ImageBitmap
44
+ ) {
45
+ img = this.CanvasKit.MakeLazyImageFromTextureSource(buffer);
46
+ } else if (buffer instanceof CanvasKitWebGLBuffer) {
47
+ img = (
48
+ buffer as CanvasKitWebGLBuffer as CanvasKitWebGLBufferImpl
49
+ ).toImage();
50
+ } else {
51
+ img = this.CanvasKit.MakeImageFromCanvasImageSource(buffer);
52
+ }
40
53
  return new JsiSkImage(this.CanvasKit, img);
41
54
  } else if (!image) {
42
55
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -42,6 +42,7 @@ import { JsiSkFontMgrFactory } from "./JsiSkFontMgrFactory";
42
42
  import { JsiSkAnimatedImageFactory } from "./JsiSkAnimatedImageFactory";
43
43
  import { JsiSkParagraphBuilderFactory } from "./JsiSkParagraphBuilderFactory";
44
44
  import { JsiSkNativeBufferFactory } from "./JsiSkNativeBufferFactory";
45
+ import { createVideo } from "./JsiVideo";
45
46
 
46
47
  export const JsiSkApi = (CanvasKit: CanvasKit): Skia => ({
47
48
  Point: (x: number, y: number) =>
@@ -127,7 +128,5 @@ export const JsiSkApi = (CanvasKit: CanvasKit): Skia => ({
127
128
  FontMgr: new JsiSkFontMgrFactory(CanvasKit),
128
129
  ParagraphBuilder: new JsiSkParagraphBuilderFactory(CanvasKit),
129
130
  NativeBuffer: new JsiSkNativeBufferFactory(CanvasKit),
130
- Video: (_localUri: string) => {
131
- throw new Error("Not implemented on React Native Web");
132
- },
131
+ Video: createVideo.bind(null, CanvasKit),
133
132
  });
@@ -0,0 +1,96 @@
1
+ import type { CanvasKit, Surface } from "canvaskit-wasm";
2
+
3
+ import type { CanvasKitWebGLBuffer, Video, ImageFactory } from "../types";
4
+
5
+ import { CanvasKitWebGLBufferImpl } from "./CanvasKitWebGLBufferImpl";
6
+ import { JsiSkImageFactory } from "./JsiSkImageFactory";
7
+
8
+ export const createVideo = async (
9
+ CanvasKit: CanvasKit,
10
+ url: string
11
+ ): Promise<Video> => {
12
+ const video = document.createElement("video");
13
+ return new Promise((resolve, reject) => {
14
+ video.src = url;
15
+ video.style.display = "none";
16
+ video.crossOrigin = "anonymous";
17
+ video.volume = 0;
18
+ video.addEventListener("loadedmetadata", () => {
19
+ document.body.appendChild(video);
20
+ resolve(new JsiVideo(new JsiSkImageFactory(CanvasKit), video));
21
+ });
22
+ video.addEventListener("error", () => {
23
+ reject(new Error(`Failed to load video from URL: ${url}`));
24
+ });
25
+ });
26
+ };
27
+
28
+ export class JsiVideo implements Video {
29
+ __typename__ = "Video" as const;
30
+
31
+ private webglBuffer: CanvasKitWebGLBuffer | null = null;
32
+
33
+ constructor(
34
+ private ImageFactory: ImageFactory,
35
+ private videoElement: HTMLVideoElement
36
+ ) {
37
+ document.body.appendChild(this.videoElement);
38
+ }
39
+
40
+ duration() {
41
+ return this.videoElement.duration * 1000;
42
+ }
43
+
44
+ framerate(): number {
45
+ throw new Error("Video.frame is not available on React Native Web");
46
+ }
47
+
48
+ setSurface(surface: Surface) {
49
+ // If we have the surface, we can use the WebGL buffer which is slightly faster
50
+ // This is because WebGL cannot be shared across contextes.
51
+ // This can be removed with WebGPU
52
+ this.webglBuffer = new CanvasKitWebGLBufferImpl(surface, this.videoElement);
53
+ }
54
+
55
+ nextImage() {
56
+ return this.ImageFactory.MakeImageFromNativeBuffer(
57
+ this.webglBuffer ? this.webglBuffer : this.videoElement
58
+ );
59
+ }
60
+
61
+ seek(time: number) {
62
+ if (isNaN(time)) {
63
+ throw new Error(`Invalid time: ${time}`);
64
+ }
65
+ this.videoElement.currentTime = time / 1000;
66
+ }
67
+
68
+ rotation() {
69
+ return 0 as const;
70
+ }
71
+
72
+ size() {
73
+ return {
74
+ width: this.videoElement.videoWidth,
75
+ height: this.videoElement.videoHeight,
76
+ };
77
+ }
78
+
79
+ pause() {
80
+ this.videoElement.pause();
81
+ }
82
+
83
+ play() {
84
+ this.videoElement.play();
85
+ }
86
+
87
+ setVolume(volume: number) {
88
+ this.videoElement.volume = volume;
89
+ }
90
+
91
+ dispose() {
92
+ if (this.videoElement.parentNode) {
93
+ this.videoElement.parentNode.removeChild(this.videoElement);
94
+ }
95
+ }
96
+ }
@@ -26,6 +26,10 @@ export class SkiaDomView extends React.Component<SkiaDomViewProps> {
26
26
  }
27
27
  if (onTouch) {
28
28
  assertSkiaViewApi();
29
+ console.warn(
30
+ `The onTouch property is deprecated and will be removed in the next Skia release.
31
+ See: https://shopify.github.io/react-native-skia/docs/animations/gestures`
32
+ );
29
33
  SkiaViewApi.setJsiProperty(this._nativeId, "onTouch", onTouch);
30
34
  }
31
35
  if (onSize) {
@@ -12,6 +12,10 @@ export class SkiaDomView extends SkiaBaseWebView<SkiaDomViewProps> {
12
12
 
13
13
  protected renderInCanvas(canvas: SkCanvas, touches: TouchInfo[]): void {
14
14
  if (this.props.onTouch) {
15
+ console.warn(
16
+ `The onTouch property is deprecated and will be removed in the next Skia release.
17
+ See: https://shopify.github.io/react-native-skia/docs/animations/gestures`
18
+ );
15
19
  this.props.onTouch([touches]);
16
20
  }
17
21
  if (this.props.onSize) {
@@ -29,6 +29,10 @@ export class SkiaJSDomView extends React.Component<
29
29
  }
30
30
  if (onTouch) {
31
31
  assertSkiaViewApi();
32
+ console.warn(
33
+ `The onTouch property is deprecated and will be removed in the next Skia release.
34
+ See: https://shopify.github.io/react-native-skia/docs/animations/gestures`
35
+ );
32
36
  SkiaViewApi.setJsiProperty(this._nativeId, "onTouch", onTouch);
33
37
  }
34
38
  if (onSize) {
@@ -1,16 +0,0 @@
1
- import type { SharedValue } from "react-native-reanimated";
2
- import type { SkImage, Video } from "../../skia/types";
3
- export type Animated<T> = SharedValue<T> | T;
4
- export interface PlaybackOptions {
5
- playbackSpeed: Animated<number>;
6
- looping: Animated<boolean>;
7
- paused: Animated<boolean>;
8
- seek: Animated<number | null>;
9
- }
10
- type Materialized<T> = {
11
- [K in keyof T]: T[K] extends Animated<infer U> ? U : T[K];
12
- };
13
- export type MaterializedPlaybackOptions = Materialized<Omit<PlaybackOptions, "seek">>;
14
- export declare const setFrame: (video: Video, currentFrame: SharedValue<SkImage | null>) => void;
15
- export declare const processVideoState: (video: Video | null, duration: number, framerate: number, currentTimestamp: number, options: Materialized<Omit<PlaybackOptions, "seek">>, currentTime: SharedValue<number>, currentFrame: SharedValue<SkImage | null>, lastTimestamp: SharedValue<number>, seek: SharedValue<number | null>) => void;
16
- export {};
@@ -1,54 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.setFrame = exports.processVideoState = void 0;
7
- var _Platform = require("../../Platform");
8
- const setFrame = (video, currentFrame) => {
9
- "worklet";
10
-
11
- const img = video.nextImage();
12
- if (img) {
13
- if (currentFrame.value) {
14
- currentFrame.value.dispose();
15
- }
16
- if (_Platform.Platform.OS === "android") {
17
- currentFrame.value = img.makeNonTextureImage();
18
- } else {
19
- currentFrame.value = img;
20
- }
21
- }
22
- };
23
- exports.setFrame = setFrame;
24
- const processVideoState = (video, duration, framerate, currentTimestamp, options, currentTime, currentFrame, lastTimestamp, seek) => {
25
- "worklet";
26
-
27
- if (!video) {
28
- return;
29
- }
30
- if (options.paused) {
31
- return;
32
- }
33
- const delta = currentTimestamp - lastTimestamp.value;
34
- const frameDuration = 1000 / framerate;
35
- const currentFrameDuration = Math.floor(frameDuration / options.playbackSpeed);
36
- if (currentTime.value + delta >= duration && options.looping) {
37
- seek.value = 0;
38
- }
39
- if (seek.value !== null) {
40
- video.seek(seek.value);
41
- currentTime.value = seek.value;
42
- setFrame(video, currentFrame);
43
- lastTimestamp.value = currentTimestamp;
44
- seek.value = null;
45
- return;
46
- }
47
- if (delta >= currentFrameDuration) {
48
- setFrame(video, currentFrame);
49
- currentTime.value += delta;
50
- lastTimestamp.value = currentTimestamp;
51
- }
52
- };
53
- exports.processVideoState = processVideoState;
54
- //# sourceMappingURL=video.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["_Platform","require","setFrame","video","currentFrame","img","nextImage","value","dispose","Platform","OS","makeNonTextureImage","exports","processVideoState","duration","framerate","currentTimestamp","options","currentTime","lastTimestamp","seek","paused","delta","frameDuration","currentFrameDuration","Math","floor","playbackSpeed","looping"],"sources":["video.ts"],"sourcesContent":["import type { SharedValue } from \"react-native-reanimated\";\n\nimport type { SkImage, Video } from \"../../skia/types\";\nimport { Platform } from \"../../Platform\";\n\nexport type Animated<T> = SharedValue<T> | T;\n\nexport interface PlaybackOptions {\n playbackSpeed: Animated<number>;\n looping: Animated<boolean>;\n paused: Animated<boolean>;\n seek: Animated<number | null>;\n}\n\ntype Materialized<T> = {\n [K in keyof T]: T[K] extends Animated<infer U> ? U : T[K];\n};\n\nexport type MaterializedPlaybackOptions = Materialized<\n Omit<PlaybackOptions, \"seek\">\n>;\n\nexport const setFrame = (\n video: Video,\n currentFrame: SharedValue<SkImage | null>\n) => {\n \"worklet\";\n const img = video.nextImage();\n if (img) {\n if (currentFrame.value) {\n currentFrame.value.dispose();\n }\n if (Platform.OS === \"android\") {\n currentFrame.value = img.makeNonTextureImage();\n } else {\n currentFrame.value = img;\n }\n }\n};\n\nexport const processVideoState = (\n video: Video | null,\n duration: number,\n framerate: number,\n currentTimestamp: number,\n options: Materialized<Omit<PlaybackOptions, \"seek\">>,\n currentTime: SharedValue<number>,\n currentFrame: SharedValue<SkImage | null>,\n lastTimestamp: SharedValue<number>,\n seek: SharedValue<number | null>\n) => {\n \"worklet\";\n if (!video) {\n return;\n }\n if (options.paused) {\n return;\n }\n const delta = currentTimestamp - lastTimestamp.value;\n\n const frameDuration = 1000 / framerate;\n const currentFrameDuration = Math.floor(\n frameDuration / options.playbackSpeed\n );\n if (currentTime.value + delta >= duration && options.looping) {\n seek.value = 0;\n }\n if (seek.value !== null) {\n video.seek(seek.value);\n currentTime.value = seek.value;\n setFrame(video, currentFrame);\n lastTimestamp.value = currentTimestamp;\n seek.value = null;\n return;\n }\n\n if (delta >= currentFrameDuration) {\n setFrame(video, currentFrame);\n currentTime.value += delta;\n lastTimestamp.value = currentTimestamp;\n }\n};\n"],"mappings":";;;;;;AAGA,IAAAA,SAAA,GAAAC,OAAA;AAmBO,MAAMC,QAAQ,GAAGA,CACtBC,KAAY,EACZC,YAAyC,KACtC;EACH,SAAS;;EACT,MAAMC,GAAG,GAAGF,KAAK,CAACG,SAAS,CAAC,CAAC;EAC7B,IAAID,GAAG,EAAE;IACP,IAAID,YAAY,CAACG,KAAK,EAAE;MACtBH,YAAY,CAACG,KAAK,CAACC,OAAO,CAAC,CAAC;IAC9B;IACA,IAAIC,kBAAQ,CAACC,EAAE,KAAK,SAAS,EAAE;MAC7BN,YAAY,CAACG,KAAK,GAAGF,GAAG,CAACM,mBAAmB,CAAC,CAAC;IAChD,CAAC,MAAM;MACLP,YAAY,CAACG,KAAK,GAAGF,GAAG;IAC1B;EACF;AACF,CAAC;AAACO,OAAA,CAAAV,QAAA,GAAAA,QAAA;AAEK,MAAMW,iBAAiB,GAAGA,CAC/BV,KAAmB,EACnBW,QAAgB,EAChBC,SAAiB,EACjBC,gBAAwB,EACxBC,OAAoD,EACpDC,WAAgC,EAChCd,YAAyC,EACzCe,aAAkC,EAClCC,IAAgC,KAC7B;EACH,SAAS;;EACT,IAAI,CAACjB,KAAK,EAAE;IACV;EACF;EACA,IAAIc,OAAO,CAACI,MAAM,EAAE;IAClB;EACF;EACA,MAAMC,KAAK,GAAGN,gBAAgB,GAAGG,aAAa,CAACZ,KAAK;EAEpD,MAAMgB,aAAa,GAAG,IAAI,GAAGR,SAAS;EACtC,MAAMS,oBAAoB,GAAGC,IAAI,CAACC,KAAK,CACrCH,aAAa,GAAGN,OAAO,CAACU,aAC1B,CAAC;EACD,IAAIT,WAAW,CAACX,KAAK,GAAGe,KAAK,IAAIR,QAAQ,IAAIG,OAAO,CAACW,OAAO,EAAE;IAC5DR,IAAI,CAACb,KAAK,GAAG,CAAC;EAChB;EACA,IAAIa,IAAI,CAACb,KAAK,KAAK,IAAI,EAAE;IACvBJ,KAAK,CAACiB,IAAI,CAACA,IAAI,CAACb,KAAK,CAAC;IACtBW,WAAW,CAACX,KAAK,GAAGa,IAAI,CAACb,KAAK;IAC9BL,QAAQ,CAACC,KAAK,EAAEC,YAAY,CAAC;IAC7Be,aAAa,CAACZ,KAAK,GAAGS,gBAAgB;IACtCI,IAAI,CAACb,KAAK,GAAG,IAAI;IACjB;EACF;EAEA,IAAIe,KAAK,IAAIE,oBAAoB,EAAE;IACjCtB,QAAQ,CAACC,KAAK,EAAEC,YAAY,CAAC;IAC7Bc,WAAW,CAACX,KAAK,IAAIe,KAAK;IAC1BH,aAAa,CAACZ,KAAK,GAAGS,gBAAgB;EACxC;AACF,CAAC;AAACJ,OAAA,CAAAC,iBAAA,GAAAA,iBAAA"}
@@ -1,16 +0,0 @@
1
- import type { SharedValue } from "react-native-reanimated";
2
- import type { SkImage, Video } from "../../skia/types";
3
- export type Animated<T> = SharedValue<T> | T;
4
- export interface PlaybackOptions {
5
- playbackSpeed: Animated<number>;
6
- looping: Animated<boolean>;
7
- paused: Animated<boolean>;
8
- seek: Animated<number | null>;
9
- }
10
- type Materialized<T> = {
11
- [K in keyof T]: T[K] extends Animated<infer U> ? U : T[K];
12
- };
13
- export type MaterializedPlaybackOptions = Materialized<Omit<PlaybackOptions, "seek">>;
14
- export declare const setFrame: (video: Video, currentFrame: SharedValue<SkImage | null>) => void;
15
- export declare const processVideoState: (video: Video | null, duration: number, framerate: number, currentTimestamp: number, options: Materialized<Omit<PlaybackOptions, "seek">>, currentTime: SharedValue<number>, currentFrame: SharedValue<SkImage | null>, lastTimestamp: SharedValue<number>, seek: SharedValue<number | null>) => void;
16
- export {};