@shopify/react-native-skia 2.4.17 → 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.
Files changed (87) hide show
  1. package/android/cpp/rnskia-android/RNSkAndroidVideo.cpp +34 -0
  2. package/android/cpp/rnskia-android/RNSkAndroidVideo.h +3 -0
  3. package/android/src/main/java/com/shopify/reactnative/skia/RNSkVideo.java +72 -18
  4. package/android/src/main/java/com/shopify/reactnative/skia/SkiaBaseView.java +7 -7
  5. package/apple/RNSkAppleVideo.h +30 -3
  6. package/apple/RNSkAppleVideo.mm +172 -17
  7. package/cpp/api/JsiSkApi.h +15 -13
  8. package/cpp/api/JsiSkHostObjects.h +57 -3
  9. package/cpp/api/JsiSkImage.h +19 -5
  10. package/cpp/api/JsiSkPicture.h +19 -5
  11. package/cpp/api/JsiSkSurface.h +19 -5
  12. package/cpp/api/JsiVideo.h +15 -2
  13. package/cpp/api/recorder/Convertor.h +4 -2
  14. package/cpp/jsi2/EnumMapper.h +49 -34
  15. package/cpp/jsi2/JSIConverter.h +149 -99
  16. package/cpp/jsi2/NativeObject.h +23 -25
  17. package/cpp/jsi2/Promise.cpp +10 -6
  18. package/cpp/jsi2/Promise.h +9 -7
  19. package/cpp/rnskia/RNDawnContext.h +3 -8
  20. package/cpp/rnskia/RNSkManager.cpp +13 -7
  21. package/cpp/rnskia/RNSkVideo.h +3 -0
  22. package/cpp/rnwgpu/api/GPUAdapter.cpp +31 -32
  23. package/cpp/rnwgpu/api/GPUAdapter.h +1 -1
  24. package/cpp/rnwgpu/api/GPUBuffer.cpp +8 -8
  25. package/cpp/rnwgpu/api/GPUCommandEncoder.h +4 -4
  26. package/cpp/rnwgpu/api/GPUDevice.h +12 -12
  27. package/cpp/rnwgpu/api/GPUQueue.cpp +45 -44
  28. package/cpp/rnwgpu/api/GPUQueue.h +1 -1
  29. package/cpp/rnwgpu/api/GPURenderBundleEncoder.h +1 -1
  30. package/cpp/rnwgpu/api/GPURenderPassEncoder.h +1 -1
  31. package/cpp/rnwgpu/api/descriptors/GPUBindGroupEntry.h +1 -1
  32. package/cpp/rnwgpu/api/descriptors/GPUComputePipelineDescriptor.h +1 -1
  33. package/cpp/rnwgpu/api/descriptors/GPUImageCopyExternalImage.h +7 -6
  34. package/cpp/rnwgpu/api/descriptors/GPURenderPassDescriptor.h +1 -1
  35. package/cpp/rnwgpu/api/descriptors/GPURenderPipelineDescriptor.h +1 -1
  36. package/cpp/rnwgpu/api/descriptors/GPUVertexState.h +1 -1
  37. package/cpp/rnwgpu/async/AsyncRunner.cpp +2 -1
  38. package/cpp/rnwgpu/async/AsyncRunner.h +2 -1
  39. package/lib/commonjs/external/reanimated/useVideo.js +30 -31
  40. package/lib/commonjs/external/reanimated/useVideo.js.map +1 -1
  41. package/lib/commonjs/renderer/Offscreen.js +1 -0
  42. package/lib/commonjs/renderer/Offscreen.js.map +1 -1
  43. package/lib/commonjs/skia/types/Video/Video.d.ts +3 -0
  44. package/lib/commonjs/skia/types/Video/Video.js.map +1 -1
  45. package/lib/commonjs/skia/web/JsiVideo.d.ts +3 -0
  46. package/lib/commonjs/skia/web/JsiVideo.js +9 -0
  47. package/lib/commonjs/skia/web/JsiVideo.js.map +1 -1
  48. package/lib/commonjs/sksg/Container.web.js +1 -0
  49. package/lib/commonjs/sksg/Container.web.js.map +1 -1
  50. package/lib/commonjs/sksg/HostConfig.js +4 -1
  51. package/lib/commonjs/sksg/HostConfig.js.map +1 -1
  52. package/lib/commonjs/sksg/Reconciler.js +6 -6
  53. package/lib/commonjs/sksg/Reconciler.js.map +1 -1
  54. package/lib/commonjs/sksg/StaticContainer.js +1 -0
  55. package/lib/commonjs/sksg/StaticContainer.js.map +1 -1
  56. package/lib/module/external/reanimated/useVideo.js +30 -31
  57. package/lib/module/external/reanimated/useVideo.js.map +1 -1
  58. package/lib/module/renderer/Offscreen.js +1 -0
  59. package/lib/module/renderer/Offscreen.js.map +1 -1
  60. package/lib/module/skia/types/Video/Video.d.ts +3 -0
  61. package/lib/module/skia/types/Video/Video.js.map +1 -1
  62. package/lib/module/skia/web/JsiVideo.d.ts +3 -0
  63. package/lib/module/skia/web/JsiVideo.js +9 -0
  64. package/lib/module/skia/web/JsiVideo.js.map +1 -1
  65. package/lib/module/sksg/Container.web.js +1 -0
  66. package/lib/module/sksg/Container.web.js.map +1 -1
  67. package/lib/module/sksg/HostConfig.js +4 -1
  68. package/lib/module/sksg/HostConfig.js.map +1 -1
  69. package/lib/module/sksg/Reconciler.js +6 -6
  70. package/lib/module/sksg/Reconciler.js.map +1 -1
  71. package/lib/module/sksg/StaticContainer.js +1 -0
  72. package/lib/module/sksg/StaticContainer.js.map +1 -1
  73. package/lib/typescript/lib/commonjs/skia/web/JsiVideo.d.ts +3 -0
  74. package/lib/typescript/lib/commonjs/sksg/HostConfig.d.ts +2 -0
  75. package/lib/typescript/lib/module/skia/web/JsiVideo.d.ts +3 -0
  76. package/lib/typescript/lib/module/sksg/HostConfig.d.ts +2 -0
  77. package/lib/typescript/src/skia/types/Video/Video.d.ts +3 -0
  78. package/lib/typescript/src/skia/web/JsiVideo.d.ts +3 -0
  79. package/package.json +1 -1
  80. package/src/external/reanimated/useVideo.ts +32 -32
  81. package/src/renderer/Offscreen.tsx +1 -0
  82. package/src/skia/types/Video/Video.ts +3 -0
  83. package/src/skia/web/JsiVideo.ts +12 -0
  84. package/src/sksg/Container.web.ts +1 -0
  85. package/src/sksg/HostConfig.ts +4 -0
  86. package/src/sksg/Reconciler.ts +5 -6
  87. 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
