Skip to content

Commit bcab375

Browse files
authored
fix(📹): Better support for rotated videos (#2461)
1 parent 8bd1ffb commit bcab375

File tree

23 files changed

+296
-50
lines changed

23 files changed

+296
-50
lines changed

‎docs/docs/video.md

+47-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,53 @@ export const useVideoFromAsset = (
9696

9797
## Returned Values
9898

99-
The `useVideo` hook returns `currentFrame` which contains the current video frame, as well as `currentTime`, and `rotationInDegrees`.
99+
The `useVideo` hook returns `currentFrame` which contains the current video frame, as well as `currentTime`, `rotation`, and `size`.
100+
101+
## Rotated Video
102+
103+
`rotation` can either be `0`, `90`, `180`, or `270`.
104+
We provide a `fitbox` function that can help rotating and scaling the video.
105+
106+
```tsx twoslash
107+
import React from "react";
108+
import {
109+
Canvas,
110+
Image,
111+
useVideo,
112+
fitbox,
113+
rect
114+
} from "@shopify/react-native-skia";
115+
import { Pressable, useWindowDimensions } from "react-native";
116+
import { useSharedValue } from "react-native-reanimated";
117+
118+
interface VideoExampleProps {
119+
localVideoFile: string;
120+
}
121+
122+
// The URL needs to be a local path; we usually use expo-asset for that.
123+
export const VideoExample = ({ localVideoFile }: VideoExampleProps) => {
124+
const paused = useSharedValue(false);
125+
const { width, height } = useWindowDimensions();
126+
const { currentFrame, rotation, size } = useVideo(require(localVideoFile));
127+
const src = rect(0, 0, size.width, size.height);
128+
const dst = rect(0, 0, width, height)
129+
const transform = fitbox("cover", src, dst, rotation);
130+
return (
131+
<Canvas style={{ flex: 1 }}>
132+
<Image
133+
image={currentFrame}
134+
x={0}
135+
y={0}
136+
width={width}
137+
height={height}
138+
fit="none"
139+
transform={transform}
140+
/>
141+
</Canvas>
142+
);
143+
};
144+
```
145+
100146

101147
## Playback Options
102148

‎package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp

+26
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#pragma clang diagnostic ignored "-Wdocumentation"
1010

1111
#include "include/core/SkImage.h"
12+
#include "include/core/SkSize.h"
1213

1314
#pragma clang diagnostic pop
1415

@@ -102,4 +103,29 @@ float RNSkAndroidVideo::getRotationInDegrees() {
102103
return static_cast<float>(rotation);
103104
}
104105

106+
SkISize RNSkAndroidVideo::getSize() {
107+
JNIEnv *env = facebook::jni::Environment::current();
108+
jclass cls = env->GetObjectClass(_jniVideo.get());
109+
jmethodID mid =
110+
env->GetMethodID(cls, "getSize", "()Landroid/graphics/Point;");
111+
if (!mid) {
112+
RNSkLogger::logToConsole("getSize method not found");
113+
return SkISize::Make(0, 0);
114+
}
115+
jobject jPoint = env->CallObjectMethod(_jniVideo.get(), mid);
116+
jclass pointCls = env->GetObjectClass(jPoint);
117+
118+
jfieldID xFid = env->GetFieldID(pointCls, "x", "I");
119+
jfieldID yFid = env->GetFieldID(pointCls, "y", "I");
120+
if (!xFid || !yFid) {
121+
RNSkLogger::logToConsole("Point class fields not found");
122+
return SkISize::Make(0, 0);
123+
}
124+
125+
jint width = env->GetIntField(jPoint, xFid);
126+
jint height = env->GetIntField(jPoint, yFid);
127+
128+
return SkISize::Make(width, height);
129+
}
130+
105131
} // namespace RNSkia

‎package/android/cpp/rnskia-android/RNSkAndroidVideo.h

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class RNSkAndroidVideo : public RNSkVideo {
3232
double framerate() override;
3333
void seek(double timestamp) override;
3434
float getRotationInDegrees() override;
35+
SkISize getSize() override;
3536
};
3637

3738
} // namespace RNSkia

‎package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import android.net.Uri;
1212
import android.os.Build;
1313
import android.view.Surface;
14+
import android.graphics.Point;
1415

1516
import androidx.annotation.RequiresApi;
1617

