@shopify/react-native-skia 2.4.18 → 2.4.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp +34 -0
- package/android/cpp/rnskia-android/RNSkAndroidVideo.h +3 -0
- package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java +72 -18
- package/apple/RNSkAppleVideo.h +30 -3
- package/apple/RNSkAppleVideo.mm +172 -17
- package/cpp/api/JsiSkApi.h +15 -13
- package/cpp/api/JsiSkHostObjects.h +57 -3
- package/cpp/api/JsiSkImage.h +19 -5
- package/cpp/api/JsiSkPicture.h +19 -5
- package/cpp/api/JsiSkSurface.h +19 -5
- package/cpp/api/JsiVideo.h +15 -2
- package/cpp/api/recorder/Convertor.h +4 -2
- package/cpp/jsi2/EnumMapper.h +49 -34
- package/cpp/jsi2/JSIConverter.h +149 -99
- package/cpp/jsi2/NativeObject.h +23 -25
- package/cpp/jsi2/Promise.cpp +10 -6
- package/cpp/jsi2/Promise.h +9 -7
- package/cpp/rnskia/RNDawnContext.h +3 -8
- package/cpp/rnskia/RNSkManager.cpp +13 -7
- package/cpp/rnskia/RNSkVideo.h +3 -0
- package/cpp/rnwgpu/api/GPUAdapter.cpp +31 -32
- package/cpp/rnwgpu/api/GPUAdapter.h +1 -1
- package/cpp/rnwgpu/api/GPUBuffer.cpp +8 -8
- package/cpp/rnwgpu/api/GPUCommandEncoder.h +4 -4
- package/cpp/rnwgpu/api/GPUDevice.h +12 -12
- package/cpp/rnwgpu/api/GPUQueue.cpp +45 -44
- package/cpp/rnwgpu/api/GPUQueue.h +1 -1
- package/cpp/rnwgpu/api/GPURenderBundleEncoder.h +1 -1
- package/cpp/rnwgpu/api/GPURenderPassEncoder.h +1 -1
- package/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h +1 -1
- package/cpp/rnwgpu/api/descriptors/GPUComputePipelineDescriptor.h +1 -1
- package/cpp/rnwgpu/api/descriptors/GPUImageCopyExternalImage.h +7 -6
- package/cpp/rnwgpu/api/descriptors/GPURenderPassDescriptor.h +1 -1
- package/cpp/rnwgpu/api/descriptors/GPURenderPipelineDescriptor.h +1 -1
- package/cpp/rnwgpu/api/descriptors/GPUVertexState.h +1 -1
- package/cpp/rnwgpu/async/AsyncRunner.cpp +2 -1
- package/cpp/rnwgpu/async/AsyncRunner.h +2 -1
- package/lib/commonjs/external/reanimated/useVideo.js +30 -31
- package/lib/commonjs/external/reanimated/useVideo.js.map +1 -1
- package/lib/commonjs/renderer/Offscreen.js +1 -0
- package/lib/commonjs/renderer/Offscreen.js.map +1 -1
- package/lib/commonjs/skia/types/Video/Video.d.ts +3 -0
- package/lib/commonjs/skia/types/Video/Video.js.map +1 -1
- package/lib/commonjs/skia/web/JsiVideo.d.ts +3 -0
- package/lib/commonjs/skia/web/JsiVideo.js +9 -0
- package/lib/commonjs/skia/web/JsiVideo.js.map +1 -1
- package/lib/commonjs/sksg/Container.web.js +1 -0
- package/lib/commonjs/sksg/Container.web.js.map +1 -1
- package/lib/commonjs/sksg/HostConfig.js +4 -1
- package/lib/commonjs/sksg/HostConfig.js.map +1 -1
- package/lib/commonjs/sksg/Reconciler.js +6 -6
- package/lib/commonjs/sksg/Reconciler.js.map +1 -1
- package/lib/commonjs/sksg/StaticContainer.js +1 -0
- package/lib/commonjs/sksg/StaticContainer.js.map +1 -1
- package/lib/module/external/reanimated/useVideo.js +30 -31
- package/lib/module/external/reanimated/useVideo.js.map +1 -1
- package/lib/module/renderer/Offscreen.js +1 -0
- package/lib/module/renderer/Offscreen.js.map +1 -1
- package/lib/module/skia/types/Video/Video.d.ts +3 -0
- package/lib/module/skia/types/Video/Video.js.map +1 -1
- package/lib/module/skia/web/JsiVideo.d.ts +3 -0
- package/lib/module/skia/web/JsiVideo.js +9 -0
- package/lib/module/skia/web/JsiVideo.js.map +1 -1
- package/lib/module/sksg/Container.web.js +1 -0
- package/lib/module/sksg/Container.web.js.map +1 -1
- package/lib/module/sksg/HostConfig.js +4 -1
- package/lib/module/sksg/HostConfig.js.map +1 -1
- package/lib/module/sksg/Reconciler.js +6 -6
- package/lib/module/sksg/Reconciler.js.map +1 -1
- package/lib/module/sksg/StaticContainer.js +1 -0
- package/lib/module/sksg/StaticContainer.js.map +1 -1
- package/lib/typescript/lib/commonjs/skia/web/JsiVideo.d.ts +3 -0
- package/lib/typescript/lib/commonjs/sksg/HostConfig.d.ts +2 -0
- package/lib/typescript/lib/module/skia/web/JsiVideo.d.ts +3 -0
- package/lib/typescript/lib/module/sksg/HostConfig.d.ts +2 -0
- package/lib/typescript/src/skia/types/Video/Video.d.ts +3 -0
- package/lib/typescript/src/skia/web/JsiVideo.d.ts +3 -0
- package/package.json +1 -1
- package/src/external/reanimated/useVideo.ts +32 -32
- package/src/renderer/Offscreen.tsx +1 -0
- package/src/skia/types/Video/Video.ts +3 -0
- package/src/skia/web/JsiVideo.ts +12 -0
- package/src/sksg/Container.web.ts +1 -0
- package/src/sksg/HostConfig.ts +4 -0
- package/src/sksg/Reconciler.ts +5 -6
- package/src/sksg/StaticContainer.ts +1 -0
|
@@ -171,4 +171,38 @@ void RNSkAndroidVideo::setVolume(float volume) {
|
|
|
171
171
|
}
|
|
172
172
|
env->CallVoidMethod(_jniVideo.get(), mid, volume);
|
|
173
173
|
}
|
|
174
|
+
|
|
175
|
+
double RNSkAndroidVideo::currentTime() {
|
|
176
|
+
JNIEnv *env = facebook::jni::Environment::current();
|
|
177
|
+
jclass cls = env->GetObjectClass(_jniVideo.get());
|
|
178
|
+
jmethodID mid = env->GetMethodID(cls, "getCurrentTime", "()D");
|
|
179
|
+
if (!mid) {
|
|
180
|
+
RNSkLogger::logToConsole("getCurrentTime method not found");
|
|
181
|
+
return 0.0;
|
|
182
|
+
}
|
|
183
|
+
return env->CallDoubleMethod(_jniVideo.get(), mid);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
void RNSkAndroidVideo::setLooping(bool looping) {
|
|
187
|
+
JNIEnv *env = facebook::jni::Environment::current();
|
|
188
|
+
jclass cls = env->GetObjectClass(_jniVideo.get());
|
|
189
|
+
jmethodID mid = env->GetMethodID(cls, "setLooping", "(Z)V");
|
|
190
|
+
if (!mid) {
|
|
191
|
+
RNSkLogger::logToConsole("setLooping method not found");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
env->CallVoidMethod(_jniVideo.get(), mid, static_cast<jboolean>(looping));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
bool RNSkAndroidVideo::isPlaying() {
|
|
198
|
+
JNIEnv *env = facebook::jni::Environment::current();
|
|
199
|
+
jclass cls = env->GetObjectClass(_jniVideo.get());
|
|
200
|
+
jmethodID mid = env->GetMethodID(cls, "getIsPlaying", "()Z");
|
|
201
|
+
if (!mid) {
|
|
202
|
+
RNSkLogger::logToConsole("getIsPlaying method not found");
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return env->CallBooleanMethod(_jniVideo.get(), mid);
|
|
206
|
+
}
|
|
207
|
+
|
|
174
208
|
} // namespace RNSkia
|
|
@@ -30,12 +30,15 @@ public:
|
|
|
30
30
|
sk_sp<SkImage> nextImage(double *timeStamp = nullptr) override;
|
|
31
31
|
double duration() override;
|
|
32
32
|
double framerate() override;
|
|
33
|
+
double currentTime() override;
|
|
33
34
|
void seek(double timestamp) override;
|
|
34
35
|
float getRotationInDegrees() override;
|
|
35
36
|
SkISize getSize() override;
|
|
36
37
|
void play() override;
|
|
37
38
|
void pause() override;
|
|
38
39
|
void setVolume(float volume) override;
|
|
40
|
+
void setLooping(bool looping) override;
|
|
41
|
+
bool isPlaying() override;
|
|
39
42
|
};
|
|
40
43
|
|
|
41
44
|
} // namespace RNSkia
|
|
@@ -41,6 +41,12 @@ public class RNSkVideo {
|
|
|
41
41
|
private int height = 0;
|
|
42
42
|
|
|
43
43
|
private boolean isPlaying = false;
|
|
44
|
+
private boolean isLooping = false;
|
|
45
|
+
private boolean isPrepared = false;
|
|
46
|
+
private boolean playWhenReady = false;
|
|
47
|
+
private HardwareBuffer lastBuffer = null;
|
|
48
|
+
private boolean pendingSeek = false;
|
|
49
|
+
private double lastFrameTimeMs = 0;
|
|
44
50
|
|
|
45
51
|
RNSkVideo(Context context, String localUri) {
|
|
46
52
|
this.uri = Uri.parse(localUri);
|
|
@@ -65,8 +71,21 @@ public class RNSkVideo {
|
|
|
65
71
|
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
|
|
66
72
|
mediaPlayer.setOnPreparedListener(mp -> {
|
|
67
73
|
durationMs = mp.getDuration();
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
isPrepared = true;
|
|
75
|
+
// Apply looping setting
|
|
76
|
+
mp.setLooping(isLooping);
|
|
77
|
+
// Start playing if play() was called before preparation completed
|
|
78
|
+
if (playWhenReady) {
|
|
79
|
+
mp.start();
|
|
80
|
+
isPlaying = true;
|
|
81
|
+
playWhenReady = false;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// Note: When setLooping(true) is used, this listener won't fire as
|
|
85
|
+
// MediaPlayer handles looping automatically. This only handles the
|
|
86
|
+
// non-looping case to update playback state.
|
|
87
|
+
mediaPlayer.setOnCompletionListener(mp -> {
|
|
88
|
+
isPlaying = false;
|
|
70
89
|
});
|
|
71
90
|
mediaPlayer.prepareAsync();
|
|
72
91
|
|
|
@@ -125,6 +144,12 @@ public class RNSkVideo {
|
|
|
125
144
|
|
|
126
145
|
@DoNotStrip
|
|
127
146
|
public HardwareBuffer nextImage() {
|
|
147
|
+
// If paused and not seeking, return cached buffer
|
|
148
|
+
if (!isPlaying && !pendingSeek && lastBuffer != null) {
|
|
149
|
+
return lastBuffer;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Decode new frame if needed
|
|
128
153
|
if (!decoderOutputAvailable()) {
|
|
129
154
|
decodeFrame();
|
|
130
155
|
}
|
|
@@ -133,37 +158,37 @@ public class RNSkVideo {
|
|
|
133
158
|
if (image != null) {
|
|
134
159
|
HardwareBuffer hardwareBuffer = image.getHardwareBuffer();
|
|
135
160
|
image.close(); // Make sure to close the Image to free up the buffer
|
|
161
|
+
// Cache the buffer
|
|
162
|
+
lastBuffer = hardwareBuffer;
|
|
163
|
+
// Clear pending seek flag after getting frame
|
|
164
|
+
if (pendingSeek) {
|
|
165
|
+
pendingSeek = false;
|
|
166
|
+
}
|
|
136
167
|
return hardwareBuffer;
|
|
137
168
|
}
|
|
138
|
-
|
|
169
|
+
// Return cached buffer if no new frame available
|
|
170
|
+
return lastBuffer;
|
|
139
171
|
}
|
|
140
172
|
|
|
141
173
|
@DoNotStrip
|
|
142
174
|
public void seek(double timestamp) {
|
|
143
|
-
// Log the values for debugging
|
|
144
|
-
|
|
145
175
|
long timestampUs = (long)(timestamp * 1000); // Convert milliseconds to microseconds
|
|
146
176
|
|
|
147
177
|
extractor.seekTo(timestampUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
|
|
148
|
-
if (mediaPlayer != null) {
|
|
178
|
+
if (mediaPlayer != null && isPrepared) {
|
|
149
179
|
int timestampMs = (int) timestamp; // Convert to milliseconds
|
|
150
180
|
mediaPlayer.seekTo(timestampMs, MediaPlayer.SEEK_CLOSEST);
|
|
151
181
|
}
|
|
152
182
|
|
|
153
|
-
// Flush the codec to reset internal state and
|
|
183
|
+
// Flush the codec to reset internal state and prepare for new position
|
|
154
184
|
if (decoder != null) {
|
|
155
185
|
decoder.flush();
|
|
156
|
-
|
|
157
|
-
// Decode frames until reaching the exact timestamp
|
|
158
|
-
boolean isSeeking = true;
|
|
159
|
-
while (isSeeking) {
|
|
160
|
-
decodeFrame();
|
|
161
|
-
long currentTimestampUs = extractor.getSampleTime();
|
|
162
|
-
if (currentTimestampUs >= timestampUs) {
|
|
163
|
-
isSeeking = false;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
186
|
}
|
|
187
|
+
|
|
188
|
+
// Signal that we need to decode a new frame (for seek while paused)
|
|
189
|
+
pendingSeek = true;
|
|
190
|
+
// Decode a frame immediately so it's ready
|
|
191
|
+
decodeFrame();
|
|
167
192
|
}
|
|
168
193
|
|
|
169
194
|
@DoNotStrip
|
|
@@ -216,6 +241,8 @@ public class RNSkVideo {
|
|
|
216
241
|
|
|
217
242
|
int outputBufferId = decoder.dequeueOutputBuffer(info, timeoutUs);
|
|
218
243
|
if (outputBufferId >= 0) {
|
|
244
|
+
// Store the frame's presentation timestamp for accurate time reporting
|
|
245
|
+
lastFrameTimeMs = info.presentationTimeUs / 1000.0;
|
|
219
246
|
// If we have a valid buffer, release it to make it available to the ImageReader's surface
|
|
220
247
|
decoder.releaseOutputBuffer(outputBufferId, true);
|
|
221
248
|
|
|
@@ -227,14 +254,21 @@ public class RNSkVideo {
|
|
|
227
254
|
|
|
228
255
|
@DoNotStrip
|
|
229
256
|
public void play() {
|
|
230
|
-
if (mediaPlayer
|
|
257
|
+
if (mediaPlayer == null || isPlaying) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (isPrepared) {
|
|
231
261
|
mediaPlayer.start();
|
|
232
262
|
isPlaying = true;
|
|
263
|
+
} else {
|
|
264
|
+
// Queue the play request for when preparation completes
|
|
265
|
+
playWhenReady = true;
|
|
233
266
|
}
|
|
234
267
|
}
|
|
235
268
|
|
|
236
269
|
@DoNotStrip
|
|
237
270
|
public void pause() {
|
|
271
|
+
playWhenReady = false;
|
|
238
272
|
if (mediaPlayer != null && isPlaying) {
|
|
239
273
|
mediaPlayer.pause();
|
|
240
274
|
isPlaying = false;
|
|
@@ -248,7 +282,27 @@ public class RNSkVideo {
|
|
|
248
282
|
}
|
|
249
283
|
}
|
|
250
284
|
|
|
285
|
+
@DoNotStrip
|
|
286
|
+
public double getCurrentTime() {
|
|
287
|
+
// Return the timestamp of the last decoded frame for accurate synchronization
|
|
288
|
+
return lastFrameTimeMs;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@DoNotStrip
|
|
292
|
+
public void setLooping(boolean looping) {
|
|
293
|
+
isLooping = looping;
|
|
294
|
+
if (mediaPlayer != null && isPrepared) {
|
|
295
|
+
mediaPlayer.setLooping(looping);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@DoNotStrip
|
|
300
|
+
public boolean getIsPlaying() {
|
|
301
|
+
return isPlaying;
|
|
302
|
+
}
|
|
303
|
+
|
|
251
304
|
public void release() {
|
|
305
|
+
lastBuffer = null;
|
|
252
306
|
if (mediaPlayer != null) {
|
|
253
307
|
mediaPlayer.release();
|
|
254
308
|
mediaPlayer = null;
|
package/apple/RNSkAppleVideo.h
CHANGED
|
@@ -24,6 +24,13 @@ private:
|
|
|
24
24
|
AVPlayer *_player = nullptr;
|
|
25
25
|
AVPlayerItem *_playerItem = nullptr;
|
|
26
26
|
AVPlayerItemVideoOutput *_videoOutput = nullptr;
|
|
27
|
+
#if !TARGET_OS_OSX
|
|
28
|
+
CADisplayLink *_displayLink = nullptr;
|
|
29
|
+
id _displayLinkTarget = nullptr;
|
|
30
|
+
#else
|
|
31
|
+
CVDisplayLinkRef _displayLink = nullptr;
|
|
32
|
+
bool _displayLinkRunning = false;
|
|
33
|
+
#endif
|
|
27
34
|
RNSkPlatformContext *_context;
|
|
28
35
|
double _duration = 0;
|
|
29
36
|
double _framerate = 0;
|
|
@@ -31,21 +38,41 @@ private:
|
|
|
31
38
|
float _videoHeight = 0;
|
|
32
39
|
CGAffineTransform _preferredTransform;
|
|
33
40
|
bool _isPlaying = false;
|
|
41
|
+
bool _isLooping = false;
|
|
42
|
+
bool _waitingForFrame = false;
|
|
43
|
+
id _endObserver = nullptr;
|
|
44
|
+
sk_sp<SkImage> _lastImage = nullptr;
|
|
45
|
+
double _lastFrameTimeMs = 0;
|
|
34
46
|
void setupPlayer();
|
|
47
|
+
void setupDisplayLink();
|
|
35
48
|
NSDictionary *getOutputSettings();
|
|
49
|
+
#if TARGET_OS_OSX
|
|
50
|
+
void startDisplayLink();
|
|
51
|
+
void stopDisplayLink();
|
|
52
|
+
static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink,
|
|
53
|
+
const CVTimeStamp *now,
|
|
54
|
+
const CVTimeStamp *outputTime,
|
|
55
|
+
CVOptionFlags flagsIn,
|
|
56
|
+
CVOptionFlags *flagsOut, void *context);
|
|
57
|
+
#endif
|
|
36
58
|
|
|
37
59
|
public:
|
|
60
|
+
void onDisplayLink();
|
|
61
|
+
void expectFrame();
|
|
38
62
|
RNSkAppleVideo(std::string url, RNSkPlatformContext *context);
|
|
39
63
|
~RNSkAppleVideo();
|
|
40
64
|
sk_sp<SkImage> nextImage(double *timeStamp = nullptr) override;
|
|
41
65
|
double duration() override;
|
|
42
66
|
double framerate() override;
|
|
67
|
+
double currentTime() override;
|
|
43
68
|
void seek(double timestamp) override;
|
|
44
|
-
void play();
|
|
45
|
-
void pause();
|
|
69
|
+
void play() override;
|
|
70
|
+
void pause() override;
|
|
46
71
|
float getRotationInDegrees() override;
|
|
47
72
|
SkISize getSize() override;
|
|
48
|
-
void setVolume(float volume);
|
|
73
|
+
void setVolume(float volume) override;
|
|
74
|
+
void setLooping(bool looping) override;
|
|
75
|
+
bool isPlaying() override;
|
|
49
76
|
};
|
|
50
77
|
|
|
51
78
|
} // namespace RNSkia
|
package/apple/RNSkAppleVideo.mm
CHANGED
|
@@ -12,14 +12,51 @@
|
|
|
12
12
|
#include <AVFoundation/AVFoundation.h>
|
|
13
13
|
#include <CoreVideo/CoreVideo.h>
|
|
14
14
|
|
|
15
|
+
#if !TARGET_OS_OSX
|
|
16
|
+
// Helper class to bridge CADisplayLink callback to C++
|
|
17
|
+
@interface RNSkDisplayLinkTarget : NSObject
|
|
18
|
+
@property(nonatomic, assign) RNSkia::RNSkAppleVideo *video;
|
|
19
|
+
- (void)displayLinkFired:(CADisplayLink *)sender;
|
|
20
|
+
@end
|
|
21
|
+
|
|
22
|
+
@implementation RNSkDisplayLinkTarget
|
|
23
|
+
- (void)displayLinkFired:(CADisplayLink *)sender {
|
|
24
|
+
if (_video) {
|
|
25
|
+
_video->onDisplayLink();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
@end
|
|
29
|
+
#endif
|
|
30
|
+
|
|
15
31
|
namespace RNSkia {
|
|
16
32
|
|
|
17
33
|
RNSkAppleVideo::RNSkAppleVideo(std::string url, RNSkPlatformContext *context)
|
|
18
34
|
: _url(std::move(url)), _context(context) {
|
|
19
35
|
setupPlayer();
|
|
36
|
+
setupDisplayLink();
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
RNSkAppleVideo::~RNSkAppleVideo() {
|
|
40
|
+
#if !TARGET_OS_OSX
|
|
41
|
+
if (_displayLink) {
|
|
42
|
+
[_displayLink invalidate];
|
|
43
|
+
_displayLink = nullptr;
|
|
44
|
+
}
|
|
45
|
+
if (_displayLinkTarget) {
|
|
46
|
+
[_displayLinkTarget setVideo:nullptr];
|
|
47
|
+
_displayLinkTarget = nullptr;
|
|
48
|
+
}
|
|
49
|
+
#else
|
|
50
|
+
if (_displayLink) {
|
|
51
|
+
CVDisplayLinkStop(_displayLink);
|
|
52
|
+
CVDisplayLinkRelease(_displayLink);
|
|
53
|
+
_displayLink = nullptr;
|
|
54
|
+
}
|
|
55
|
+
#endif
|
|
56
|
+
if (_endObserver) {
|
|
57
|
+
[[NSNotificationCenter defaultCenter] removeObserver:_endObserver];
|
|
58
|
+
_endObserver = nullptr;
|
|
59
|
+
}
|
|
23
60
|
if (_player) {
|
|
24
61
|
[_player pause];
|
|
25
62
|
}
|
|
@@ -30,6 +67,7 @@ void RNSkAppleVideo::setupPlayer() {
|
|
|
30
67
|
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:videoURL];
|
|
31
68
|
_player = [AVPlayer playerWithPlayerItem:playerItem];
|
|
32
69
|
_playerItem = playerItem;
|
|
70
|
+
_player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
|
|
33
71
|
|
|
34
72
|
NSDictionary *outputSettings = getOutputSettings();
|
|
35
73
|
_videoOutput =
|
|
@@ -50,27 +88,118 @@ void RNSkAppleVideo::setupPlayer() {
|
|
|
50
88
|
_videoWidth = videoSize.width;
|
|
51
89
|
_videoHeight = videoSize.height;
|
|
52
90
|
}
|
|
53
|
-
|
|
91
|
+
|
|
92
|
+
// Set up end-of-playback observer
|
|
93
|
+
__weak AVPlayer *weakPlayer = _player;
|
|
94
|
+
_endObserver = [[NSNotificationCenter defaultCenter]
|
|
95
|
+
addObserverForName:AVPlayerItemDidPlayToEndTimeNotification
|
|
96
|
+
object:playerItem
|
|
97
|
+
queue:[NSOperationQueue mainQueue]
|
|
98
|
+
usingBlock:^(NSNotification *note) {
|
|
99
|
+
if (_isLooping) {
|
|
100
|
+
[weakPlayer seekToTime:kCMTimeZero
|
|
101
|
+
toleranceBefore:kCMTimeZero
|
|
102
|
+
toleranceAfter:kCMTimeZero
|
|
103
|
+
completionHandler:nil];
|
|
104
|
+
} else {
|
|
105
|
+
_isPlaying = false;
|
|
106
|
+
}
|
|
107
|
+
}];
|
|
54
108
|
}
|
|
55
109
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
void RNSkAppleVideo::setupDisplayLink() {
|
|
111
|
+
#if !TARGET_OS_OSX
|
|
112
|
+
_displayLinkTarget = [[RNSkDisplayLinkTarget alloc] init];
|
|
113
|
+
[_displayLinkTarget setVideo:this];
|
|
114
|
+
|
|
115
|
+
_displayLink =
|
|
116
|
+
[CADisplayLink displayLinkWithTarget:_displayLinkTarget
|
|
117
|
+
selector:@selector(displayLinkFired:)];
|
|
118
|
+
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop]
|
|
119
|
+
forMode:NSRunLoopCommonModes];
|
|
120
|
+
_displayLink.paused = YES; // Start paused, will unpause when play() is called
|
|
121
|
+
#else
|
|
122
|
+
// Create CVDisplayLink for macOS
|
|
123
|
+
CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
|
|
124
|
+
CVDisplayLinkSetOutputCallback(_displayLink, &displayLinkCallback, this);
|
|
125
|
+
_displayLinkRunning = false;
|
|
126
|
+
#endif
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
void RNSkAppleVideo::onDisplayLink() {
|
|
130
|
+
CMTime outputItemTime =
|
|
131
|
+
[_videoOutput itemTimeForHostTime:CACurrentMediaTime()];
|
|
132
|
+
|
|
133
|
+
if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) {
|
|
134
|
+
CMTime actualItemTime = kCMTimeZero;
|
|
135
|
+
CVPixelBufferRef pixelBuffer =
|
|
136
|
+
[_videoOutput copyPixelBufferForItemTime:outputItemTime
|
|
137
|
+
itemTimeForDisplay:&actualItemTime];
|
|
138
|
+
if (pixelBuffer) {
|
|
139
|
+
_lastImage = _context->makeImageFromNativeBuffer((void *)pixelBuffer);
|
|
140
|
+
_lastFrameTimeMs = CMTimeGetSeconds(actualItemTime) * 1000;
|
|
141
|
+
CVPixelBufferRelease(pixelBuffer);
|
|
142
|
+
|
|
143
|
+
if (_waitingForFrame) {
|
|
144
|
+
_waitingForFrame = false;
|
|
145
|
+
// If paused and we got the frame we were waiting for, pause the display
|
|
146
|
+
// link
|
|
147
|
+
if (!_isPlaying) {
|
|
148
|
+
#if !TARGET_OS_OSX
|
|
149
|
+
_displayLink.paused = YES;
|
|
150
|
+
#else
|
|
151
|
+
stopDisplayLink();
|
|
152
|
+
#endif
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
64
156
|
}
|
|
157
|
+
}
|
|
65
158
|
|
|
66
|
-
|
|
159
|
+
void RNSkAppleVideo::expectFrame() {
|
|
160
|
+
_waitingForFrame = true;
|
|
161
|
+
#if !TARGET_OS_OSX
|
|
162
|
+
_displayLink.paused = NO;
|
|
163
|
+
#else
|
|
164
|
+
startDisplayLink();
|
|
165
|
+
#endif
|
|
166
|
+
}
|
|
67
167
|
|
|
68
|
-
|
|
69
|
-
|
|
168
|
+
#if TARGET_OS_OSX
|
|
169
|
+
CVReturn RNSkAppleVideo::displayLinkCallback(CVDisplayLinkRef displayLink,
|
|
170
|
+
const CVTimeStamp *now,
|
|
171
|
+
const CVTimeStamp *outputTime,
|
|
172
|
+
CVOptionFlags flagsIn,
|
|
173
|
+
CVOptionFlags *flagsOut,
|
|
174
|
+
void *context) {
|
|
175
|
+
RNSkAppleVideo *video = static_cast<RNSkAppleVideo *>(context);
|
|
176
|
+
// Dispatch to main thread since video operations need to happen there
|
|
177
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
178
|
+
video->onDisplayLink();
|
|
179
|
+
});
|
|
180
|
+
return kCVReturnSuccess;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
void RNSkAppleVideo::startDisplayLink() {
|
|
184
|
+
if (_displayLink && !_displayLinkRunning) {
|
|
185
|
+
CVDisplayLinkStart(_displayLink);
|
|
186
|
+
_displayLinkRunning = true;
|
|
70
187
|
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
void RNSkAppleVideo::stopDisplayLink() {
|
|
191
|
+
if (_displayLink && _displayLinkRunning) {
|
|
192
|
+
CVDisplayLinkStop(_displayLink);
|
|
193
|
+
_displayLinkRunning = false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
#endif
|
|
71
197
|
|
|
72
|
-
|
|
73
|
-
|
|
198
|
+
sk_sp<SkImage> RNSkAppleVideo::nextImage(double *timeStamp) {
|
|
199
|
+
if (timeStamp) {
|
|
200
|
+
*timeStamp = CMTimeGetSeconds([_player currentTime]);
|
|
201
|
+
}
|
|
202
|
+
return _lastImage;
|
|
74
203
|
}
|
|
75
204
|
|
|
76
205
|
NSDictionary *RNSkAppleVideo::getOutputSettings() {
|
|
@@ -83,7 +212,6 @@ NSDictionary *RNSkAppleVideo::getOutputSettings() {
|
|
|
83
212
|
float RNSkAppleVideo::getRotationInDegrees() {
|
|
84
213
|
CGFloat rotationAngle = 0.0;
|
|
85
214
|
auto transform = _preferredTransform;
|
|
86
|
-
// Determine the rotation angle in radians
|
|
87
215
|
if (transform.a == 0 && transform.b == 1 && transform.c == -1 &&
|
|
88
216
|
transform.d == 0) {
|
|
89
217
|
rotationAngle = 90;
|
|
@@ -100,12 +228,16 @@ float RNSkAppleVideo::getRotationInDegrees() {
|
|
|
100
228
|
void RNSkAppleVideo::seek(double timeInMilliseconds) {
|
|
101
229
|
CMTime seekTime =
|
|
102
230
|
CMTimeMakeWithSeconds(timeInMilliseconds / 1000.0, NSEC_PER_SEC);
|
|
231
|
+
CMTime previousTime = [_player currentTime];
|
|
232
|
+
|
|
103
233
|
[_player seekToTime:seekTime
|
|
104
234
|
toleranceBefore:kCMTimeZero
|
|
105
235
|
toleranceAfter:kCMTimeZero
|
|
106
236
|
completionHandler:^(BOOL finished) {
|
|
107
|
-
if (
|
|
108
|
-
|
|
237
|
+
if (finished &&
|
|
238
|
+
CMTimeCompare([_player currentTime], previousTime) != 0) {
|
|
239
|
+
// Ensure a frame is extracted after seek, even when paused
|
|
240
|
+
expectFrame();
|
|
109
241
|
}
|
|
110
242
|
}];
|
|
111
243
|
}
|
|
@@ -114,6 +246,11 @@ void RNSkAppleVideo::play() {
|
|
|
114
246
|
if (_player) {
|
|
115
247
|
[_player play];
|
|
116
248
|
_isPlaying = true;
|
|
249
|
+
#if !TARGET_OS_OSX
|
|
250
|
+
_displayLink.paused = NO;
|
|
251
|
+
#else
|
|
252
|
+
startDisplayLink();
|
|
253
|
+
#endif
|
|
117
254
|
}
|
|
118
255
|
}
|
|
119
256
|
|
|
@@ -121,6 +258,14 @@ void RNSkAppleVideo::pause() {
|
|
|
121
258
|
if (_player) {
|
|
122
259
|
[_player pause];
|
|
123
260
|
_isPlaying = false;
|
|
261
|
+
// Only pause display link if not waiting for a frame
|
|
262
|
+
if (!_waitingForFrame) {
|
|
263
|
+
#if !TARGET_OS_OSX
|
|
264
|
+
_displayLink.paused = YES;
|
|
265
|
+
#else
|
|
266
|
+
stopDisplayLink();
|
|
267
|
+
#endif
|
|
268
|
+
}
|
|
124
269
|
}
|
|
125
270
|
}
|
|
126
271
|
|
|
@@ -128,10 +273,20 @@ double RNSkAppleVideo::duration() { return _duration; }
|
|
|
128
273
|
|
|
129
274
|
double RNSkAppleVideo::framerate() { return _framerate; }
|
|
130
275
|
|
|
276
|
+
double RNSkAppleVideo::currentTime() {
|
|
277
|
+
// Return the timestamp of the last captured frame for accurate
|
|
278
|
+
// synchronization
|
|
279
|
+
return _lastFrameTimeMs;
|
|
280
|
+
}
|
|
281
|
+
|
|
131
282
|
SkISize RNSkAppleVideo::getSize() {
|
|
132
283
|
return SkISize::Make(_videoWidth, _videoHeight);
|
|
133
284
|
}
|
|
134
285
|
|
|
135
286
|
void RNSkAppleVideo::setVolume(float volume) { _player.volume = volume; }
|
|
136
287
|
|
|
288
|
+
void RNSkAppleVideo::setLooping(bool looping) { _isLooping = looping; }
|
|
289
|
+
|
|
290
|
+
bool RNSkAppleVideo::isPlaying() { return _isPlaying; }
|
|
291
|
+
|
|
137
292
|
} // namespace RNSkia
|
package/cpp/api/JsiSkApi.h
CHANGED
|
@@ -147,30 +147,32 @@ public:
|
|
|
147
147
|
|
|
148
148
|
installFunction("Recorder", JsiRecorder::createCtor(context));
|
|
149
149
|
|
|
150
|
-
installFunction(
|
|
150
|
+
installFunction(
|
|
151
|
+
"hasDevice", JSI_HOST_FUNCTION_LAMBDA {
|
|
151
152
|
#ifdef SK_GRAPHITE
|
|
152
|
-
|
|
153
|
+
return jsi::Value(true);
|
|
153
154
|
#else
|
|
154
155
|
return jsi::Value(false);
|
|
155
156
|
#endif
|
|
156
|
-
|
|
157
|
+
});
|
|
157
158
|
|
|
158
|
-
installFunction(
|
|
159
|
+
installFunction(
|
|
160
|
+
"getDevice", JSI_HOST_FUNCTION_LAMBDA {
|
|
159
161
|
#ifdef SK_GRAPHITE
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
auto &dawnContext = DawnContext::getInstance();
|
|
163
|
+
auto asyncRunner = rnwgpu::async::AsyncRunner::get(runtime);
|
|
164
|
+
if (!asyncRunner) {
|
|
165
|
+
throw jsi::JSError(runtime, "AsyncRunner not initialized");
|
|
166
|
+
}
|
|
167
|
+
auto device = std::make_shared<rnwgpu::GPUDevice>(
|
|
168
|
+
dawnContext.getWGPUDevice(), asyncRunner, "Skia Device");
|
|
169
|
+
return rnwgpu::GPUDevice::create(runtime, device);
|
|
168
170
|
#else
|
|
169
171
|
throw jsi::JSError(runtime,
|
|
170
172
|
"getDevice() is only available with the Graphite "
|
|
171
173
|
"backend. Rebuild with SK_GRAPHITE enabled.");
|
|
172
174
|
#endif
|
|
173
|
-
|
|
175
|
+
});
|
|
174
176
|
}
|
|
175
177
|
};
|
|
176
178
|
} // namespace RNSkia
|
|
@@ -126,8 +126,8 @@ public:
|
|
|
126
126
|
* Throws if the object has been disposed.
|
|
127
127
|
* @return Underlying object
|
|
128
128
|
*/
|
|
129
|
-
T getObject() { return
|
|
130
|
-
const T getObject() const { return
|
|
129
|
+
T getObject() { return validateObject(); }
|
|
130
|
+
const T getObject() const { return validateObject(); }
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
133
|
* Updates the inner object with a new version of the object.
|
|
@@ -139,15 +139,57 @@ public:
|
|
|
139
139
|
* macro.
|
|
140
140
|
*/
|
|
141
141
|
JSI_HOST_FUNCTION(dispose) {
|
|
142
|
-
|
|
142
|
+
safeDispose();
|
|
143
143
|
return jsi::Value::undefined();
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
protected:
|
|
147
|
+
/**
|
|
148
|
+
* Override to implement disposal of allocated resources like smart pointers.
|
|
149
|
+
* This method will only be called once for each instance of this class.
|
|
150
|
+
*/
|
|
151
|
+
virtual void releaseResources() = 0;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Returns true if the object has been disposed.
|
|
155
|
+
*/
|
|
156
|
+
bool isDisposed() const { return _isDisposed.load(std::memory_order_acquire); }
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Returns the underlying object without checking if disposed.
|
|
160
|
+
* Use this in destructors and releaseResources() where we need to access
|
|
161
|
+
* the object even after dispose() was called.
|
|
162
|
+
*/
|
|
163
|
+
T getObjectUnchecked() const { return _object; }
|
|
164
|
+
|
|
146
165
|
private:
|
|
166
|
+
/**
|
|
167
|
+
* Validates that _object was not disposed and returns it.
|
|
168
|
+
*/
|
|
169
|
+
T validateObject() const {
|
|
170
|
+
if (_isDisposed.load(std::memory_order_acquire)) {
|
|
171
|
+
throw std::runtime_error("Attempted to access a disposed object.");
|
|
172
|
+
}
|
|
173
|
+
return _object;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
void safeDispose() {
|
|
177
|
+
bool expected = false;
|
|
178
|
+
if (_isDisposed.compare_exchange_strong(expected, true,
|
|
179
|
+
std::memory_order_acq_rel)) {
|
|
180
|
+
releaseResources();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
147
184
|
/**
|
|
148
185
|
* Wrapped object.
|
|
149
186
|
*/
|
|
150
187
|
T _object;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resource disposed flag.
|
|
191
|
+
*/
|
|
192
|
+
std::atomic<bool> _isDisposed = {false};
|
|
151
193
|
};
|
|
152
194
|
|
|
153
195
|
template <typename T>
|
|
@@ -168,6 +210,12 @@ public:
|
|
|
168
210
|
obj.asObject(runtime).asHostObject(runtime))
|
|
169
211
|
->getObject();
|
|
170
212
|
}
|
|
213
|
+
|
|
214
|
+
protected:
|
|
215
|
+
void releaseResources() override {
|
|
216
|
+
// Clear internally allocated objects
|
|
217
|
+
this->setObject(nullptr);
|
|
218
|
+
}
|
|
171
219
|
};
|
|
172
220
|
|
|
173
221
|
template <typename T>
|
|
@@ -186,6 +234,12 @@ public:
|
|
|
186
234
|
obj.asObject(runtime).asHostObject(runtime))
|
|
187
235
|
->getObject();
|
|
188
236
|
}
|
|
237
|
+
|
|
238
|
+
protected:
|
|
239
|
+
void releaseResources() override {
|
|
240
|
+
// Clear internally allocated objects
|
|
241
|
+
this->setObject(nullptr);
|
|
242
|
+
}
|
|
189
243
|
};
|
|
190
244
|
|
|
191
245
|
} // namespace RNSkia
|