- mp.start();
69
- isPlaying = true;
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
- return null;
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 buffers
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 != null && !isPlaying) {
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;
@@ -18,7 +18,7 @@ public abstract class SkiaBaseView extends ReactViewGroup implements SkiaViewAPI
18
18
 
19
19
  public SkiaBaseView(Context context) {
20
20
  super(context);
21
- mView = new SkiaSurfaceView(context, this, debug);
21
+ mView = new SkiaTextureView(context, this, debug);
22
22
  addView(mView);
23
23
  }
24
24
 
@@ -33,15 +33,15 @@ public abstract class SkiaBaseView extends ReactViewGroup implements SkiaViewAPI
33
33
  }
34
34
 
35
35
  public void setOpaque(boolean value) {
36
- // if (value && mView instanceof SkiaTextureView) {
36
+ if (value && mView instanceof SkiaTextureView) {
37
37
  removeView(mView);
38
38
  mView = new SkiaSurfaceView(getContext(), this, debug);
39
39
  addView(mView);
40
- // } else if (!value && mView instanceof SkiaSurfaceView) {
41
- // removeView(mView);
42
- // mView = new SkiaTextureView(getContext(), this, debug);
43
- // addView(mView);
44
- // }
40
+ } else if (!value && mView instanceof SkiaSurfaceView) {
41
+ removeView(mView);
42
+ mView = new SkiaTextureView(getContext(), this, debug);
43
+ addView(mView);
44
+ }
45
45
  }
46
46
 
47
47
  void dropInstance() {
@@ -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
@@ -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
- play();
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
- sk_sp<SkImage> RNSkAppleVideo::nextImage(double *timeStamp) {
57
- CMTime currentTime = [_player currentTime];
58
- CVPixelBufferRef pixelBuffer =
59
- [_videoOutput copyPixelBufferForItemTime:currentTime
60
- itemTimeForDisplay:nullptr];
61
- if (!pixelBuffer) {
62
- NSLog(@"No pixel buffer.");
63
- return nullptr;
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
- auto skImage = _context->makeImageFromNativeBuffer((void *)pixelBuffer);
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
- if (timeStamp) {
69
- *timeStamp = CMTimeGetSeconds(currentTime);
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
- CVPixelBufferRelease(pixelBuffer);
73
- return skImage;
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 (!finished) {
108
- NSLog(@"Seek failed or was interrupted.");
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
@@ -147,30 +147,32 @@ public:
147
147
 
148
148
  installFunction("Recorder", JsiRecorder::createCtor(context));
149
149
 
150
- installFunction("hasDevice", JSI_HOST_FUNCTION_LAMBDA {
150
+ installFunction(
151
+ "hasDevice", JSI_HOST_FUNCTION_LAMBDA {
151
152
  #ifdef SK_GRAPHITE
152
- return jsi::Value(true);
153
+ return jsi::Value(true);
153
154
  #else
154
155
  return jsi::Value(false);
155
156
  #endif
156
- });
157
+ });
157
158
 
158
- installFunction("getDevice", JSI_HOST_FUNCTION_LAMBDA {
159
+ installFunction(
160
+ "getDevice", JSI_HOST_FUNCTION_LAMBDA {
159
161
  #ifdef SK_GRAPHITE
160
- auto &dawnContext = DawnContext::getInstance();
161
- auto asyncRunner = rnwgpu::async::AsyncRunner::get(runtime);
162
- if (!asyncRunner) {
163
- throw jsi::JSError(runtime, "AsyncRunner not initialized");
164
- }
165
- auto device = std::make_shared<rnwgpu::GPUDevice>(
166
- dawnContext.getWGPUDevice(), asyncRunner, "Skia Device");
167
- return rnwgpu::GPUDevice::create(runtime, device);
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