From f7f6e86582aa028be05d789193464c94ac998c74 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 4 Jun 2024 17:35:24 +0200 Subject: [PATCH 01/32] Add getSize on iOS --- package/cpp/api/JsiVideo.h | 10 +++++ package/cpp/rnskia/RNSkVideo.h | 1 + package/ios/RNSkia-iOS/RNSkiOSVideo.h | 4 ++ package/ios/RNSkia-iOS/RNSkiOSVideo.mm | 8 +++- package/src/dom/nodes/datatypes/Fitting.ts | 43 ++++++++++--------- package/src/external/reanimated/useVideo.ts | 3 +- .../src/renderer/__tests__/e2e/Video.spec.tsx | 9 +++- .../src/renderer/components/shapes/FitBox.tsx | 1 + package/src/skia/core/Matrix.ts | 6 ++- package/src/skia/types/Matrix.ts | 1 + package/src/skia/types/Video/Video.ts | 1 + 11 files changed, 62 insertions(+), 25 deletions(-) diff --git a/package/cpp/api/JsiVideo.h b/package/cpp/api/JsiVideo.h index 8b6e9bac26..6c1c9514c4 100644 --- a/package/cpp/api/JsiVideo.h +++ b/package/cpp/api/JsiVideo.h @@ -59,11 +59,21 @@ class JsiVideo : public JsiSkWrappingSharedPtrHostObject { return jsi::Value(static_cast(rot)); } + JSI_HOST_FUNCTION(size) { + auto context = getContext(); + auto size = getObject()->getSize(); + auto result = jsi::Object(runtime); + result.setProperty(runtime, "width", static_cast(size.width())); + result.setProperty(runtime, "height", static_cast(size.height())); + return result; + } + JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiVideo, nextImage), JSI_EXPORT_FUNC(JsiVideo, duration), JSI_EXPORT_FUNC(JsiVideo, framerate), JSI_EXPORT_FUNC(JsiVideo, seek), JSI_EXPORT_FUNC(JsiVideo, getRotationInDegrees), + JSI_EXPORT_FUNC(JsiVideo, size), JSI_EXPORT_FUNC(JsiVideo, dispose)) JsiVideo(std::shared_ptr context, diff --git a/package/cpp/rnskia/RNSkVideo.h b/package/cpp/rnskia/RNSkVideo.h index fdb4e84285..d02d76359e 100644 --- a/package/cpp/rnskia/RNSkVideo.h +++ b/package/cpp/rnskia/RNSkVideo.h @@ -19,6 +19,7 @@ class RNSkVideo { virtual double framerate() = 0; virtual void seek(double timestamp) = 0; virtual float getRotationInDegrees() = 0; + virtual SkISize getSize() = 0; }; } // namespace RNSkia diff --git a/package/ios/RNSkia-iOS/RNSkiOSVideo.h b/package/ios/RNSkia-iOS/RNSkiOSVideo.h index 23b02caadb..4344c63c86 100644 --- a/package/ios/RNSkia-iOS/RNSkiOSVideo.h +++ b/package/ios/RNSkia-iOS/RNSkiOSVideo.h @@ -9,6 +9,7 @@ #pragma clang diagnostic ignored "-Wdocumentation" #include "include/core/SkImage.h" +#include "include/core/SkSize.h" #pragma clang diagnostic pop @@ -25,6 +26,8 @@ class RNSkiOSVideo : public RNSkVideo { RNSkPlatformContext *_context; double _duration = 0; double _framerate = 0; + float _videoWidth = 0; + float _videoHeight = 0; void setupReader(CMTimeRange timeRange); NSDictionary *getOutputSettings(); CGAffineTransform _preferredTransform; @@ -37,6 +40,7 @@ class RNSkiOSVideo : public RNSkVideo { double framerate() override; void seek(double timestamp) override; float getRotationInDegrees() override; + SkISize getSize() override; }; } // namespace RNSkia diff --git a/package/ios/RNSkia-iOS/RNSkiOSVideo.mm b/package/ios/RNSkia-iOS/RNSkiOSVideo.mm index 86c6c8616f..146769e118 100644 --- a/package/ios/RNSkia-iOS/RNSkiOSVideo.mm +++ b/package/ios/RNSkia-iOS/RNSkiOSVideo.mm @@ -45,7 +45,9 @@ [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject]; _framerate = videoTrack.nominalFrameRate; _preferredTransform = videoTrack.preferredTransform; - + CGSize videoSize = videoTrack.naturalSize; + _videoWidth = videoSize.width; + _videoHeight = videoSize.height; NSDictionary *outputSettings = getOutputSettings(); AVAssetReaderTrackOutput *trackOutput = [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack @@ -136,4 +138,8 @@ double RNSkiOSVideo::framerate() { return _framerate; } +SkISize RNSkiOSVideo::getSize() { + return SkISize::Make(_videoWidth, _videoHeight); +} + } // namespace RNSkia diff --git a/package/src/dom/nodes/datatypes/Fitting.ts b/package/src/dom/nodes/datatypes/Fitting.ts index 8d8ec86224..273932dd39 100644 --- a/package/src/dom/nodes/datatypes/Fitting.ts +++ b/package/src/dom/nodes/datatypes/Fitting.ts @@ -25,30 +25,11 @@ export const rect2rect = ( return [{ translateX }, { translateY }, { scaleX }, { scaleY }]; }; -export const fitRects = ( - fit: Fit, - rect: SkRect, - { x, y, width, height }: SkRect -) => { - const sizes = applyBoxFit( - fit, - { width: rect.width, height: rect.height }, - { width, height } - ); - const src = inscribe(sizes.src, rect); - const dst = inscribe(sizes.dst, { - x, - y, - width, - height, - }); - return { src, dst }; -}; - const inscribe = ( { width, height }: Size, rect: { x: number; y: number; width: number; height: number } ) => { + "worklet"; const halfWidthDelta = (rect.width - width) / 2.0; const halfHeightDelta = (rect.height - height) / 2.0; return { @@ -60,6 +41,7 @@ const inscribe = ( }; const applyBoxFit = (fit: Fit, input: Size, output: Size) => { + "worklet"; let src = size(), dst = size(); if ( @@ -122,3 +104,24 @@ const applyBoxFit = (fit: Fit, input: Size, output: Size) => { } return { src, dst }; }; + +export const fitRects = ( + fit: Fit, + rect: SkRect, + { x, y, width, height }: SkRect +) => { + "worklet"; + const sizes = applyBoxFit( + fit, + { width: rect.width, height: rect.height }, + { width, height } + ); + const src = inscribe(sizes.src, rect); + const dst = inscribe(sizes.dst, { + x, + y, + width, + height, + }); + return { src, dst }; +}; diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index b2971cb3a4..d9772651e5 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -49,6 +49,7 @@ export const useVideo = ( const lastTimestamp = Rea.useSharedValue(-1); const duration = useMemo(() => video?.duration() ?? 0, [video]); const framerate = useMemo(() => video?.framerate() ?? 0, [video]); + const size = useMemo(() => video?.size() ?? { width: 0, height: 0 }, [video]); const rotationInDegrees = useMemo( () => video?.getRotationInDegrees() ?? 0, [video] @@ -78,5 +79,5 @@ export const useVideo = ( }; }, [video]); - return { currentFrame, currentTime, duration, framerate, rotationInDegrees }; + return { currentFrame, currentTime, duration, framerate, rotationInDegrees, size }; }; diff --git a/package/src/renderer/__tests__/e2e/Video.spec.tsx b/package/src/renderer/__tests__/e2e/Video.spec.tsx index ea16167c86..82591c2342 100644 --- a/package/src/renderer/__tests__/e2e/Video.spec.tsx +++ b/package/src/renderer/__tests__/e2e/Video.spec.tsx @@ -8,9 +8,16 @@ describe("Videos", () => { return { duration: video.duration(), framerate: video.framerate(), + width: video.size().width, + height: video.size().height, }; }); - expect(result).toEqual({ duration: 5280, framerate: 25 }); + expect(result).toEqual({ + duration: 5280, + framerate: 25, + height: 0, + width: 0, + }); }); // TODO: We need to reanable these tests once we can run them on the UI thread // itRunsE2eOnly("get frame", async () => { diff --git a/package/src/renderer/components/shapes/FitBox.tsx b/package/src/renderer/components/shapes/FitBox.tsx index bcd78ea647..02acebfd42 100644 --- a/package/src/renderer/components/shapes/FitBox.tsx +++ b/package/src/renderer/components/shapes/FitBox.tsx @@ -14,6 +14,7 @@ interface FitProps { } export const fitbox = (fit: Fit, src: SkRect, dst: SkRect) => { + "worklet"; const rects = fitRects(fit, src, dst); return rect2rect(rects.src, rects.dst); }; diff --git a/package/src/skia/core/Matrix.ts b/package/src/skia/core/Matrix.ts index a8e0646829..453c671561 100644 --- a/package/src/skia/core/Matrix.ts +++ b/package/src/skia/core/Matrix.ts @@ -2,5 +2,7 @@ import { Skia } from "../Skia"; import type { Transforms3d } from "../types"; import { processTransform } from "../types"; -export const processTransform2d = (transforms: Transforms3d) => - processTransform(Skia.Matrix(), transforms); +export const processTransform2d = (transforms: Transforms3d) => { + "worklet"; + return processTransform(Skia.Matrix(), transforms); +}; diff --git a/package/src/skia/types/Matrix.ts b/package/src/skia/types/Matrix.ts index 37e75d9c28..353c3894ec 100644 --- a/package/src/skia/types/Matrix.ts +++ b/package/src/skia/types/Matrix.ts @@ -30,6 +30,7 @@ export const processTransform = ( m: T, transforms: Transforms3d ) => { + "worklet"; const m3 = processTransform3d(transforms); m.concat(m3); return m; diff --git a/package/src/skia/types/Video/Video.ts b/package/src/skia/types/Video/Video.ts index fbe8ab8527..5e5d6d6fd3 100644 --- a/package/src/skia/types/Video/Video.ts +++ b/package/src/skia/types/Video/Video.ts @@ -7,4 +7,5 @@ export interface Video extends SkJSIInstance<"Video"> { nextImage(): SkImage | null; seek(time: number): void; getRotationInDegrees(): number; + size(): { width: number; height: number }; } From 1531b3506746f81bb79e5e883fab57b014986897 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 08:38:47 +0200 Subject: [PATCH 02/32] :wrench: --- .../cpp/rnskia-android/RNSkAndroidVideo.cpp | 26 +++++++++++++++++++ .../cpp/rnskia-android/RNSkAndroidVideo.h | 1 + .../shopify/reactnative/skia/RNSkVideo.java | 12 +++++++-- package/src/dom/nodes/datatypes/Fitting.ts | 6 ++++- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp b/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp index f561097771..ba203d03f9 100644 --- a/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp +++ b/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp @@ -9,6 +9,7 @@ #pragma clang diagnostic ignored "-Wdocumentation" #include "include/core/SkImage.h" +#include "include/core/SkSize.h" #pragma clang diagnostic pop @@ -102,4 +103,29 @@ float RNSkAndroidVideo::getRotationInDegrees() { return static_cast(rotation); } +SkISize RNSkAndroidVideo::getSize() { + JNIEnv *env = facebook::jni::Environment::current(); + jclass cls = env->GetObjectClass(_jniVideo.get()); + jmethodID mid = env->GetMethodID(cls, "getSize", "()Landroid/graphics/Point;"); + if (!mid) { + RNSkLogger::logToConsole("getSize method not found"); + return SkISize::Make(0, 0); + } + jobject jPoint = env->CallObjectMethod(_jniVideo.get(), mid); + jclass pointCls = env->GetObjectClass(jPoint); + + jfieldID xFid = env->GetFieldID(pointCls, "x", "I"); + jfieldID yFid = env->GetFieldID(pointCls, "y", "I"); + if (!xFid || !yFid) { + RNSkLogger::logToConsole("Point class fields not found"); + return SkISize::Make(0, 0); + } + + jint width = env->GetIntField(jPoint, xFid); + jint height = env->GetIntField(jPoint, yFid); + + return SkISize::Make(width, height); +} + + } // namespace RNSkia diff --git a/package/android/cpp/rnskia-android/RNSkAndroidVideo.h b/package/android/cpp/rnskia-android/RNSkAndroidVideo.h index 3d08728c6a..0d18c47b74 100644 --- a/package/android/cpp/rnskia-android/RNSkAndroidVideo.h +++ b/package/android/cpp/rnskia-android/RNSkAndroidVideo.h @@ -32,6 +32,7 @@ class RNSkAndroidVideo : public RNSkVideo { double framerate() override; void seek(double timestamp) override; float getRotationInDegrees() override; + SkISize getSize() override; }; } // namespace RNSkia diff --git a/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java b/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java index 7646818f03..63233123f2 100644 --- a/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java +++ b/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java @@ -11,6 +11,7 @@ import android.net.Uri; import android.os.Build; import android.view.Surface; +import android.graphics.Point; import androidx.annotation.RequiresApi; @@ -30,6 +31,8 @@ public class RNSkVideo { private double durationMs; private double frameRate; private int rotationDegrees = 0; + private int width = 0; + private int height = 0; RNSkVideo(Context context, String localUri) { this.uri = Uri.parse(localUri); @@ -57,8 +60,8 @@ private void initializeReader() { if (format.containsKey(MediaFormat.KEY_ROTATION)) { rotationDegrees = format.getInteger(MediaFormat.KEY_ROTATION); } - int width = format.getInteger(MediaFormat.KEY_WIDTH); - int height = format.getInteger(MediaFormat.KEY_HEIGHT); + width = format.getInteger(MediaFormat.KEY_WIDTH); + height = format.getInteger(MediaFormat.KEY_HEIGHT); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { imageReader = ImageReader.newInstance( width, @@ -125,6 +128,11 @@ public void seek(long timestamp) { } } + @DoNotStrip + public Point getSize() { + return new Point(width, height); + } + private int selectVideoTrack(MediaExtractor extractor) { int numTracks = extractor.getTrackCount(); for (int i = 0; i < numTracks; i++) { diff --git a/package/src/dom/nodes/datatypes/Fitting.ts b/package/src/dom/nodes/datatypes/Fitting.ts index 273932dd39..bf084d65c0 100644 --- a/package/src/dom/nodes/datatypes/Fitting.ts +++ b/package/src/dom/nodes/datatypes/Fitting.ts @@ -7,7 +7,10 @@ export interface Size { height: number; } -export const size = (width = 0, height = 0) => ({ width, height }); +export const size = (width = 0, height = 0) => { + "worklet"; + return { width, height }; +}; export const rect2rect = ( src: SkRect, @@ -18,6 +21,7 @@ export const rect2rect = ( { scaleX: number }, { scaleY: number } ] => { + "worklet"; const scaleX = dst.width / src.width; const scaleY = dst.height / src.height; const translateX = dst.x - src.x * scaleX; From 99fe665499d1680cc7a5827718760894d23da5b1 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 09:02:53 +0200 Subject: [PATCH 03/32] :wrench: --- .../snapshots/drawings/rotated-image.png | Bin 0 -> 2970 bytes .../snapshots/drawings/scaled-image.png | Bin 0 -> 4061 bytes .../src/renderer/__tests__/FitBox.spec.tsx | 33 +++++++++++++++++- package/src/skia/__tests__/assets/box.png | Bin 0 -> 904 bytes package/src/skia/__tests__/assets/box2.png | Bin 0 -> 1672 bytes 5 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 package/src/__tests__/snapshots/drawings/rotated-image.png create mode 100644 package/src/__tests__/snapshots/drawings/scaled-image.png create mode 100644 package/src/skia/__tests__/assets/box.png create mode 100644 package/src/skia/__tests__/assets/box2.png diff --git a/package/src/__tests__/snapshots/drawings/rotated-image.png b/package/src/__tests__/snapshots/drawings/rotated-image.png new file mode 100644 index 0000000000000000000000000000000000000000..07ad737909c634f5484c43e2dbf9cdc20cb15106 GIT binary patch literal 2970 zcmeAS@N?(olHy`uVBq!ia0y~yU*#r>mdK II;Vst0E_{RLjV8( literal 0 HcmV?d00001 diff --git a/package/src/__tests__/snapshots/drawings/scaled-image.png b/package/src/__tests__/snapshots/drawings/scaled-image.png new file mode 100644 index 0000000000000000000000000000000000000000..9586774cd7924f30d9b84446c22c8540124e6abb GIT binary patch literal 4061 zcmeAS@N?(olHy`uVBq!ia0y~yUU#5|I_B{{diEO9Z { ); processResult(surface, "snapshots/drawings/lightblue-quarter-circle.png"); }); + it("Should scale the image", () => { + const { Skia, rect } = importSkia(); + const image = Skia.Image.MakeImageFromEncoded( + Skia.Data.fromBytes( + fs.readFileSync( + path.resolve(__dirname, "../../skia/__tests__/assets/box.png") + ) + ) + )!; + const surface = drawOnNode( + + + + ); + processResult(surface, "snapshots/drawings/scaled-image.png"); + }); }); diff --git a/package/src/skia/__tests__/assets/box.png b/package/src/skia/__tests__/assets/box.png new file mode 100644 index 0000000000000000000000000000000000000000..9ea1d64c0c88e9980850a6599bd6c4d8d5b6a2f9 GIT binary patch literal 904 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|G$6%U;1OBOz`!jG!i)^F=172) z6bHFGF|0c$^AgBmNq6*hWMJ6X&;2Knm4Sg-*3-o?q=ND7HAY?r1)c*NoIm9M&U>cv7h@-A}a#}o2;jcV@L(#+iQ%x3<^94HaLIC|H%=l zopkf&O<9GPd=pd|1k^^+Xb6mkz-S22Hw3=5ZQ1NRgSo+gaTE0&x)78&qol`;+070_S3jhEB literal 0 HcmV?d00001 From 7cb098820ea7392f74024dd030630cb6a2c4d020 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 09:13:45 +0200 Subject: [PATCH 04/32] :wrench: --- .../snapshots/drawings/scaled-image2.png | Bin 0 -> 3544 bytes .../src/renderer/__tests__/FitBox.spec.tsx | 31 +++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 package/src/__tests__/snapshots/drawings/scaled-image2.png diff --git a/package/src/__tests__/snapshots/drawings/scaled-image2.png b/package/src/__tests__/snapshots/drawings/scaled-image2.png new file mode 100644 index 0000000000000000000000000000000000000000..1a54ced57d8699469635a5491d56072829bc52fe GIT binary patch literal 3544 zcmeAS@N?(olHy`uVBq!ia0y~yUWp+ z_~Bn&#jyFDReBY3@w@Z3wu}-*wd-GU^4!Ya&3HKOGBCCqo^t@*#vuqK1eAb;f(wvv z0LCptg8(CtR0PGJ6AO@N8C5tMJfn$XG%Jjj6r<(iXstl!O7s0oMur2Qzs-z0u)@~1 x!P5>{A&=JAz(5$SkOsIy0>;587^o01c*h<-lbcykm0Jzu1W#8#mvv4FO#pB{^`HO% literal 0 HcmV?d00001 diff --git a/package/src/renderer/__tests__/FitBox.spec.tsx b/package/src/renderer/__tests__/FitBox.spec.tsx index 48b1d49a46..8607291ef1 100644 --- a/package/src/renderer/__tests__/FitBox.spec.tsx +++ b/package/src/renderer/__tests__/FitBox.spec.tsx @@ -38,7 +38,7 @@ describe("FitBox", () => { ); processResult(surface, "snapshots/drawings/lightblue-quarter-circle.png"); }); - it("Should scale the image", () => { + it("Should scale the image (1)", () => { const { Skia, rect } = importSkia(); const image = Skia.Image.MakeImageFromEncoded( Skia.Data.fromBytes( @@ -66,4 +66,33 @@ describe("FitBox", () => { ); processResult(surface, "snapshots/drawings/scaled-image.png"); }); + it("Should scale the image (2)", () => { + const { Skia, rect } = importSkia(); + const image = Skia.Image.MakeImageFromEncoded( + Skia.Data.fromBytes( + fs.readFileSync( + path.resolve(__dirname, "../../skia/__tests__/assets/box.png") + ) + ) + )!; + const screen = rect(256, 128, 256, 512); + const surface = drawOnNode( + + + + + ); + processResult(surface, "snapshots/drawings/scaled-image2.png", true); + }); }); From c16f64fe6b59f6863d864c7c226992d8fea6c739 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 09:33:19 +0200 Subject: [PATCH 05/32] :wrench: --- .../drawings/rotated-scaled-image.png | Bin 0 -> 3821 bytes .../src/renderer/__tests__/FitBox.spec.tsx | 33 +++++++++++++++++- package/src/skia/__tests__/assets/box2.png | Bin 1672 -> 3531 bytes 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 package/src/__tests__/snapshots/drawings/rotated-scaled-image.png diff --git a/package/src/__tests__/snapshots/drawings/rotated-scaled-image.png b/package/src/__tests__/snapshots/drawings/rotated-scaled-image.png new file mode 100644 index 0000000000000000000000000000000000000000..f41de097280c263284e42650e837d6d5475bb814 GIT binary patch literal 3821 zcmeAS@N?(olHy`uVBq!ia0y~yUl8i=##F!#Pjah<{O5*3sbn=<#*m>wBy!Z?}H` z!p87m?f#dH3=9XXnSkavu>gq{4j{oH2qXlQfP{hzkZ=H+%FrMHRLGz>s&F)TXq6~V znA@R~8D^K?h2}Mqj8$4b8T-G@yGywp!eJ)r4 literal 0 HcmV?d00001 diff --git a/package/src/renderer/__tests__/FitBox.spec.tsx b/package/src/renderer/__tests__/FitBox.spec.tsx index 8607291ef1..11ee50a02c 100644 --- a/package/src/renderer/__tests__/FitBox.spec.tsx +++ b/package/src/renderer/__tests__/FitBox.spec.tsx @@ -93,6 +93,37 @@ describe("FitBox", () => { ); - processResult(surface, "snapshots/drawings/scaled-image2.png", true); + processResult(surface, "snapshots/drawings/scaled-image2.png"); + }); + it("Should rotate and scale the image", () => { + const { Skia, rect } = importSkia(); + const image = Skia.Image.MakeImageFromEncoded( + Skia.Data.fromBytes( + fs.readFileSync( + path.resolve(__dirname, "../../skia/__tests__/assets/box2.png") + ) + ) + )!; + const screen = rect(256, 128, 256, 512); + const surface = drawOnNode( + + + + + + + ); + processResult(surface, "snapshots/drawings/rotated-scaled-image.png", true); }); }); diff --git a/package/src/skia/__tests__/assets/box2.png b/package/src/skia/__tests__/assets/box2.png index 8180456963dda46c52db0795e7cbba88890b65e6..07d55fa35c401e761dbf9826197ceb6048abf4f6 100644 GIT binary patch literal 3531 zcmeHK&ubGw6n>N4#x!l9TCuhDu&!8JvAf#{N|z)in%cxRHHK1Bp_;T!YtZ~4*^<ftgL<(3ph^%#@F4XjJ*5ZHgNS%4YN6u6D(W8~LVYtyQ~eKm*ja}6=I!^s@69m# zV6O%Hx^31bD*)L1K5q!Xgf0P#8O_U^Z^zN_E7l6Hqqv-~?%|_#x6- z#$Y8^Bo2GPzt#c&`f z97|l>o{a8{!cNbow6uy)n#?am1A>Rcb@sY1y8%z z@AdTO$IC;ng95uu zgf1-BMp%5`)xt1ly}1PT5Kag$yk_7cq&emYtr6gR8tx$(Z)t=iW#uh=RjnTRCf2h= zu4qGg9i|F8sXH(<%H$&Of=LN=+ZC98e7%&U>cv7h@-A}a#}o2;jcV@L(#+iQ%x3<^94HaLIC|H%=l zopkf&O<9GPd=pd|1k^^+Xb6mkz-S22Hw3=5ZQ1NRgSo+gaTE0&x)78&qol`;+070_S3jhEB From ec4a47a5a74fb1eedca6e634d0b10c950cc8376b Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 09:38:45 +0200 Subject: [PATCH 06/32] :wrench: --- .../drawings/rotated-scaled-image.png | Bin 3821 -> 3531 bytes .../src/renderer/__tests__/FitBox.spec.tsx | 38 +++++++++++------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/package/src/__tests__/snapshots/drawings/rotated-scaled-image.png b/package/src/__tests__/snapshots/drawings/rotated-scaled-image.png index f41de097280c263284e42650e837d6d5475bb814..589a32482b99a7b9a29492c1412e876509772d86 100644 GIT binary patch delta 139 zcmV;60CfND9m^Y#KqL)yNklt{Z@8@gDKy1PZ!6KiaBp@9_(dN6ku_*=zTcn=^F7b%E~$#oF6^D?t6WYRpss0 z?_by$9<1H}l97Sofc55!Osz};C(LabBucK$oX5cP@xS6}*3BQ8C0PY05Ds#RszWHa}$QViKyUers#nZ3nII;3{?~qXB PWB>wBS3j3^P6 { ) )!; const screen = rect(256, 128, 256, 512); + const transform = fitbox( + "contain", + rect(0, 0, image.height(), image.width()), + screen + ); const surface = drawOnNode( - - - + ); - processResult(surface, "snapshots/drawings/rotated-scaled-image.png", true); + processResult(surface, "snapshots/drawings/rotated-scaled-image.png"); }); }); From ffaf65df1d3ed1c7494dcb544e20076f9b470817 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 09:53:13 +0200 Subject: [PATCH 07/32] :wrench: --- package/src/renderer/__tests__/FitBox.spec.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/package/src/renderer/__tests__/FitBox.spec.tsx b/package/src/renderer/__tests__/FitBox.spec.tsx index ba5929f397..e13f04db33 100644 --- a/package/src/renderer/__tests__/FitBox.spec.tsx +++ b/package/src/renderer/__tests__/FitBox.spec.tsx @@ -104,12 +104,14 @@ describe("FitBox", () => { ) ) )!; + // rotate can be PI/2, -PI/2, or PI + const rotate = -Math.PI / 2; const screen = rect(256, 128, 256, 512); - const transform = fitbox( - "contain", - rect(0, 0, image.height(), image.width()), - screen - ); + const imageRect = + rotate === Math.PI / 2 || rotate === -Math.PI / 2 + ? rect(0, 0, image.height(), image.width()) + : rect(0, 0, image.width(), image.height()); + const transform = fitbox("contain", imageRect, screen); const surface = drawOnNode( { { translate: [-128, 512 - 384] }, // Rotation Transform { translate: [image.width() / 2, image.height() / 2] }, - { rotate: -Math.PI / 2 }, + { rotate }, { translate: [-image.width() / 2, -image.height() / 2] }, ]} /> From 9a04dcbf4eac7e2e8ea5a7ed108339fece993751 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 10:29:23 +0200 Subject: [PATCH 08/32] :wrench: --- package/src/renderer/__tests__/FitBox.spec.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/package/src/renderer/__tests__/FitBox.spec.tsx b/package/src/renderer/__tests__/FitBox.spec.tsx index e13f04db33..b2100aa539 100644 --- a/package/src/renderer/__tests__/FitBox.spec.tsx +++ b/package/src/renderer/__tests__/FitBox.spec.tsx @@ -112,6 +112,18 @@ describe("FitBox", () => { ? rect(0, 0, image.height(), image.width()) : rect(0, 0, image.width(), image.height()); const transform = fitbox("contain", imageRect, screen); + + let translationToTopLeft: { translate: [number, number] } = { + translate: [0, 0], + }; + if (rotate === -Math.PI / 2) { + translationToTopLeft = { translate: [0, image.width()] }; + } else if (rotate === Math.PI / 2) { + translationToTopLeft = { translate: [image.height(), 0] }; + } else if (rotate === Math.PI) { + translationToTopLeft = { translate: [image.width(), image.height()] }; + } + console.log(translationToTopLeft); const surface = drawOnNode( { // Scale the rotated image ...transform, // Translate to top left corner - { translate: [-128, 512 - 384] }, + translationToTopLeft, // Rotation Transform - { translate: [image.width() / 2, image.height() / 2] }, { rotate }, - { translate: [-image.width() / 2, -image.height() / 2] }, ]} /> From 0baafc47fcd7479ba29818214ca2aebd575d3807 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 10:54:33 +0200 Subject: [PATCH 09/32] Add utility to rotate/scale a video frame --- package/ios/RNSkia-iOS/RNSkiOSVideo.mm | 12 ++---- package/src/external/reanimated/useVideo.ts | 9 +++- .../src/renderer/__tests__/FitBox.spec.tsx | 30 ++------------ .../src/renderer/components/shapes/FitBox.tsx | 41 +++++++++++++++++-- package/src/skia/types/Video/Video.ts | 4 +- 5 files changed, 55 insertions(+), 41 deletions(-) diff --git a/package/ios/RNSkia-iOS/RNSkiOSVideo.mm b/package/ios/RNSkia-iOS/RNSkiOSVideo.mm index 146769e118..b18f199566 100644 --- a/package/ios/RNSkia-iOS/RNSkiOSVideo.mm +++ b/package/ios/RNSkia-iOS/RNSkiOSVideo.mm @@ -106,19 +106,15 @@ // Determine the rotation angle in radians if (transform.a == 0 && transform.b == 1 && transform.c == -1 && transform.d == 0) { - rotationAngle = M_PI_2; // 90 degrees + rotationAngle = 90; } else if (transform.a == 0 && transform.b == -1 && transform.c == 1 && transform.d == 0) { - rotationAngle = -M_PI_2; // -90 degrees + rotationAngle = 270; } else if (transform.a == -1 && transform.b == 0 && transform.c == 0 && transform.d == -1) { - rotationAngle = M_PI; // 180 degrees - } else if (transform.a == 1 && transform.b == 0 && transform.c == 0 && - transform.d == 1) { - rotationAngle = 0.0; // 0 degrees + rotationAngle = 180; } - // Convert the rotation angle from radians to degrees - return rotationAngle * 180 / M_PI; + return rotationAngle; } void RNSkiOSVideo::seek(double timeInMilliseconds) { diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index d9772651e5..15a5a3a697 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -79,5 +79,12 @@ export const useVideo = ( }; }, [video]); - return { currentFrame, currentTime, duration, framerate, rotationInDegrees, size }; + return { + currentFrame, + currentTime, + duration, + framerate, + rotationInDegrees, + size, + }; }; diff --git a/package/src/renderer/__tests__/FitBox.spec.tsx b/package/src/renderer/__tests__/FitBox.spec.tsx index b2100aa539..f3002568d0 100644 --- a/package/src/renderer/__tests__/FitBox.spec.tsx +++ b/package/src/renderer/__tests__/FitBox.spec.tsx @@ -104,26 +104,9 @@ describe("FitBox", () => { ) ) )!; - // rotate can be PI/2, -PI/2, or PI - const rotate = -Math.PI / 2; const screen = rect(256, 128, 256, 512); - const imageRect = - rotate === Math.PI / 2 || rotate === -Math.PI / 2 - ? rect(0, 0, image.height(), image.width()) - : rect(0, 0, image.width(), image.height()); - const transform = fitbox("contain", imageRect, screen); - - let translationToTopLeft: { translate: [number, number] } = { - translate: [0, 0], - }; - if (rotate === -Math.PI / 2) { - translationToTopLeft = { translate: [0, image.width()] }; - } else if (rotate === Math.PI / 2) { - translationToTopLeft = { translate: [image.height(), 0] }; - } else if (rotate === Math.PI) { - translationToTopLeft = { translate: [image.width(), image.height()] }; - } - console.log(translationToTopLeft); + const imageRect = rect(0, 0, image.width(), image.height()); + const transform = fitbox("contain", imageRect, screen, 270); const surface = drawOnNode( { y={0} width={image.width()} height={image.height()} - transform={[ - // Scale the rotated image - ...transform, - // Translate to top left corner - translationToTopLeft, - // Rotation Transform - { rotate }, - ]} + transform={transform} /> diff --git a/package/src/renderer/components/shapes/FitBox.tsx b/package/src/renderer/components/shapes/FitBox.tsx index 02acebfd42..42a1ac39d2 100644 --- a/package/src/renderer/components/shapes/FitBox.tsx +++ b/package/src/renderer/components/shapes/FitBox.tsx @@ -3,7 +3,7 @@ import React, { useMemo } from "react"; import type { Fit } from "../../../dom/nodes"; import { fitRects, rect2rect } from "../../../dom/nodes"; -import type { SkRect } from "../../../skia/types"; +import type { SkRect, Transforms3d } from "../../../skia/types"; import { Group } from "../Group"; interface FitProps { @@ -13,10 +13,43 @@ interface FitProps { children: ReactNode | ReactNode[]; } -export const fitbox = (fit: Fit, src: SkRect, dst: SkRect) => { +export const fitbox = ( + fit: Fit, + src: SkRect, + dst: SkRect, + rotation: 0 | 90 | 180 | 270 = 0 +) => { "worklet"; - const rects = fitRects(fit, src, dst); - return rect2rect(rects.src, rects.dst); + const rects = fitRects( + fit, + rotation === 90 || rotation === 270 + ? { x: 0, y: 0, width: src.height, height: src.width } + : src, + dst + ); + const result = rect2rect(rects.src, rects.dst); + if (rotation === 90) { + return [ + ...result, + { translate: [src.height, 0] }, + { rotate: Math.PI / 2 }, + ] as Transforms3d; + } + if (rotation === 180) { + return [ + ...result, + { translate: [src.width, src.height] }, + { rotate: Math.PI }, + ] as Transforms3d; + } + if (rotation === 270) { + return [ + ...result, + { translate: [0, src.width] }, + { rotate: -Math.PI / 2 }, + ] as Transforms3d; + } + return result; }; export const FitBox = ({ fit = "contain", src, dst, children }: FitProps) => { diff --git a/package/src/skia/types/Video/Video.ts b/package/src/skia/types/Video/Video.ts index 5e5d6d6fd3..784cb395ea 100644 --- a/package/src/skia/types/Video/Video.ts +++ b/package/src/skia/types/Video/Video.ts @@ -1,11 +1,13 @@ import type { SkImage } from "../Image"; import type { SkJSIInstance } from "../JsiInstance"; +export type VideoRotation = 0 | 90 | 180 | 270; + export interface Video extends SkJSIInstance<"Video"> { duration(): number; framerate(): number; nextImage(): SkImage | null; seek(time: number): void; - getRotationInDegrees(): number; + getRotationInDegrees(): VideoRotation; size(): { width: number; height: number }; } From d2063d5a59f5ba9aeb71c6cb9b777bff96aeb0f7 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 11:05:32 +0200 Subject: [PATCH 10/32] :books: --- docs/docs/video.md | 48 ++++++++++++++++++- package/src/external/reanimated/useVideo.ts | 7 +-- .../src/renderer/__tests__/FitBox.spec.tsx | 1 + 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/docs/docs/video.md b/docs/docs/video.md index 8890c70215..8ee1bd447c 100644 --- a/docs/docs/video.md +++ b/docs/docs/video.md @@ -96,7 +96,53 @@ export const useVideoFromAsset = ( ## Returned Values -The `useVideo` hook returns `currentFrame` which contains the current video frame, as well as `currentTime`, and `rotationInDegrees`. +The `useVideo` hook returns `currentFrame` which contains the current video frame, as well as `currentTime`, `rotation`, and `size`. + +## Rotated Video + +`rotation` can either be `0`, `90`, `180`, or `270`. +We provide a `fitbox` function that can help rotating and scaling the video. + +```tsx twoslash +import React from "react"; +import { + Canvas, + Image, + useVideo, + fitbox, + rect +} from "@shopify/react-native-skia"; +import { Pressable, useWindowDimensions } from "react-native"; +import { useSharedValue } from "react-native-reanimated"; + +interface VideoExampleProps { + localVideoFile: string; +} + +// The URL needs to be a local path; we usually use expo-asset for that. +export const VideoExample = ({ localVideoFile }: VideoExampleProps) => { + const paused = useSharedValue(false); + const { width, height } = useWindowDimensions(); + const { currentFrame, rotation, size } = useVideo(require(localVideoFile)); + const src = rect(0, 0, size.width, size.height); + const dst = rect(0, 0, width, height) + const transform = fitbox("cover", src, dst, rotation); + return ( + + + + ); +}; +``` + ## Playback Options diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index 15a5a3a697..b4fdf5a6e9 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -50,10 +50,7 @@ export const useVideo = ( const duration = useMemo(() => video?.duration() ?? 0, [video]); const framerate = useMemo(() => video?.framerate() ?? 0, [video]); const size = useMemo(() => video?.size() ?? { width: 0, height: 0 }, [video]); - const rotationInDegrees = useMemo( - () => video?.getRotationInDegrees() ?? 0, - [video] - ); + const rotation = useMemo(() => video?.getRotationInDegrees() ?? 0, [video]); Rea.useFrameCallback((frameInfo: FrameInfo) => { processVideoState( video, @@ -84,7 +81,7 @@ export const useVideo = ( currentTime, duration, framerate, - rotationInDegrees, + rotation, size, }; }; diff --git a/package/src/renderer/__tests__/FitBox.spec.tsx b/package/src/renderer/__tests__/FitBox.spec.tsx index f3002568d0..569a8dfdb9 100644 --- a/package/src/renderer/__tests__/FitBox.spec.tsx +++ b/package/src/renderer/__tests__/FitBox.spec.tsx @@ -115,6 +115,7 @@ describe("FitBox", () => { y={0} width={image.width()} height={image.height()} + fit="none" transform={transform} /> From d31c0a8158feb2724946adec787919b72a1a94b0 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 11:09:48 +0200 Subject: [PATCH 11/32] :green_heart: --- package/cpp/api/JsiVideo.h | 4 ++-- package/src/external/reanimated/useVideo.ts | 2 +- package/src/renderer/__tests__/Video.spec.tsx | 3 ++- package/src/skia/types/Video/Video.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package/cpp/api/JsiVideo.h b/package/cpp/api/JsiVideo.h index 6c1c9514c4..da0dcad8c5 100644 --- a/package/cpp/api/JsiVideo.h +++ b/package/cpp/api/JsiVideo.h @@ -53,7 +53,7 @@ class JsiVideo : public JsiSkWrappingSharedPtrHostObject { return jsi::Value::undefined(); } - JSI_HOST_FUNCTION(getRotationInDegrees) { + JSI_HOST_FUNCTION(rotation) { auto context = getContext(); auto rot = getObject()->getRotationInDegrees(); return jsi::Value(static_cast(rot)); @@ -72,7 +72,7 @@ class JsiVideo : public JsiSkWrappingSharedPtrHostObject { JSI_EXPORT_FUNC(JsiVideo, duration), JSI_EXPORT_FUNC(JsiVideo, framerate), JSI_EXPORT_FUNC(JsiVideo, seek), - JSI_EXPORT_FUNC(JsiVideo, getRotationInDegrees), + JSI_EXPORT_FUNC(JsiVideo, rotation), JSI_EXPORT_FUNC(JsiVideo, size), JSI_EXPORT_FUNC(JsiVideo, dispose)) diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index b4fdf5a6e9..71aac12040 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -50,7 +50,7 @@ export const useVideo = ( const duration = useMemo(() => video?.duration() ?? 0, [video]); const framerate = useMemo(() => video?.framerate() ?? 0, [video]); const size = useMemo(() => video?.size() ?? { width: 0, height: 0 }, [video]); - const rotation = useMemo(() => video?.getRotationInDegrees() ?? 0, [video]); + const rotation = useMemo(() => video?.rotation() ?? 0, [video]); Rea.useFrameCallback((frameInfo: FrameInfo) => { processVideoState( video, diff --git a/package/src/renderer/__tests__/Video.spec.tsx b/package/src/renderer/__tests__/Video.spec.tsx index eccab935e3..ff2b086d1f 100644 --- a/package/src/renderer/__tests__/Video.spec.tsx +++ b/package/src/renderer/__tests__/Video.spec.tsx @@ -34,7 +34,8 @@ describe("Video Player", () => { duration: jest.fn().mockReturnValue(duration), seek: jest.fn(), nextImage: jest.fn().mockReturnValue({} as SkImage), - getRotationInDegrees: jest.fn().mockReturnValue(0), + rotation: jest.fn().mockReturnValue(0), + size: jest.fn().mockReturnValue({ width: 0, height: 0 }), }; options = { playbackSpeed: 1, diff --git a/package/src/skia/types/Video/Video.ts b/package/src/skia/types/Video/Video.ts index 784cb395ea..7175de8cd8 100644 --- a/package/src/skia/types/Video/Video.ts +++ b/package/src/skia/types/Video/Video.ts @@ -8,6 +8,6 @@ export interface Video extends SkJSIInstance<"Video"> { framerate(): number; nextImage(): SkImage | null; seek(time: number): void; - getRotationInDegrees(): VideoRotation; + rotation(): VideoRotation; size(): { width: number; height: number }; } From 5f5b70fac3a05b0d9eadfe52bc21ebb6e016cb7c Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 11:42:05 +0200 Subject: [PATCH 12/32] :green_heart: --- package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp b/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp index ba203d03f9..c73bc4d79c 100644 --- a/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp +++ b/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp @@ -106,14 +106,15 @@ float RNSkAndroidVideo::getRotationInDegrees() { SkISize RNSkAndroidVideo::getSize() { JNIEnv *env = facebook::jni::Environment::current(); jclass cls = env->GetObjectClass(_jniVideo.get()); - jmethodID mid = env->GetMethodID(cls, "getSize", "()Landroid/graphics/Point;"); + jmethodID mid = + env->GetMethodID(cls, "getSize", "()Landroid/graphics/Point;"); if (!mid) { RNSkLogger::logToConsole("getSize method not found"); return SkISize::Make(0, 0); } jobject jPoint = env->CallObjectMethod(_jniVideo.get(), mid); jclass pointCls = env->GetObjectClass(jPoint); - + jfieldID xFid = env->GetFieldID(pointCls, "x", "I"); jfieldID yFid = env->GetFieldID(pointCls, "y", "I"); if (!xFid || !yFid) { @@ -127,5 +128,4 @@ SkISize RNSkAndroidVideo::getSize() { return SkISize::Make(width, height); } - } // namespace RNSkia From f52718f6a6cef15aff695e464842af6e7618d3ff Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 13:26:11 +0200 Subject: [PATCH 13/32] :wrench: --- example/src/App.tsx | 2 +- example/src/Examples/Video/Video.tsx | 2 + package/cpp/api/JsiVideo.h | 19 ++++ package/cpp/rnskia/RNSkVideo.h | 3 + package/ios/RNSkia-iOS/RNSkiOSVideo.h | 13 ++- package/ios/RNSkia-iOS/RNSkiOSVideo.mm | 114 +++++++++----------- package/src/external/reanimated/useVideo.ts | 19 ++++ package/src/external/reanimated/video.ts | 28 ++--- package/src/skia/types/Video/Video.ts | 3 + 9 files changed, 122 insertions(+), 81 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 4fd9c447a9..02af9b745c 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -98,7 +98,7 @@ const App = () => { screenOptions={{ headerLeft: HeaderLeft, }} - initialRouteName={CI ? "Tests" : "Home"} + initialRouteName={CI ? "Tests" : "Video"} > { const paused = useSharedValue(false); + const seek = useSharedValue(0); const { width, height } = useWindowDimensions(); const { currentFrame } = useVideoFromAsset( require("../../Tests/assets/BigBuckBunny.mp4"), { paused, looping: true, + seek, } ); return ( diff --git a/package/cpp/api/JsiVideo.h b/package/cpp/api/JsiVideo.h index da0dcad8c5..213673d6cd 100644 --- a/package/cpp/api/JsiVideo.h +++ b/package/cpp/api/JsiVideo.h @@ -68,12 +68,31 @@ class JsiVideo : public JsiSkWrappingSharedPtrHostObject { return result; } + JSI_HOST_FUNCTION(play) { + getObject()->play(); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(pause) { + getObject()->pause(); + return jsi::Value::undefined(); + } + + JSI_HOST_FUNCTION(setVolume) { + auto volume = arguments[0].asNumber(); + getObject()->setVolume(static_cast(volume)); + return jsi::Value::undefined(); + } + JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiVideo, nextImage), JSI_EXPORT_FUNC(JsiVideo, duration), JSI_EXPORT_FUNC(JsiVideo, framerate), JSI_EXPORT_FUNC(JsiVideo, seek), JSI_EXPORT_FUNC(JsiVideo, rotation), JSI_EXPORT_FUNC(JsiVideo, size), + JSI_EXPORT_FUNC(JsiVideo, play), + JSI_EXPORT_FUNC(JsiVideo, pause), + JSI_EXPORT_FUNC(JsiVideo, setVolume), JSI_EXPORT_FUNC(JsiVideo, dispose)) JsiVideo(std::shared_ptr context, diff --git a/package/cpp/rnskia/RNSkVideo.h b/package/cpp/rnskia/RNSkVideo.h index d02d76359e..924906497d 100644 --- a/package/cpp/rnskia/RNSkVideo.h +++ b/package/cpp/rnskia/RNSkVideo.h @@ -20,6 +20,9 @@ class RNSkVideo { virtual void seek(double timestamp) = 0; virtual float getRotationInDegrees() = 0; virtual SkISize getSize() = 0; + virtual void play() = 0; + virtual void pause() = 0; + virtual void setVolume(float volume) = 0; }; } // namespace RNSkia diff --git a/package/ios/RNSkia-iOS/RNSkiOSVideo.h b/package/ios/RNSkia-iOS/RNSkiOSVideo.h index 4344c63c86..31ed7287ef 100644 --- a/package/ios/RNSkia-iOS/RNSkiOSVideo.h +++ b/package/ios/RNSkia-iOS/RNSkiOSVideo.h @@ -21,16 +21,18 @@ namespace RNSkia { class RNSkiOSVideo : public RNSkVideo { private: std::string _url; - AVAssetReader *_reader = nullptr; - AVAssetReaderTrackOutput *_trackOutput = nullptr; + AVPlayer *_player = nullptr; + AVPlayerItem *_playerItem = nullptr; + AVPlayerItemVideoOutput *_videoOutput = nullptr; RNSkPlatformContext *_context; double _duration = 0; double _framerate = 0; float _videoWidth = 0; float _videoHeight = 0; - void setupReader(CMTimeRange timeRange); - NSDictionary *getOutputSettings(); CGAffineTransform _preferredTransform; + bool _isPlaying = false; + void setupPlayer(); + NSDictionary *getOutputSettings(); public: RNSkiOSVideo(std::string url, RNSkPlatformContext *context); @@ -39,8 +41,11 @@ class RNSkiOSVideo : public RNSkVideo { double duration() override; double framerate() override; void seek(double timestamp) override; + void play(); + void pause(); float getRotationInDegrees() override; SkISize getSize() override; + void setVolume(float volume); }; } // namespace RNSkia diff --git a/package/ios/RNSkia-iOS/RNSkiOSVideo.mm b/package/ios/RNSkia-iOS/RNSkiOSVideo.mm index b18f199566..42872ef576 100644 --- a/package/ios/RNSkia-iOS/RNSkiOSVideo.mm +++ b/package/ios/RNSkia-iOS/RNSkiOSVideo.mm @@ -16,80 +16,56 @@ RNSkiOSVideo::RNSkiOSVideo(std::string url, RNSkPlatformContext *context) : _url(std::move(url)), _context(context) { - setupReader(CMTimeRangeMake(kCMTimeZero, kCMTimePositiveInfinity)); + setupPlayer(); } -RNSkiOSVideo::~RNSkiOSVideo() {} - -void RNSkiOSVideo::setupReader(CMTimeRange timeRange) { - NSError *error = nil; - - AVURLAsset *asset = - [AVURLAsset URLAssetWithURL:[NSURL URLWithString:@(_url.c_str())] - options:nil]; - AVAssetReader *assetReader = [[AVAssetReader alloc] initWithAsset:asset - error:&error]; - if (error) { - NSLog(@"Error initializing asset reader: %@", error.localizedDescription); - return; +RNSkiOSVideo::~RNSkiOSVideo() { + if (_player) { + [_player pause]; } +} - CMTime time = [asset duration]; - if (time.timescale == 0) { - NSLog(@"Error: Timescale of the asset is zero."); - return; - } +void RNSkiOSVideo::setupPlayer() { + NSURL *videoURL = [NSURL URLWithString:@(_url.c_str())]; + AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:videoURL]; + _player = [AVPlayer playerWithPlayerItem:playerItem]; + _playerItem = playerItem; - _duration = CMTimeGetSeconds(time) * 1000; // Store duration in milliseconds - AVAssetTrack *videoTrack = - [[asset tracksWithMediaType:AVMediaTypeVideo] firstObject]; - _framerate = videoTrack.nominalFrameRate; - _preferredTransform = videoTrack.preferredTransform; - CGSize videoSize = videoTrack.naturalSize; - _videoWidth = videoSize.width; - _videoHeight = videoSize.height; NSDictionary *outputSettings = getOutputSettings(); - AVAssetReaderTrackOutput *trackOutput = - [[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack - outputSettings:outputSettings]; - - assetReader.timeRange = timeRange; - if ([assetReader canAddOutput:trackOutput]) { - [assetReader addOutput:trackOutput]; - [assetReader startReading]; - } else { - NSLog(@"Cannot add output to asset reader."); - return; + _videoOutput = [[AVPlayerItemVideoOutput alloc] initWithOutputSettings:outputSettings]; + [playerItem addOutput:_videoOutput]; + + CMTime time = playerItem.asset.duration; + if (time.timescale != 0) { + _duration = CMTimeGetSeconds(time) * 1000; // Store duration in milliseconds } - _reader = assetReader; - _trackOutput = trackOutput; + AVAssetTrack *videoTrack = [[playerItem.asset tracksWithMediaType:AVMediaTypeVideo] firstObject]; + if (videoTrack) { + _framerate = videoTrack.nominalFrameRate; + _preferredTransform = videoTrack.preferredTransform; + CGSize videoSize = videoTrack.naturalSize; + _videoWidth = videoSize.width; + _videoHeight = videoSize.height; + } + play(); } sk_sp RNSkiOSVideo::nextImage(double *timeStamp) { - CMSampleBufferRef sampleBuffer = [_trackOutput copyNextSampleBuffer]; - if (!sampleBuffer) { - NSLog(@"No sample buffer."); - return nullptr; - } - - // Extract the pixel buffer from the sample buffer - CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CMTime currentTime = [_player currentTime]; + CVPixelBufferRef pixelBuffer = [_videoOutput copyPixelBufferForItemTime:currentTime itemTimeForDisplay:nullptr]; if (!pixelBuffer) { NSLog(@"No pixel buffer."); - CFRelease(sampleBuffer); return nullptr; } - auto skImage = _context->makeImageFromNativeBuffer( - reinterpret_cast(pixelBuffer)); + auto skImage = _context->makeImageFromNativeBuffer((void *)pixelBuffer); if (timeStamp) { - CMTime time = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); - *timeStamp = CMTimeGetSeconds(time); + *timeStamp = CMTimeGetSeconds(currentTime); } - CFRelease(sampleBuffer); + CVPixelBufferRelease(pixelBuffer); return skImage; } @@ -118,16 +94,26 @@ } void RNSkiOSVideo::seek(double timeInMilliseconds) { - if (_reader) { - [_reader cancelReading]; - _reader = nil; - _trackOutput = nil; + CMTime seekTime = CMTimeMakeWithSeconds(timeInMilliseconds / 1000.0, NSEC_PER_SEC); + [_player seekToTime:seekTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) { + if (!finished) { + NSLog(@"Seek failed or was interrupted."); + } + }]; +} + +void RNSkiOSVideo::play() { + if (_player) { + [_player play]; + _isPlaying = true; } +} - CMTime startTime = - CMTimeMakeWithSeconds(timeInMilliseconds / 1000.0, NSEC_PER_SEC); - CMTimeRange timeRange = CMTimeRangeMake(startTime, kCMTimePositiveInfinity); - setupReader(timeRange); +void RNSkiOSVideo::pause() { + if (_player) { + [_player pause]; + _isPlaying = false; + } } double RNSkiOSVideo::duration() { return _duration; } @@ -138,4 +124,8 @@ return SkISize::Make(_videoWidth, _videoHeight); } +void RNSkiOSVideo::setVolume(float volume) { + _player.volume = volume; +} + } // namespace RNSkia diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index 71aac12040..73c8161c07 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -51,6 +51,25 @@ export const useVideo = ( const framerate = useMemo(() => video?.framerate() ?? 0, [video]); const size = useMemo(() => video?.size() ?? { width: 0, height: 0 }, [video]); const rotation = useMemo(() => video?.rotation() ?? 0, [video]); + Rea.useAnimatedReaction( + () => isPaused.value, + (paused) => { + if (paused) { + video?.pause(); + } else { + video?.play(); + } + } + ); + Rea.useAnimatedReaction( + () => seek.value, + (value) => { + if (value !== null) { + video?.seek(value); + seek.value = null; + } + } + ); Rea.useFrameCallback((frameInfo: FrameInfo) => { processVideoState( video, diff --git a/package/src/external/reanimated/video.ts b/package/src/external/reanimated/video.ts index 9720d8aa83..b8d5dbc6a7 100644 --- a/package/src/external/reanimated/video.ts +++ b/package/src/external/reanimated/video.ts @@ -53,26 +53,26 @@ export const processVideoState = ( if (!video) { return; } - if (options.paused) { - return; - } + // if (options.paused) { + // return; + // } const delta = currentTimestamp - lastTimestamp.value; const frameDuration = 1000 / framerate; const currentFrameDuration = Math.floor( frameDuration / options.playbackSpeed ); - if (currentTime.value + delta >= duration && options.looping) { - seek.value = 0; - } - if (seek.value !== null) { - video.seek(seek.value); - currentTime.value = seek.value; - setFrame(video, currentFrame); - lastTimestamp.value = currentTimestamp; - seek.value = null; - return; - } + // if (currentTime.value + delta >= duration && options.looping) { + // seek.value = 0; + // } + // if (seek.value !== null) { + // video.seek(seek.value); + // currentTime.value = seek.value; + // setFrame(video, currentFrame); + // lastTimestamp.value = currentTimestamp; + // seek.value = null; + // return; + // } if (delta >= currentFrameDuration) { setFrame(video, currentFrame); diff --git a/package/src/skia/types/Video/Video.ts b/package/src/skia/types/Video/Video.ts index 7175de8cd8..5a23678d25 100644 --- a/package/src/skia/types/Video/Video.ts +++ b/package/src/skia/types/Video/Video.ts @@ -10,4 +10,7 @@ export interface Video extends SkJSIInstance<"Video"> { seek(time: number): void; rotation(): VideoRotation; size(): { width: number; height: number }; + pause(): void; + play(): void; + setVolume(volume: number): void; } From 72666a2539be0dea3b5315d1f74d14d0fb87f7e7 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Wed, 5 Jun 2024 19:59:29 +0200 Subject: [PATCH 14/32] :wrench: --- example/src/Examples/Video/Video.tsx | 10 ++++- package/src/external/reanimated/useVideo.ts | 47 ++++++++++++--------- package/src/external/reanimated/video.ts | 46 +------------------- 3 files changed, 38 insertions(+), 65 deletions(-) diff --git a/example/src/Examples/Video/Video.tsx b/example/src/Examples/Video/Video.tsx index 3a4481c8af..66fd232c17 100644 --- a/example/src/Examples/Video/Video.tsx +++ b/example/src/Examples/Video/Video.tsx @@ -6,7 +6,7 @@ import { ImageShader, } from "@shopify/react-native-skia"; import { Pressable, useWindowDimensions } from "react-native"; -import { useSharedValue } from "react-native-reanimated"; +import { useAnimatedReaction, useSharedValue } from "react-native-reanimated"; import { useVideoFromAsset } from "../../components/Animations"; @@ -14,7 +14,7 @@ export const Video = () => { const paused = useSharedValue(false); const seek = useSharedValue(0); const { width, height } = useWindowDimensions(); - const { currentFrame } = useVideoFromAsset( + const { currentFrame, currentTime } = useVideoFromAsset( require("../../Tests/assets/BigBuckBunny.mp4"), { paused, @@ -22,6 +22,12 @@ export const Video = () => { seek, } ); + useAnimatedReaction( + () => currentTime.value, + (time) => { + console.log({ time }); + } + ); return ( { - processVideoState( - video, - duration, - framerate, - frameInfo.timestamp, - { - paused: isPaused.value, - looping: looping.value, - playbackSpeed: playbackSpeed.value, - }, - currentTime, - currentFrame, - lastTimestamp, - seek + "worklet"; + if (!video) { + return; + } + if (isPaused.value) { + return; + } + const currentTimestamp = frameInfo.timestamp; + if (lastTimestamp.value === -1) { + lastTimestamp.value = currentTimestamp; + } + const delta = currentTimestamp - lastTimestamp.value; + + const frameDuration = 1000 / framerate; + const currentFrameDuration = Math.floor( + frameDuration / playbackSpeed.value ); + if (currentTime.value + delta >= duration && looping) { + seek.value = 0; + currentTime.value = seek.value; + lastTimestamp.value = currentTimestamp; + } + if (delta >= currentFrameDuration) { + setFrame(video, currentFrame); + currentTime.value += delta; + lastTimestamp.value = currentTimestamp; + } }); useEffect(() => { diff --git a/package/src/external/reanimated/video.ts b/package/src/external/reanimated/video.ts index b8d5dbc6a7..a3ee874482 100644 --- a/package/src/external/reanimated/video.ts +++ b/package/src/external/reanimated/video.ts @@ -4,7 +4,7 @@ import type { SkImage, Video } from "../../skia/types"; import { Platform } from "../../Platform"; export type Animated = SharedValue | T; - +// TODO: Move to useVideo.ts export interface PlaybackOptions { playbackSpeed: Animated; looping: Animated; @@ -20,6 +20,7 @@ export type MaterializedPlaybackOptions = Materialized< Omit >; +// TODO: move export const setFrame = ( video: Video, currentFrame: SharedValue @@ -37,46 +38,3 @@ export const setFrame = ( } } }; - -export const processVideoState = ( - video: Video | null, - duration: number, - framerate: number, - currentTimestamp: number, - options: Materialized>, - currentTime: SharedValue, - currentFrame: SharedValue, - lastTimestamp: SharedValue, - seek: SharedValue -) => { - "worklet"; - if (!video) { - return; - } - // if (options.paused) { - // return; - // } - const delta = currentTimestamp - lastTimestamp.value; - - const frameDuration = 1000 / framerate; - const currentFrameDuration = Math.floor( - frameDuration / options.playbackSpeed - ); - // if (currentTime.value + delta >= duration && options.looping) { - // seek.value = 0; - // } - // if (seek.value !== null) { - // video.seek(seek.value); - // currentTime.value = seek.value; - // setFrame(video, currentFrame); - // lastTimestamp.value = currentTimestamp; - // seek.value = null; - // return; - // } - - if (delta >= currentFrameDuration) { - setFrame(video, currentFrame); - currentTime.value += delta; - lastTimestamp.value = currentTimestamp; - } -}; From 99334faf43e83dee5acd584df38f668838583831 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 07:08:21 +0200 Subject: [PATCH 15/32] :wrench: --- example/src/Examples/Video/Video.tsx | 3 ++- package/src/external/reanimated/useVideo.ts | 10 +++++++++- package/src/external/reanimated/video.ts | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/example/src/Examples/Video/Video.tsx b/example/src/Examples/Video/Video.tsx index 66fd232c17..7cd3e61d51 100644 --- a/example/src/Examples/Video/Video.tsx +++ b/example/src/Examples/Video/Video.tsx @@ -18,8 +18,9 @@ export const Video = () => { require("../../Tests/assets/BigBuckBunny.mp4"), { paused, - looping: true, + looping: false, seek, + volume: 0.2, } ); useAnimatedReaction( diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index 152814f467..dbe0951b11 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -13,6 +13,7 @@ const defaultOptions = { paused: false, seek: null, currentTime: 0, + volume: 0, }; const useOption = (value: Animated) => { @@ -37,6 +38,7 @@ export const useVideo = ( const isPaused = useOption(userOptions?.paused ?? defaultOptions.paused); const looping = useOption(userOptions?.looping ?? defaultOptions.looping); const seek = useOption(userOptions?.seek ?? defaultOptions.seek); + const volume = useOption(userOptions?.volume ?? defaultOptions.volume); const playbackSpeed = useOption( userOptions?.playbackSpeed ?? defaultOptions.playbackSpeed ); @@ -66,6 +68,12 @@ export const useVideo = ( seek.value = null; } } + ); + Rea.useAnimatedReaction( + () => volume.value, + (value) => { + video?.setVolume(value); + } ); Rea.useFrameCallback((frameInfo: FrameInfo) => { "worklet"; @@ -85,7 +93,7 @@ export const useVideo = ( const currentFrameDuration = Math.floor( frameDuration / playbackSpeed.value ); - if (currentTime.value + delta >= duration && looping) { + if (currentTime.value + delta >= duration && looping.value) { seek.value = 0; currentTime.value = seek.value; lastTimestamp.value = currentTimestamp; diff --git a/package/src/external/reanimated/video.ts b/package/src/external/reanimated/video.ts index a3ee874482..268a690694 100644 --- a/package/src/external/reanimated/video.ts +++ b/package/src/external/reanimated/video.ts @@ -10,6 +10,7 @@ export interface PlaybackOptions { looping: Animated; paused: Animated; seek: Animated; + volume: Animated; } type Materialized = { From 96be66d8c6f965ae1db967af150189faeb88b5ad Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 07:34:28 +0200 Subject: [PATCH 16/32] :wrench: --- example/ios/Podfile.lock | 6 ++ example/package.json | 1 + example/src/Examples/Video/Video.tsx | 87 +++++++++++++-------- example/yarn.lock | 5 ++ package/src/external/reanimated/useVideo.ts | 7 +- 5 files changed, 70 insertions(+), 36 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index c8532d3fdb..2945186a34 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -356,6 +356,8 @@ PODS: - React - React-callinvoker - React-Core + - react-native-slider (4.4.2): + - React-Core - React-perflogger (0.71.7) - React-RCTActionSheet (0.71.7): - React-Core/RCTActionSheetHeaders (= 0.71.7) @@ -511,6 +513,7 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - "react-native-skia (from `../node_modules/@shopify/react-native-skia`)" + - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - React-perflogger (from `../node_modules/react-native/ReactCommon/reactperflogger`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) - React-RCTAnimation (from `../node_modules/react-native/Libraries/NativeAnimation`) @@ -607,6 +610,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-safe-area-context" react-native-skia: :path: "../node_modules/@shopify/react-native-skia" + react-native-slider: + :path: "../node_modules/@react-native-community/slider" React-perflogger: :path: "../node_modules/react-native/ReactCommon/reactperflogger" React-RCTActionSheet: @@ -687,6 +692,7 @@ SPEC CHECKSUMS: React-logger: 3f8ebad1be1bf3299d1ab6d7f971802d7395c7ef react-native-safe-area-context: dfe5aa13bee37a0c7e8059d14f72ffc076d120e9 react-native-skia: c2c416b864962e73d8b9c81f0fa399ee89c8435e + react-native-slider: 33b8d190b59d4f67a541061bb91775d53d617d9d React-perflogger: 2d505bbe298e3b7bacdd9e542b15535be07220f6 React-RCTActionSheet: 0e96e4560bd733c9b37efbf68f5b1a47615892fb React-RCTAnimation: fd138e26f120371c87e406745a27535e2c8a04ef diff --git a/example/package.json b/example/package.json index 73054d43b4..d076c81920 100644 --- a/example/package.json +++ b/example/package.json @@ -14,6 +14,7 @@ "android-reverse-tcp": "adb devices | grep '\t' | awk '{print $1}' | sed 's/\\s//g' | xargs -I {} adb -s {} reverse tcp:8081 tcp:8081" }, "dependencies": { + "@react-native-community/slider": "4.4.2", "@react-navigation/bottom-tabs": "6.5.7", "@react-navigation/elements": "1.3.6", "@react-navigation/native": "6.0.13", diff --git a/example/src/Examples/Video/Video.tsx b/example/src/Examples/Video/Video.tsx index 7cd3e61d51..d47d19b53b 100644 --- a/example/src/Examples/Video/Video.tsx +++ b/example/src/Examples/Video/Video.tsx @@ -4,9 +4,16 @@ import { ColorMatrix, Fill, ImageShader, + Text, + useFont, } from "@shopify/react-native-skia"; -import { Pressable, useWindowDimensions } from "react-native"; -import { useAnimatedReaction, useSharedValue } from "react-native-reanimated"; +import { Alert, Pressable, View, useWindowDimensions } from "react-native"; +import { + useAnimatedReaction, + useDerivedValue, + useSharedValue, +} from "react-native-reanimated"; +import Slider from "@react-native-community/slider"; import { useVideoFromAsset } from "../../components/Animations"; @@ -14,44 +21,56 @@ export const Video = () => { const paused = useSharedValue(false); const seek = useSharedValue(0); const { width, height } = useWindowDimensions(); - const { currentFrame, currentTime } = useVideoFromAsset( + const fontSize = 20; + const font = useFont(require("../../assets/SF-Mono-Semibold.otf"), fontSize); + const { currentFrame, currentTime, duration } = useVideoFromAsset( require("../../Tests/assets/BigBuckBunny.mp4"), { paused, - looping: false, + looping: true, seek, - volume: 0.2, - } - ); - useAnimatedReaction( - () => currentTime.value, - (time) => { - console.log({ time }); + volume: 0, } ); + const text = useDerivedValue(() => currentTime.value.toFixed(0)); return ( - (paused.value = !paused.value)} - > - - - - - - - + + (paused.value = !paused.value)} + > + + + + + + + + + + { + seek.value = value * duration; + }} + /> + + ); }; diff --git a/example/yarn.lock b/example/yarn.lock index 53dd8fcb71..00291751a9 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -2379,6 +2379,11 @@ resolved "https://registry.yarnpkg.com/@react-native-community/eslint-plugin/-/eslint-plugin-1.3.0.tgz#9e558170c106bbafaa1ef502bd8e6d4651012bf9" integrity sha512-+zDZ20NUnSWghj7Ku5aFphMzuM9JulqCW+aPXT6IfIXFbb8tzYTTOSeRFOtuekJ99ibW2fUCSsjuKNlwDIbHFg== +"@react-native-community/slider@4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@react-native-community/slider/-/slider-4.4.2.tgz#1fea0eb3ae31841fe87bd6c4fc67569066e9cf4b" + integrity sha512-D9bv+3Vd2gairAhnRPAghwccgEmoM7g562pm8i4qB3Esrms5mggF81G3UvCyc0w3jjtFHh8dpQkfEoKiP0NW/Q== + "@react-native/assets@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@react-native/assets/-/assets-1.0.0.tgz#c6f9bf63d274bafc8e970628de24986b30a55c8e" diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index dbe0951b11..378e5e283b 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -64,8 +64,10 @@ export const useVideo = ( () => seek.value, (value) => { if (value !== null) { + video?.pause(); video?.seek(value); seek.value = null; + video?.play(); } } ); @@ -93,12 +95,13 @@ export const useVideo = ( const currentFrameDuration = Math.floor( frameDuration / playbackSpeed.value ); - if (currentTime.value + delta >= duration && looping.value) { + const isOver = currentTime.value + delta > duration + if (isOver && looping.value) { seek.value = 0; currentTime.value = seek.value; lastTimestamp.value = currentTimestamp; } - if (delta >= currentFrameDuration) { + if (delta >= currentFrameDuration && !isOver) { setFrame(video, currentFrame); currentTime.value += delta; lastTimestamp.value = currentTimestamp; From 503a5834207e72970c9a0cc31d2dbe7da97e34cb Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 07:36:41 +0200 Subject: [PATCH 17/32] :green_heart: --- package/src/renderer/__tests__/e2e/Video.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package/src/renderer/__tests__/e2e/Video.spec.tsx b/package/src/renderer/__tests__/e2e/Video.spec.tsx index 82591c2342..f65764ba6c 100644 --- a/package/src/renderer/__tests__/e2e/Video.spec.tsx +++ b/package/src/renderer/__tests__/e2e/Video.spec.tsx @@ -15,8 +15,8 @@ describe("Videos", () => { expect(result).toEqual({ duration: 5280, framerate: 25, - height: 0, - width: 0, + height: 720, + width: 1280, }); }); // TODO: We need to reanable these tests once we can run them on the UI thread From 054961c54d1ef7a60d9a2979f2a3e6640ebeb962 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 07:46:49 +0200 Subject: [PATCH 18/32] :wrench: --- example/src/Examples/Video/Video.tsx | 12 +- package/src/external/reanimated/useVideo.ts | 43 ++++- package/src/external/reanimated/video.ts | 41 ----- package/src/renderer/__tests__/Video.spec.tsx | 166 ------------------ 4 files changed, 47 insertions(+), 215 deletions(-) delete mode 100644 package/src/external/reanimated/video.ts delete mode 100644 package/src/renderer/__tests__/Video.spec.tsx diff --git a/example/src/Examples/Video/Video.tsx b/example/src/Examples/Video/Video.tsx index d47d19b53b..b5e10db71b 100644 --- a/example/src/Examples/Video/Video.tsx +++ b/example/src/Examples/Video/Video.tsx @@ -7,12 +7,8 @@ import { Text, useFont, } from "@shopify/react-native-skia"; -import { Alert, Pressable, View, useWindowDimensions } from "react-native"; -import { - useAnimatedReaction, - useDerivedValue, - useSharedValue, -} from "react-native-reanimated"; +import { Pressable, View, useWindowDimensions } from "react-native"; +import { useDerivedValue, useSharedValue } from "react-native-reanimated"; import Slider from "@react-native-community/slider"; import { useVideoFromAsset } from "../../components/Animations"; @@ -68,6 +64,10 @@ export const Video = () => { maximumTrackTintColor="#000000" onSlidingComplete={(value) => { seek.value = value * duration; + paused.value = false; + }} + onSlidingStart={() => { + paused.value = true; }} /> diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index 378e5e283b..87b33a44c1 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -1,11 +1,50 @@ -import { type FrameInfo } from "react-native-reanimated"; +import type { SharedValue, FrameInfo } from "react-native-reanimated"; import { useEffect, useMemo } from "react"; import { Skia } from "../../skia/Skia"; import type { SkImage, Video } from "../../skia/types"; import Rea from "./ReanimatedProxy"; -import { setFrame, type Animated, type PlaybackOptions } from "./video"; +import { Platform } from "../../Platform"; + + +export type Animated = SharedValue | T; +// TODO: Move to useVideo.ts +export interface PlaybackOptions { + playbackSpeed: Animated; + looping: Animated; + paused: Animated; + seek: Animated; + volume: Animated; +} + +type Materialized = { + [K in keyof T]: T[K] extends Animated ? U : T[K]; +}; + +export type MaterializedPlaybackOptions = Materialized< + Omit +>; + +// TODO: move +export const setFrame = ( + video: Video, + currentFrame: SharedValue +) => { + "worklet"; + const img = video.nextImage(); + if (img) { + if (currentFrame.value) { + currentFrame.value.dispose(); + } + if (Platform.OS === "android") { + currentFrame.value = img.makeNonTextureImage(); + } else { + currentFrame.value = img; + } + } +}; + const defaultOptions = { playbackSpeed: 1, diff --git a/package/src/external/reanimated/video.ts b/package/src/external/reanimated/video.ts deleted file mode 100644 index 268a690694..0000000000 --- a/package/src/external/reanimated/video.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { SharedValue } from "react-native-reanimated"; - -import type { SkImage, Video } from "../../skia/types"; -import { Platform } from "../../Platform"; - -export type Animated = SharedValue | T; -// TODO: Move to useVideo.ts -export interface PlaybackOptions { - playbackSpeed: Animated; - looping: Animated; - paused: Animated; - seek: Animated; - volume: Animated; -} - -type Materialized = { - [K in keyof T]: T[K] extends Animated ? U : T[K]; -}; - -export type MaterializedPlaybackOptions = Materialized< - Omit ->; - -// TODO: move -export const setFrame = ( - video: Video, - currentFrame: SharedValue -) => { - "worklet"; - const img = video.nextImage(); - if (img) { - if (currentFrame.value) { - currentFrame.value.dispose(); - } - if (Platform.OS === "android") { - currentFrame.value = img.makeNonTextureImage(); - } else { - currentFrame.value = img; - } - } -}; diff --git a/package/src/renderer/__tests__/Video.spec.tsx b/package/src/renderer/__tests__/Video.spec.tsx deleted file mode 100644 index ff2b086d1f..0000000000 --- a/package/src/renderer/__tests__/Video.spec.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import type { SharedValue } from "react-native-reanimated"; - -import type { SkImage, Video } from "../../skia/types"; -import { - processVideoState, - type MaterializedPlaybackOptions, -} from "../../external/reanimated/video"; - -const createValue = (value: T) => ({ value } as unknown as SharedValue); - -jest.mock("../../Platform", () => ({ - Platform: { - OS: "ios", - }, -})); - -// Test cases -describe("Video Player", () => { - let mockVideo: Video; - let options: MaterializedPlaybackOptions; - let currentTimestamp: number; - - const currentTime = createValue(0); - const currentFrame = createValue(null); - const lastTimestamp = createValue(0); - const seek = createValue(null); - const framerate = 30; - const duration = 5000; - beforeEach(() => { - mockVideo = { - __typename__: "Video", - dispose: jest.fn(), - framerate: jest.fn().mockReturnValue(framerate), - duration: jest.fn().mockReturnValue(duration), - seek: jest.fn(), - nextImage: jest.fn().mockReturnValue({} as SkImage), - rotation: jest.fn().mockReturnValue(0), - size: jest.fn().mockReturnValue({ width: 0, height: 0 }), - }; - options = { - playbackSpeed: 1, - looping: false, - paused: false, - }; - currentTimestamp = 0; - currentTime.value = 0; - currentFrame.value = null; - lastTimestamp.value = 0; - }); - - test("should not update state when paused", () => { - options.paused = true; - processVideoState( - mockVideo, - duration, - framerate, - currentTimestamp, - options, - currentTime, - currentFrame, - lastTimestamp, - seek - ); - expect(currentTime.value).toBe(0); - expect(currentFrame.value).toBeNull(); - expect(lastTimestamp.value).toBe(0); - }); - - test("should update state with next frame if not paused and delta exceeds frame duration", () => { - currentTimestamp = 100; - lastTimestamp.value = 0; - processVideoState( - mockVideo, - duration, - framerate, - currentTimestamp, - options, - currentTime, - currentFrame, - lastTimestamp, - seek - ); - expect(currentFrame.value).not.toBeNull(); - expect(currentTime.value).toBe(100); - expect(lastTimestamp.value).toBe(100); - }); - - test("should handle looping when current time exceeds video duration", () => { - currentTimestamp = 5100; - lastTimestamp.value = 0; - currentTime.value = 5000; - options.looping = true; - processVideoState( - mockVideo, - duration, - framerate, - currentTimestamp, - options, - currentTime, - currentFrame, - lastTimestamp, - seek - ); - expect(seek.value).toBe(null); - expect(currentTime.value).toBe(0); - }); - - test("should seek to specified time", () => { - seek.value = 2000; - processVideoState( - mockVideo, - duration, - framerate, - currentTimestamp, - options, - currentTime, - currentFrame, - lastTimestamp, - seek - ); - expect(mockVideo.seek).toHaveBeenCalledWith(2000); - expect(currentTime.value).toBe(2000); - expect(currentFrame.value).not.toBeNull(); - expect(lastTimestamp.value).toBe(currentTimestamp); - expect(seek.value).toBeNull(); - }); - - test("should not update frame if delta does not exceed frame duration", () => { - currentTimestamp = 10; - lastTimestamp.value = 0; - processVideoState( - mockVideo, - duration, - framerate, - currentTimestamp, - options, - currentTime, - currentFrame, - lastTimestamp, - seek - ); - expect(currentFrame.value).toBeNull(); - expect(currentTime.value).toBe(0); - expect(lastTimestamp.value).toBe(0); - }); - - test("should update frame based on playback speed", () => { - options.playbackSpeed = 2; // double speed - currentTimestamp = 100; - lastTimestamp.value = 0; - processVideoState( - mockVideo, - duration, - framerate, - currentTimestamp, - options, - currentTime, - currentFrame, - lastTimestamp, - seek - ); - expect(currentFrame.value).not.toBeNull(); - expect(currentTime.value).toBe(100); - expect(lastTimestamp.value).toBe(100); - }); -}); From 3c9582aa740fefa8c7a80c0b2ed17e62c0441a00 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 08:30:54 +0200 Subject: [PATCH 19/32] Add java impl --- example/src/Examples/Video/Video.tsx | 7 ++- .../cpp/rnskia-android/RNSkAndroidVideo.cpp | 32 ++++++++++++ .../cpp/rnskia-android/RNSkAndroidVideo.h | 4 ++ .../shopify/reactnative/skia/RNSkVideo.java | 52 ++++++++++++++++++- 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/example/src/Examples/Video/Video.tsx b/example/src/Examples/Video/Video.tsx index b5e10db71b..8528ea2d9b 100644 --- a/example/src/Examples/Video/Video.tsx +++ b/example/src/Examples/Video/Video.tsx @@ -52,7 +52,12 @@ export const Video = () => { ]} /> - + diff --git a/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp b/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp index c73bc4d79c..28b3fbd434 100644 --- a/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp +++ b/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp @@ -128,4 +128,36 @@ SkISize RNSkAndroidVideo::getSize() { return SkISize::Make(width, height); } +void RNSkAndroidVideo::play() { + JNIEnv *env = facebook::jni::Environment::current(); + jclass cls = env->GetObjectClass(_jniVideo.get()); + jmethodID mid = env->GetMethodID(cls, "play", "()V"); + if (!mid) { + RNSkLogger::logToConsole("play method not found"); + return; + } + env->CallVoidMethod(_jniVideo.get(), mid); +} + +void RNSkAndroidVideo::pause() { + JNIEnv *env = facebook::jni::Environment::current(); + jclass cls = env->GetObjectClass(_jniVideo.get()); + jmethodID mid = env->GetMethodID(cls, "pause", "()V"); + if (!mid) { + RNSkLogger::logToConsole("pause method not found"); + return; + } + env->CallVoidMethod(_jniVideo.get(), mid); +} + +void RNSkAndroidVideo::setVolume(float volume) { + JNIEnv *env = facebook::jni::Environment::current(); + jclass cls = env->GetObjectClass(_jniVideo.get()); + jmethodID mid = env->GetMethodID(cls, "setVolume", "(F)V"); + if (!mid) { + RNSkLogger::logToConsole("setVolume method not found"); + return; + } + env->CallVoidMethod(_jniVideo.get(), mid, volume); +} } // namespace RNSkia diff --git a/package/android/cpp/rnskia-android/RNSkAndroidVideo.h b/package/android/cpp/rnskia-android/RNSkAndroidVideo.h index 0d18c47b74..3353f2cdfe 100644 --- a/package/android/cpp/rnskia-android/RNSkAndroidVideo.h +++ b/package/android/cpp/rnskia-android/RNSkAndroidVideo.h @@ -33,6 +33,10 @@ class RNSkAndroidVideo : public RNSkVideo { void seek(double timestamp) override; float getRotationInDegrees() override; SkISize getSize() override; + void play() override; + void pause() override; + void setVolume(float volume) override; + }; } // namespace RNSkia diff --git a/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java b/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java index 63233123f2..3761a73a84 100644 --- a/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java +++ b/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java @@ -3,11 +3,15 @@ import android.content.Context; import android.graphics.ImageFormat; import android.hardware.HardwareBuffer; -import android.media.Image; -import android.media.ImageReader; +import android.media.AudioAttributes; +import android.media.AudioManager; import android.media.MediaCodec; import android.media.MediaExtractor; import android.media.MediaFormat; +import android.media.MediaPlayer; +import android.media.MediaSync; +import android.media.Image; +import android.media.ImageReader; import android.net.Uri; import android.os.Build; import android.view.Surface; @@ -28,12 +32,16 @@ public class RNSkVideo { private MediaCodec decoder; private ImageReader imageReader; private Surface outputSurface; + private MediaPlayer mediaPlayer; + private MediaSync mediaSync; private double durationMs; private double frameRate; private int rotationDegrees = 0; private int width = 0; private int height = 0; + private boolean isPlaying = false; + RNSkVideo(Context context, String localUri) { this.uri = Uri.parse(localUri); this.context = context; @@ -50,6 +58,18 @@ private void initializeReader() { } extractor.selectTrack(trackIndex); MediaFormat format = extractor.getTrackFormat(trackIndex); + + // Initialize MediaPlayer + mediaPlayer = new MediaPlayer(); + mediaPlayer.setDataSource(context, uri); + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mediaPlayer.setOnPreparedListener(mp -> { + durationMs = mp.getDuration(); + mp.start(); + isPlaying = true; + }); + mediaPlayer.prepareAsync(); + // Retrieve and store video properties if (format.containsKey(MediaFormat.KEY_DURATION)) { durationMs = format.getLong(MediaFormat.KEY_DURATION) / 1000; // Convert microseconds to milliseconds @@ -126,6 +146,10 @@ public void seek(long timestamp) { if (decoder != null) { decoder.flush(); } + + if (mediaPlayer != null) { + mediaPlayer.seekTo((int) timestamp); + } } @DoNotStrip @@ -187,7 +211,31 @@ private void decodeFrame() { } } + public void play() { + if (mediaPlayer != null && !isPlaying) { + mediaPlayer.start(); + isPlaying = true; + } + } + + public void pause() { + if (mediaPlayer != null && isPlaying) { + mediaPlayer.pause(); + isPlaying = false; + } + } + + public void setVolume(float volume) { + if (mediaPlayer != null) { + mediaPlayer.setVolume(volume, volume); + } + } + public void release() { + if (mediaPlayer != null) { + mediaPlayer.release(); + mediaPlayer = null; + } if (decoder != null) { decoder.stop(); decoder.release(); From 609a303ae4b07a2e9aa269eb4b0825b58327a83b Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 09:34:50 +0200 Subject: [PATCH 20/32] :wrench: --- .../cpp/rnskia-android/RNSkAndroidVideo.cpp | 2 +- .../com/shopify/reactnative/skia/RNSkVideo.java | 16 ++++++++++------ package/src/external/reanimated/useVideo.ts | 1 + 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp b/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp index 28b3fbd434..48cd9ea99d 100644 --- a/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp +++ b/package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp @@ -83,7 +83,7 @@ double RNSkAndroidVideo::framerate() { void RNSkAndroidVideo::seek(double timestamp) { JNIEnv *env = facebook::jni::Environment::current(); jclass cls = env->GetObjectClass(_jniVideo.get()); - jmethodID mid = env->GetMethodID(cls, "seek", "(J)V"); + jmethodID mid = env->GetMethodID(cls, "seek", "(D)V"); if (!mid) { RNSkLogger::logToConsole("seek method not found"); return; diff --git a/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java b/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java index 3761a73a84..b99a74008e 100644 --- a/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java +++ b/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java @@ -139,17 +139,18 @@ public HardwareBuffer nextImage() { } @DoNotStrip - public void seek(long timestamp) { + public void seek(double timestamp) { // Seek to the closest sync frame at or before the specified time - extractor.seekTo(timestamp * 1000, MediaExtractor.SEEK_TO_PREVIOUS_SYNC); + long timestampUs = (long)(timestamp * 1000); + extractor.seekTo(timestampUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC); + if (mediaPlayer != null) { + int timestampMs = (int)timestamp; // Convert to milliseconds + mediaPlayer.seekTo(timestampMs); + } // Flush the codec to reset internal state and buffers if (decoder != null) { decoder.flush(); } - - if (mediaPlayer != null) { - mediaPlayer.seekTo((int) timestamp); - } } @DoNotStrip @@ -211,6 +212,7 @@ private void decodeFrame() { } } + @DoNotStrip public void play() { if (mediaPlayer != null && !isPlaying) { mediaPlayer.start(); @@ -218,6 +220,7 @@ public void play() { } } + @DoNotStrip public void pause() { if (mediaPlayer != null && isPlaying) { mediaPlayer.pause(); @@ -225,6 +228,7 @@ public void pause() { } } + @DoNotStrip public void setVolume(float volume) { if (mediaPlayer != null) { mediaPlayer.setVolume(volume, volume); diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index 87b33a44c1..17bf7d4ca8 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -104,6 +104,7 @@ export const useVideo = ( (value) => { if (value !== null) { video?.pause(); + console.log("seeking to ", value, " seconds."); video?.seek(value); seek.value = null; video?.play(); From 377846b9e28d0fecee835618d7d3b0e76976e94b Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 09:36:27 +0200 Subject: [PATCH 21/32] :wrench: --- .../shopify/reactnative/skia/RNSkVideo.java | 22 +++++++++++++++---- package/src/external/reanimated/useVideo.ts | 3 --- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java b/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java index b99a74008e..cdc4b71d77 100644 --- a/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java +++ b/package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java @@ -140,19 +140,33 @@ public HardwareBuffer nextImage() { @DoNotStrip public void seek(double timestamp) { - // Seek to the closest sync frame at or before the specified time - long timestampUs = (long)(timestamp * 1000); + // Log the values for debugging + + long timestampUs = (long)(timestamp * 1000); // Convert milliseconds to microseconds + extractor.seekTo(timestampUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC); if (mediaPlayer != null) { - int timestampMs = (int)timestamp; // Convert to milliseconds - mediaPlayer.seekTo(timestampMs); + int timestampMs = (int) timestamp; // Convert to milliseconds + mediaPlayer.seekTo(timestampMs, MediaPlayer.SEEK_CLOSEST); } + // Flush the codec to reset internal state and buffers if (decoder != null) { decoder.flush(); + + // Decode frames until reaching the exact timestamp + boolean isSeeking = true; + while (isSeeking) { + decodeFrame(); + long currentTimestampUs = extractor.getSampleTime(); + if (currentTimestampUs >= timestampUs) { + isSeeking = false; + } + } } } + @DoNotStrip public Point getSize() { return new Point(width, height); diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index 17bf7d4ca8..2acde79fdf 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -103,11 +103,8 @@ export const useVideo = ( () => seek.value, (value) => { if (value !== null) { - video?.pause(); - console.log("seeking to ", value, " seconds."); video?.seek(value); seek.value = null; - video?.play(); } } ); From 0944f3f8e479426e4045c97087252065e13ab465 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 10:29:58 +0200 Subject: [PATCH 22/32] Update documentation --- docs/docs/video.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/docs/video.md b/docs/docs/video.md index 8ee1bd447c..1e692b8cba 100644 --- a/docs/docs/video.md +++ b/docs/docs/video.md @@ -9,10 +9,9 @@ React Native Skia provides a way to load video frames as images, enabling rich m ## Requirements +- **Reanimated** version 3 or higher. - **Android:** API level 26 or higher. - **Video URL:** Must be a local path. We recommend using it in combination with [expo-asset](https://docs.expo.dev/versions/latest/sdk/asset/) to download the video. -- **Animated Playback:** Available only via [Reanimated 3](/docs/animations/animations) and above. -- **Sound Playback:** Coming soon. In the meantime, audio can be played using [expo-av](https://docs.expo.dev/versions/latest/sdk/av/). ## Example From a0ac4ca473150db9bd64a19951b2d92ef5517113 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 10:40:05 +0200 Subject: [PATCH 23/32] :green_heart: --- .../cpp/rnskia-android/RNSkAndroidVideo.h | 1 - package/cpp/api/JsiVideo.h | 16 ++++------ package/ios/RNSkia-iOS/RNSkiOSVideo.mm | 30 +++++++++++-------- package/src/external/reanimated/useVideo.ts | 10 +++---- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/package/android/cpp/rnskia-android/RNSkAndroidVideo.h b/package/android/cpp/rnskia-android/RNSkAndroidVideo.h index 3353f2cdfe..4198d667f4 100644 --- a/package/android/cpp/rnskia-android/RNSkAndroidVideo.h +++ b/package/android/cpp/rnskia-android/RNSkAndroidVideo.h @@ -36,7 +36,6 @@ class RNSkAndroidVideo : public RNSkVideo { void play() override; void pause() override; void setVolume(float volume) override; - }; } // namespace RNSkia diff --git a/package/cpp/api/JsiVideo.h b/package/cpp/api/JsiVideo.h index 213673d6cd..6dbca1ef69 100644 --- a/package/cpp/api/JsiVideo.h +++ b/package/cpp/api/JsiVideo.h @@ -84,16 +84,12 @@ class JsiVideo : public JsiSkWrappingSharedPtrHostObject { return jsi::Value::undefined(); } - JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiVideo, nextImage), - JSI_EXPORT_FUNC(JsiVideo, duration), - JSI_EXPORT_FUNC(JsiVideo, framerate), - JSI_EXPORT_FUNC(JsiVideo, seek), - JSI_EXPORT_FUNC(JsiVideo, rotation), - JSI_EXPORT_FUNC(JsiVideo, size), - JSI_EXPORT_FUNC(JsiVideo, play), - JSI_EXPORT_FUNC(JsiVideo, pause), - JSI_EXPORT_FUNC(JsiVideo, setVolume), - JSI_EXPORT_FUNC(JsiVideo, dispose)) + JSI_EXPORT_FUNCTIONS( + JSI_EXPORT_FUNC(JsiVideo, nextImage), JSI_EXPORT_FUNC(JsiVideo, duration), + JSI_EXPORT_FUNC(JsiVideo, framerate), JSI_EXPORT_FUNC(JsiVideo, seek), + JSI_EXPORT_FUNC(JsiVideo, rotation), JSI_EXPORT_FUNC(JsiVideo, size), + JSI_EXPORT_FUNC(JsiVideo, play), JSI_EXPORT_FUNC(JsiVideo, pause), + JSI_EXPORT_FUNC(JsiVideo, setVolume), JSI_EXPORT_FUNC(JsiVideo, dispose)) JsiVideo(std::shared_ptr context, std::shared_ptr video) diff --git a/package/ios/RNSkia-iOS/RNSkiOSVideo.mm b/package/ios/RNSkia-iOS/RNSkiOSVideo.mm index 42872ef576..ed47544e3c 100644 --- a/package/ios/RNSkia-iOS/RNSkiOSVideo.mm +++ b/package/ios/RNSkia-iOS/RNSkiOSVideo.mm @@ -32,7 +32,8 @@ _playerItem = playerItem; NSDictionary *outputSettings = getOutputSettings(); - _videoOutput = [[AVPlayerItemVideoOutput alloc] initWithOutputSettings:outputSettings]; + _videoOutput = + [[AVPlayerItemVideoOutput alloc] initWithOutputSettings:outputSettings]; [playerItem addOutput:_videoOutput]; CMTime time = playerItem.asset.duration; @@ -40,7 +41,8 @@ _duration = CMTimeGetSeconds(time) * 1000; // Store duration in milliseconds } - AVAssetTrack *videoTrack = [[playerItem.asset tracksWithMediaType:AVMediaTypeVideo] firstObject]; + AVAssetTrack *videoTrack = + [[playerItem.asset tracksWithMediaType:AVMediaTypeVideo] firstObject]; if (videoTrack) { _framerate = videoTrack.nominalFrameRate; _preferredTransform = videoTrack.preferredTransform; @@ -53,7 +55,9 @@ sk_sp RNSkiOSVideo::nextImage(double *timeStamp) { CMTime currentTime = [_player currentTime]; - CVPixelBufferRef pixelBuffer = [_videoOutput copyPixelBufferForItemTime:currentTime itemTimeForDisplay:nullptr]; + CVPixelBufferRef pixelBuffer = + [_videoOutput copyPixelBufferForItemTime:currentTime + itemTimeForDisplay:nullptr]; if (!pixelBuffer) { NSLog(@"No pixel buffer."); return nullptr; @@ -94,12 +98,16 @@ } void RNSkiOSVideo::seek(double timeInMilliseconds) { - CMTime seekTime = CMTimeMakeWithSeconds(timeInMilliseconds / 1000.0, NSEC_PER_SEC); - [_player seekToTime:seekTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) { - if (!finished) { - NSLog(@"Seek failed or was interrupted."); - } - }]; + CMTime seekTime = + CMTimeMakeWithSeconds(timeInMilliseconds / 1000.0, NSEC_PER_SEC); + [_player seekToTime:seekTime + toleranceBefore:kCMTimeZero + toleranceAfter:kCMTimeZero + completionHandler:^(BOOL finished) { + if (!finished) { + NSLog(@"Seek failed or was interrupted."); + } + }]; } void RNSkiOSVideo::play() { @@ -124,8 +132,6 @@ return SkISize::Make(_videoWidth, _videoHeight); } -void RNSkiOSVideo::setVolume(float volume) { - _player.volume = volume; -} +void RNSkiOSVideo::setVolume(float volume) { _player.volume = volume; } } // namespace RNSkia diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index 2acde79fdf..c0a1f55e06 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -1,12 +1,11 @@ -import type { SharedValue, FrameInfo } from "react-native-reanimated"; +import type { SharedValue, FrameInfo } from "react-native-reanimated"; import { useEffect, useMemo } from "react"; import { Skia } from "../../skia/Skia"; import type { SkImage, Video } from "../../skia/types"; - -import Rea from "./ReanimatedProxy"; import { Platform } from "../../Platform"; +import Rea from "./ReanimatedProxy"; export type Animated = SharedValue | T; // TODO: Move to useVideo.ts @@ -45,7 +44,6 @@ export const setFrame = ( } }; - const defaultOptions = { playbackSpeed: 1, looping: true, @@ -107,7 +105,7 @@ export const useVideo = ( seek.value = null; } } - ); + ); Rea.useAnimatedReaction( () => volume.value, (value) => { @@ -132,7 +130,7 @@ export const useVideo = ( const currentFrameDuration = Math.floor( frameDuration / playbackSpeed.value ); - const isOver = currentTime.value + delta > duration + const isOver = currentTime.value + delta > duration; if (isOver && looping.value) { seek.value = 0; currentTime.value = seek.value; From 626ebcbafe17dd08d6bcebf6ae5833764e8afb02 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 10:41:38 +0200 Subject: [PATCH 24/32] Remove bogus change --- example/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 02af9b745c..4fd9c447a9 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -98,7 +98,7 @@ const App = () => { screenOptions={{ headerLeft: HeaderLeft, }} - initialRouteName={CI ? "Tests" : "Video"} + initialRouteName={CI ? "Tests" : "Home"} > Date: Thu, 6 Jun 2024 11:49:18 +0200 Subject: [PATCH 25/32] remove bogus file --- package/.vscode/settings.json | 68 ----------------------------------- 1 file changed, 68 deletions(-) delete mode 100644 package/.vscode/settings.json diff --git a/package/.vscode/settings.json b/package/.vscode/settings.json deleted file mode 100644 index a4f44e08fc..0000000000 --- a/package/.vscode/settings.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "files.associations": { - "algorithm": "cpp", - "__bit_reference": "cpp", - "__config": "cpp", - "__hash_table": "cpp", - "__locale": "cpp", - "__node_handle": "cpp", - "__split_buffer": "cpp", - "__threading_support": "cpp", - "__tree": "cpp", - "__verbose_abort": "cpp", - "array": "cpp", - "bitset": "cpp", - "cctype": "cpp", - "cfenv": "cpp", - "charconv": "cpp", - "cinttypes": "cpp", - "clocale": "cpp", - "cmath": "cpp", - "codecvt": "cpp", - "complex": "cpp", - "condition_variable": "cpp", - "cstdarg": "cpp", - "cstddef": "cpp", - "cstdint": "cpp", - "cstdio": "cpp", - "cstdlib": "cpp", - "cstring": "cpp", - "ctime": "cpp", - "cwchar": "cpp", - "cwctype": "cpp", - "deque": "cpp", - "execution": "cpp", - "fstream": "cpp", - "initializer_list": "cpp", - "iomanip": "cpp", - "ios": "cpp", - "iosfwd": "cpp", - "iostream": "cpp", - "istream": "cpp", - "limits": "cpp", - "list": "cpp", - "locale": "cpp", - "map": "cpp", - "mutex": "cpp", - "new": "cpp", - "optional": "cpp", - "ostream": "cpp", - "queue": "cpp", - "ratio": "cpp", - "regex": "cpp", - "set": "cpp", - "shared_mutex": "cpp", - "sstream": "cpp", - "stack": "cpp", - "stdexcept": "cpp", - "streambuf": "cpp", - "string": "cpp", - "string_view": "cpp", - "tuple": "cpp", - "typeinfo": "cpp", - "unordered_map": "cpp", - "unordered_set": "cpp", - "variant": "cpp", - "vector": "cpp" - } -} \ No newline at end of file From c2d9128f9d82c410466398a6db01b7accf42b5ee Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 11:50:12 +0200 Subject: [PATCH 26/32] Remove bogus files --- .../rotated-scaled-image.png-diff-test.png | Bin 3662 -> 0 bytes .../drawings/rotated-scaled-image.png.test.png | Bin 3245 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 package/src/__tests__/snapshots/drawings/rotated-scaled-image.png-diff-test.png delete mode 100644 package/src/__tests__/snapshots/drawings/rotated-scaled-image.png.test.png diff --git a/package/src/__tests__/snapshots/drawings/rotated-scaled-image.png-diff-test.png b/package/src/__tests__/snapshots/drawings/rotated-scaled-image.png-diff-test.png deleted file mode 100644 index a6bf30aeb7a917537a9be868827783cf5c4961a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3662 zcmeAS@N?(olHy`uVBq!ia0y~yU|lPgYaPSld*%n0ji#f~tTbAh(6vCl!FS$kP61P2N`pj83mX( z=u{=7ZqwfEq}}7&^%kgX6bw`d{0X*aEQ{4UupX(h{yc`w?@Hh6{AOWzHrgH_)Fc|H7Tu`5zz{ef|C@eZfOxw4xvX Date: Thu, 6 Jun 2024 12:08:01 +0200 Subject: [PATCH 27/32] :wrench: --- package/src/external/reanimated/useVideo.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index c0a1f55e06..4e46a6c3e9 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -102,6 +102,7 @@ export const useVideo = ( (value) => { if (value !== null) { video?.seek(value); + currentTime.value = value; seek.value = null; } } From e91d087e98047f7c60ef1cd9af67038b5ea89871 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 14:47:27 +0200 Subject: [PATCH 28/32] Update docs --- docs/docs/video.md | 158 +++++++++----------- package/src/external/reanimated/useVideo.ts | 30 +--- 2 files changed, 81 insertions(+), 107 deletions(-) diff --git a/docs/docs/video.md b/docs/docs/video.md index 1e692b8cba..34d4d9081e 100644 --- a/docs/docs/video.md +++ b/docs/docs/video.md @@ -7,15 +7,18 @@ slug: /video React Native Skia provides a way to load video frames as images, enabling rich multimedia experiences within your applications. A video frame can be used anywhere a Skia image is accepted: `Image`, `ImageShader`, and `Atlas`. -## Requirements +### Requirements - **Reanimated** version 3 or higher. - **Android:** API level 26 or higher. -- **Video URL:** Must be a local path. We recommend using it in combination with [expo-asset](https://docs.expo.dev/versions/latest/sdk/asset/) to download the video. ## Example -Here is an example of how to use the video support in React Native Skia. This example demonstrates how to load and display video frames within a canvas, applying a color matrix for visual effects. Tapping the screen will pause and play the video. +Here is an example of how to use the video support in React Native Skia. +This example demonstrates how to load and display video frames within a canvas, applying a color matrix for visual effects. +Tapping the screen will pause and play the video. + +The video can be a remote (`http://...`) or local URL (`file://`), as well as a [video from the bundle](#using-assets). ```tsx twoslash import React from "react"; @@ -29,16 +32,11 @@ import { import { Pressable, useWindowDimensions } from "react-native"; import { useSharedValue } from "react-native-reanimated"; -interface VideoExampleProps { - localVideoFile: string; -} - -// The URL needs to be a local path; we usually use expo-asset for that. -export const VideoExample = ({ localVideoFile }: VideoExampleProps) => { +export const VideoExample = () => { const paused = useSharedValue(false); const { width, height } = useWindowDimensions(); const { currentFrame } = useVideo( - require(localVideoFile), + "https://bit.ly/skia-video", { paused, } @@ -71,85 +69,17 @@ export const VideoExample = ({ localVideoFile }: VideoExampleProps) => { }; ``` -## Using expo-asset - -Below is an example of how to use [expo-asset](https://docs.expo.dev/versions/latest/sdk/asset/) to load the video. - -```tsx twoslash -import { useVideo } from "@shopify/react-native-skia"; -import { useAssets } from "expo-asset"; - -// Example usage: -// const video = useVideoFromAsset(require("./BigBuckBunny.mp4")); -export const useVideoFromAsset = ( - mod: number, - options?: Parameters[1] -) => { - const [assets, error] = useAssets([mod]); - if (error) { - throw error; - } - return useVideo(assets ? assets[0].localUri : null, options); -}; -``` - ## Returned Values The `useVideo` hook returns `currentFrame` which contains the current video frame, as well as `currentTime`, `rotation`, and `size`. -## Rotated Video - -`rotation` can either be `0`, `90`, `180`, or `270`. -We provide a `fitbox` function that can help rotating and scaling the video. - -```tsx twoslash -import React from "react"; -import { - Canvas, - Image, - useVideo, - fitbox, - rect -} from "@shopify/react-native-skia"; -import { Pressable, useWindowDimensions } from "react-native"; -import { useSharedValue } from "react-native-reanimated"; - -interface VideoExampleProps { - localVideoFile: string; -} - -// The URL needs to be a local path; we usually use expo-asset for that. -export const VideoExample = ({ localVideoFile }: VideoExampleProps) => { - const paused = useSharedValue(false); - const { width, height } = useWindowDimensions(); - const { currentFrame, rotation, size } = useVideo(require(localVideoFile)); - const src = rect(0, 0, size.width, size.height); - const dst = rect(0, 0, width, height) - const transform = fitbox("cover", src, dst, rotation); - return ( - - - - ); -}; -``` - - ## Playback Options You can seek a video via the `seek` playback option. By default, the seek option is null. If you set a value in milliseconds, it will seek to that point in the video and then set the option value to null again. `looping` indicates whether the video should be looped or not. -`playbackSpeed` indicates the playback speed of the video (default is 1). +`volume` is a 0 to 1 value (at 0 the value is muted and 1 is the maxium volume). In the example below, every time we tap on the video, we set the video to 2 seconds. @@ -164,17 +94,13 @@ import { import { Pressable, useWindowDimensions } from "react-native"; import { useSharedValue } from "react-native-reanimated"; -interface VideoExampleProps { - localVideoFile: string; -} - -export const VideoExample = ({ localVideoFile }: VideoExampleProps) => { +export const VideoExample = () => { const seek = useSharedValue(null); // Set this value to true to pause the video const paused = useSharedValue(false); const { width, height } = useWindowDimensions(); const {currentFrame, currentTime} = useVideo( - require(localVideoFile), + "https://bit.ly/skia-video", { seek, paused, @@ -200,4 +126,66 @@ export const VideoExample = ({ localVideoFile }: VideoExampleProps) => { ); }; +``` + +## Rotated Video + +`rotation` can either be `0`, `90`, `180`, or `270`. +We provide a `fitbox` function that can help rotating and scaling the video. + +```tsx twoslash +import React from "react"; +import { + Canvas, + Image, + useVideo, + fitbox, + rect +} from "@shopify/react-native-skia"; +import { Pressable, useWindowDimensions } from "react-native"; +import { useSharedValue } from "react-native-reanimated"; + +export const VideoExample = () => { + const paused = useSharedValue(false); + const { width, height } = useWindowDimensions(); + const { currentFrame, rotation, size } = useVideo("https://bit.ly/skia-video"); + const src = rect(0, 0, size.width, size.height); + const dst = rect(0, 0, width, height) + const transform = fitbox("cover", src, dst, rotation); + return ( + + + + ); +}; +``` + +## Using Assets + +Below is an example where we use [expo-asset](https://docs.expo.dev/versions/latest/sdk/asset/) to load a video file from the bundle. + +```tsx twoslash +import { useVideo } from "@shopify/react-native-skia"; +import { useAssets } from "expo-asset"; + +// Example usage: +// const video = useVideoFromAsset(require("./BigBuckBunny.mp4")); +export const useVideoFromAsset = ( + mod: number, + options?: Parameters[1] +) => { + const [assets, error] = useAssets([mod]); + if (error) { + throw error; + } + return useVideo(assets ? assets[0].localUri : null, options); +}; ``` \ No newline at end of file diff --git a/package/src/external/reanimated/useVideo.ts b/package/src/external/reanimated/useVideo.ts index 4e46a6c3e9..629430bb9f 100644 --- a/package/src/external/reanimated/useVideo.ts +++ b/package/src/external/reanimated/useVideo.ts @@ -7,26 +7,16 @@ import { Platform } from "../../Platform"; import Rea from "./ReanimatedProxy"; -export type Animated = SharedValue | T; -// TODO: Move to useVideo.ts -export interface PlaybackOptions { - playbackSpeed: Animated; +type Animated = SharedValue | T; + +interface PlaybackOptions { looping: Animated; paused: Animated; seek: Animated; volume: Animated; } -type Materialized = { - [K in keyof T]: T[K] extends Animated ? U : T[K]; -}; - -export type MaterializedPlaybackOptions = Materialized< - Omit ->; - -// TODO: move -export const setFrame = ( +const setFrame = ( video: Video, currentFrame: SharedValue ) => { @@ -45,7 +35,6 @@ export const setFrame = ( }; const defaultOptions = { - playbackSpeed: 1, looping: true, paused: false, seek: null, @@ -76,9 +65,6 @@ export const useVideo = ( const looping = useOption(userOptions?.looping ?? defaultOptions.looping); const seek = useOption(userOptions?.seek ?? defaultOptions.seek); const volume = useOption(userOptions?.volume ?? defaultOptions.volume); - const playbackSpeed = useOption( - userOptions?.playbackSpeed ?? defaultOptions.playbackSpeed - ); const currentFrame = Rea.useSharedValue(null); const currentTime = Rea.useSharedValue(0); const lastTimestamp = Rea.useSharedValue(-1); @@ -86,6 +72,10 @@ export const useVideo = ( const framerate = useMemo(() => video?.framerate() ?? 0, [video]); const size = useMemo(() => video?.size() ?? { width: 0, height: 0 }, [video]); const rotation = useMemo(() => video?.rotation() ?? 0, [video]); + const frameDuration = 1000 / framerate; + const currentFrameDuration = Math.floor( + frameDuration + ); Rea.useAnimatedReaction( () => isPaused.value, (paused) => { @@ -127,10 +117,6 @@ export const useVideo = ( } const delta = currentTimestamp - lastTimestamp.value; - const frameDuration = 1000 / framerate; - const currentFrameDuration = Math.floor( - frameDuration / playbackSpeed.value - ); const isOver = currentTime.value + delta > duration; if (isOver && looping.value) { seek.value = 0; From 985ab72594ebbd36153089e9372463bfa2953a3b Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 14:53:46 +0200 Subject: [PATCH 29/32] :wrench: --- docs/docs/video.md | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/docs/video.md b/docs/docs/video.md index 34d4d9081e..a100382cb6 100644 --- a/docs/docs/video.md +++ b/docs/docs/video.md @@ -14,9 +14,7 @@ React Native Skia provides a way to load video frames as images, enabling rich m ## Example -Here is an example of how to use the video support in React Native Skia. -This example demonstrates how to load and display video frames within a canvas, applying a color matrix for visual effects. -Tapping the screen will pause and play the video. +Here is an example of how to use the video support in React Native Skia. This example demonstrates how to load and display video frames within a canvas, applying a color matrix for visual effects. Tapping the screen will pause and play the video. The video can be a remote (`http://...`) or local URL (`file://`), as well as a [video from the bundle](#using-assets). @@ -71,15 +69,19 @@ export const VideoExample = () => { ## Returned Values -The `useVideo` hook returns `currentFrame` which contains the current video frame, as well as `currentTime`, `rotation`, and `size`. +The `useVideo` hook returns `currentFrame`, which contains the current video frame, as well as `currentTime`, `rotation`, and `size`. ## Playback Options -You can seek a video via the `seek` playback option. By default, the seek option is null. If you set a value in milliseconds, it will seek to that point in the video and then set the option value to null again. +The following table describes the playback options available for the `useVideo` hook: -`looping` indicates whether the video should be looped or not. - -`volume` is a 0 to 1 value (at 0 the value is muted and 1 is the maxium volume). +| Option | Description | +|---------------|----------------------------------------------------------------------------------------------| +| `seek` | Allows seeking to a specific point in the video in milliseconds. Default is `null`. | +| `paused` | Indicates whether the video is paused. | +| `looping` | Indicates whether the video should loop. | +| `volume` | A value from 0 to 1 representing the volume level (0 is muted, 1 is the maximum volume). | +| `playbackSpeed` | Adjusts the speed of video playback. | In the example below, every time we tap on the video, we set the video to 2 seconds. @@ -130,8 +132,7 @@ export const VideoExample = () => { ## Rotated Video -`rotation` can either be `0`, `90`, `180`, or `270`. -We provide a `fitbox` function that can help rotating and scaling the video. +The `rotation` property can be `0`, `90`, `180`, or `270`. We provide a `fitbox` function that can help with rotating and scaling the video. ```tsx twoslash import React from "react"; From 5f0c526ccf5ca9e7f002edd1d6ea2bb4c73987af Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 14:57:31 +0200 Subject: [PATCH 30/32] :wrench: --- docs/docs/video.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/video.md b/docs/docs/video.md index a100382cb6..bca5ed4704 100644 --- a/docs/docs/video.md +++ b/docs/docs/video.md @@ -83,7 +83,7 @@ The following table describes the playback options available for the `useVideo` | `volume` | A value from 0 to 1 representing the volume level (0 is muted, 1 is the maximum volume). | | `playbackSpeed` | Adjusts the speed of video playback. | -In the example below, every time we tap on the video, we set the video to 2 seconds. +In the example below, every time we tap on the video, we set the video seek at 2 seconds. ```tsx twoslash import React from "react"; From 310a073b025e1ec6b668f3a26bdcf6fbac222a96 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 15:07:06 +0200 Subject: [PATCH 31/32] :wrench: --- docs/docs/video.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/docs/video.md b/docs/docs/video.md index 7848f4d859..8b28e0b336 100644 --- a/docs/docs/video.md +++ b/docs/docs/video.md @@ -11,10 +11,6 @@ React Native Skia provides a way to load video frames as images, enabling rich m - **Reanimated** version 3 or higher. - **Android:** API level 26 or higher. -<<<<<<< HEAD -======= -- **Video URL:** Must be a local path. We recommend using it in combination with [expo-asset](https://docs.expo.dev/versions/latest/sdk/asset/) to download the video. ->>>>>>> main ## Example @@ -193,4 +189,8 @@ export const useVideoFromAsset = ( } return useVideo(assets ? assets[0].localUri : null, options); }; -``` \ No newline at end of file +``` + +## Video Encoding + +To encode videos from Skia images, you can use ffmpeg or also look into [react-native-skia-video](https://github.com/AzzappApp/react-native-skia-video). \ No newline at end of file From 91d5ea5dd53bc2f5ebbff07d8b63b1972121fcd2 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Thu, 6 Jun 2024 16:02:16 +0200 Subject: [PATCH 32/32] :green_heart: --- docs/docs/video.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/docs/video.md b/docs/docs/video.md index 8b28e0b336..670ed29118 100644 --- a/docs/docs/video.md +++ b/docs/docs/video.md @@ -81,7 +81,6 @@ The following table describes the playback options available for the `useVideo` | `paused` | Indicates whether the video is paused. | | `looping` | Indicates whether the video should loop. | | `volume` | A value from 0 to 1 representing the volume level (0 is muted, 1 is the maximum volume). | -| `playbackSpeed` | Adjusts the speed of video playback. | In the example below, every time we tap on the video, we set the video seek at 2 seconds. @@ -106,8 +105,7 @@ export const VideoExample = () => { { seek, paused, - looping: true, - playbackSpeed: 1 + looping: true } ); return (