@@ -30,6 +31,8 @@ public class RNSkVideo {
3031
private double durationMs;
3132
private double frameRate;
3233
private int rotationDegrees = 0;
34+
private int width = 0;
35+
private int height = 0;
3336

3437
RNSkVideo(Context context, String localUri) {
3538
this.uri = Uri.parse(localUri);
@@ -57,8 +60,8 @@ private void initializeReader() {
5760
if (format.containsKey(MediaFormat.KEY_ROTATION)) {
5861
rotationDegrees = format.getInteger(MediaFormat.KEY_ROTATION);
5962
}
60-
int width = format.getInteger(MediaFormat.KEY_WIDTH);
61-
int height = format.getInteger(MediaFormat.KEY_HEIGHT);
63+
width = format.getInteger(MediaFormat.KEY_WIDTH);
64+
height = format.getInteger(MediaFormat.KEY_HEIGHT);
6265
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
6366
imageReader = ImageReader.newInstance(
6467
width,
@@ -125,6 +128,11 @@ public void seek(long timestamp) {
125128
}
126129
}
127130

131+
@DoNotStrip
132+
public Point getSize() {
133+
return new Point(width, height);
134+
}
135+
128136
private int selectVideoTrack(MediaExtractor extractor) {
129137
int numTracks = extractor.getTrackCount();
130138
for (int i = 0; i < numTracks; i++) {

‎package/cpp/api/JsiVideo.h

+12-2
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,27 @@ class JsiVideo : public JsiSkWrappingSharedPtrHostObject<RNSkVideo> {
5353
return jsi::Value::undefined();
5454
}
5555

56-
JSI_HOST_FUNCTION(getRotationInDegrees) {
56+
JSI_HOST_FUNCTION(rotation) {
5757
auto context = getContext();
5858
auto rot = getObject()->getRotationInDegrees();
5959
return jsi::Value(static_cast<double>(rot));
6060
}
6161

62+
JSI_HOST_FUNCTION(size) {
63+
auto context = getContext();
64+
auto size = getObject()->getSize();
65+
auto result = jsi::Object(runtime);
66+
result.setProperty(runtime, "width", static_cast<double>(size.width()));
67+
result.setProperty(runtime, "height", static_cast<double>(size.height()));
68+
return result;
69+
}
70+
6271
JSI_EXPORT_FUNCTIONS(JSI_EXPORT_FUNC(JsiVideo, nextImage),
6372
JSI_EXPORT_FUNC(JsiVideo, duration),
6473
JSI_EXPORT_FUNC(JsiVideo, framerate),
6574
JSI_EXPORT_FUNC(JsiVideo, seek),
66-
JSI_EXPORT_FUNC(JsiVideo, getRotationInDegrees),
75+
JSI_EXPORT_FUNC(JsiVideo, rotation),
76+
JSI_EXPORT_FUNC(JsiVideo, size),
6777
JSI_EXPORT_FUNC(JsiVideo, dispose))
6878

6979
JsiVideo(std::shared_ptr<RNSkPlatformContext> context,

‎package/cpp/rnskia/RNSkVideo.h

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class RNSkVideo {
1919
virtual double framerate() = 0;
2020
virtual void seek(double timestamp) = 0;
2121
virtual float getRotationInDegrees() = 0;
22+
virtual SkISize getSize() = 0;
2223
};
2324

2425
} // namespace RNSkia

‎package/ios/RNSkia-iOS/RNSkiOSVideo.h

+4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#pragma clang diagnostic ignored "-Wdocumentation"
1010

1111
#include "include/core/SkImage.h"
12+
#include "include/core/SkSize.h"
1213

1314
#pragma clang diagnostic pop
1415

@@ -25,6 +26,8 @@ class RNSkiOSVideo : public RNSkVideo {
2526
RNSkPlatformContext *_context;
2627
double _duration = 0;
2728
double _framerate = 0;
29+
float _videoWidth = 0;
30+
float _videoHeight = 0;
2831
void setupReader(CMTimeRange timeRange);
2932
NSDictionary *getOutputSettings();
3033
CGAffineTransform _preferredTransform;
@@ -37,6 +40,7 @@ class RNSkiOSVideo : public RNSkVideo {
3740
double framerate() override;
3841
void seek(double timestamp) override;
3942
float getRotationInDegrees() override;
43+
SkISize getSize() override;
4044
};
4145

4246
} // namespace RNSkia

‎package/ios/RNSkia-iOS/RNSkiOSVideo.mm

+11-9
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
[[asset tracksWithMediaType:AVMediaTypeVideo] firstObject];
4646
_framerate = videoTrack.nominalFrameRate;
4747
_preferredTransform = videoTrack.preferredTransform;
48-
48+
CGSize videoSize = videoTrack.naturalSize;
49+
_videoWidth = videoSize.width;
50+
_videoHeight = videoSize.height;
4951
NSDictionary *outputSettings = getOutputSettings();
5052
AVAssetReaderTrackOutput *trackOutput =
5153
[[AVAssetReaderTrackOutput alloc] initWithTrack:videoTrack
@@ -104,19 +106,15 @@
104106
// Determine the rotation angle in radians
105107
if (transform.a == 0 && transform.b == 1 && transform.c == -1 &&
106108
transform.d == 0) {
107-
rotationAngle = M_PI_2; // 90 degrees
109+
rotationAngle = 90;
108110
} else if (transform.a == 0 && transform.b == -1 && transform.c == 1 &&
109111
transform.d == 0) {
110-
rotationAngle = -M_PI_2; // -90 degrees
112+
rotationAngle = 270;
111113
} else if (transform.a == -1 && transform.b == 0 && transform.c == 0 &&
112114
transform.d == -1) {
113-
rotationAngle = M_PI; // 180 degrees
114-
} else if (transform.a == 1 && transform.b == 0 && transform.c == 0 &&
115-
transform.d == 1) {
116-
rotationAngle = 0.0; // 0 degrees
115+
rotationAngle = 180;
117116
}
118-
// Convert the rotation angle from radians to degrees
119-
return rotationAngle * 180 / M_PI;
117+
return rotationAngle;
120118
}
121119

122120
void RNSkiOSVideo::seek(double timeInMilliseconds) {
@@ -136,4 +134,8 @@
136134

137135
double RNSkiOSVideo::framerate() { return _framerate; }
138136

137+
SkISize RNSkiOSVideo::getSize() {
138+
return SkISize::Make(_videoWidth, _videoHeight);
139+
}
140+
139141
} // namespace RNSkia
Loading
Loading
Loading
Loading

‎package/src/dom/nodes/datatypes/Fitting.ts

+28-21
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ export interface Size {
77
height: number;
88
}
99

10-
export const size = (width = 0, height = 0) => ({ width, height });
10+
export const size = (width = 0, height = 0) => {
11+
"worklet";
12+
return { width, height };
13+
};
1114

1215
export const rect2rect = (
1316
src: SkRect,
@@ -18,37 +21,19 @@ export const rect2rect = (
1821
{ scaleX: number },
1922
{ scaleY: number }
2023
] => {
24+
"worklet";
2125
const scaleX = dst.width / src.width;
2226
const scaleY = dst.height / src.height;
2327
const translateX = dst.x - src.x * scaleX;
2428
const translateY = dst.y - src.y * scaleY;
2529
return [{ translateX }, { translateY }, { scaleX }, { scaleY }];
2630
};
2731

28-
export const fitRects = (
29-
fit: Fit,
30-
rect: SkRect,
31-
{ x, y, width, height }: SkRect
32-
) => {
33-
const sizes = applyBoxFit(
34-
fit,
35-
{ width: rect.width, height: rect.height },
36-
{ width, height }
37-
);
38-
const src = inscribe(sizes.src, rect);
39-
const dst = inscribe(sizes.dst, {
40-
x,
41-
y,
42-
width,
43-
height,
44-
});
45-
return { src, dst };
46-
};
47-
4832
const inscribe = (
4933
{ width, height }: Size,
5034
rect: { x: number; y: number; width: number; height: number }
5135
) => {
36+
"worklet";
5237
const halfWidthDelta = (rect.width - width) / 2.0;
5338
const halfHeightDelta = (rect.height - height) / 2.0;
5439
return {
@@ -60,6 +45,7 @@ const inscribe = (
6045
};
6146

6247
const applyBoxFit = (fit: Fit, input: Size, output: Size) => {
48+
"worklet";
6349
let src = size(),
6450
dst = size();
6551
if (
@@ -122,3 +108,24 @@ const applyBoxFit = (fit: Fit, input: Size, output: Size) => {
122108
}
123109
return { src, dst };
124110
};
111+
112+
export const fitRects = (
113+
fit: Fit,
114+
rect: SkRect,
115+
{ x, y, width, height }: SkRect
116+
) => {
117+
"worklet";
118+
const sizes = applyBoxFit(
119+
fit,
120+
{ width: rect.width, height: rect.height },
121+
{ width, height }
122+
);
123+
const src = inscribe(sizes.src, rect);
124+
const dst = inscribe(sizes.dst, {
125+
x,
126+
y,
127+
width,
128+
height,
129+
});
130+
return { src, dst };
131+
};

‎package/src/external/reanimated/useVideo.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,8 @@ export const useVideo = (
4949
const lastTimestamp = Rea.useSharedValue(-1);
5050
const duration = useMemo(() => video?.duration() ?? 0, [video]);
5151
const framerate = useMemo(() => video?.framerate() ?? 0, [video]);
52-
const rotationInDegrees = useMemo(
53-
() => video?.getRotationInDegrees() ?? 0,
54-
[video]
55-
);
52+
const size = useMemo(() => video?.size() ?? { width: 0, height: 0 }, [video]);
53+
const rotation = useMemo(() => video?.rotation() ?? 0, [video]);
5654
Rea.useFrameCallback((frameInfo: FrameInfo) => {
5755
processVideoState(
5856
video,
@@ -78,5 +76,12 @@ export const useVideo = (
7876
};
7977
}, [video]);
8078

81-
return { currentFrame, currentTime, duration, framerate, rotationInDegrees };
79+
return {
80+
currentFrame,
81+
currentTime,
82+
duration,
83+
framerate,
84+
rotation,
85+
size,
86+
};
8287
};

0 commit comments

Comments
 (0)