react-native-webgpu 0.10.5 → 0.11.1

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 (61) hide show
  1. package/android/cpp/AndroidPlatformContext.h +100 -9
  2. package/android/cpp/AndroidVideoPlayer.h +307 -0
  3. package/android/cpp/cpp-adapter.cpp +66 -6
  4. package/android/src/main/java/com/webgpu/ViewSnapshot.java +162 -0
  5. package/android/src/main/java/com/webgpu/WebGPUModule.java +78 -0
  6. package/android/src/main/java/com/webgpu/WebGPUVideoPlayer.java +420 -0
  7. package/apple/ApplePlatformContext.h +14 -1
  8. package/apple/ApplePlatformContext.mm +176 -1
  9. package/apple/AppleVideoPlayer.mm +157 -47
  10. package/apple/WebGPUModule.mm +20 -1
  11. package/cpp/rnwgpu/PlatformContext.h +76 -1
  12. package/cpp/rnwgpu/RNWebGPUManager.cpp +7 -0
  13. package/cpp/rnwgpu/api/GPUDevice.cpp +2 -1
  14. package/cpp/rnwgpu/api/GPUDevice.h +6 -0
  15. package/cpp/rnwgpu/api/GPUQueue.cpp +261 -4
  16. package/cpp/rnwgpu/api/GPUQueue.h +29 -2
  17. package/cpp/rnwgpu/api/VideoFrameBlit.h +355 -0
  18. package/cpp/rnwgpu/api/VideoPlayer.h +50 -3
  19. package/cpp/rnwgpu/api/descriptors/GPUDrawElementImageDestination.h +60 -0
  20. package/cpp/rnwgpu/api/descriptors/GPUDrawElementImageSource.h +106 -0
  21. package/cpp/rnwgpu/api/descriptors/GPUImageCopyExternalImage.h +30 -3
  22. package/lib/commonjs/importDevice.js +2 -2
  23. package/lib/commonjs/index.js.map +1 -1
  24. package/lib/commonjs/main/index.js +45 -0
  25. package/lib/commonjs/main/index.js.map +1 -1
  26. package/lib/commonjs/main/index.web.js +11 -0
  27. package/lib/commonjs/main/index.web.js.map +1 -1
  28. package/lib/commonjs/video.js +58 -0
  29. package/lib/commonjs/video.js.map +1 -0
  30. package/lib/module/importDevice.js +2 -2
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/main/index.js +34 -0
  33. package/lib/module/main/index.js.map +1 -1
  34. package/lib/module/main/index.web.js +1 -0
  35. package/lib/module/main/index.web.js.map +1 -1
  36. package/lib/module/video.js +49 -0
  37. package/lib/module/video.js.map +1 -0
  38. package/lib/typescript/lib/commonjs/video.d.ts +30 -0
  39. package/lib/typescript/lib/commonjs/video.d.ts.map +1 -0
  40. package/lib/typescript/lib/module/main/index.d.ts +1 -0
  41. package/lib/typescript/lib/module/main/index.web.d.ts +1 -0
  42. package/lib/typescript/lib/module/video.d.ts +4 -0
  43. package/lib/typescript/lib/module/video.d.ts.map +1 -0
  44. package/lib/typescript/src/importDevice.d.ts +2 -2
  45. package/lib/typescript/src/index.d.ts +8 -3
  46. package/lib/typescript/src/index.d.ts.map +1 -1
  47. package/lib/typescript/src/main/index.d.ts +1 -0
  48. package/lib/typescript/src/main/index.d.ts.map +1 -1
  49. package/lib/typescript/src/main/index.web.d.ts +1 -0
  50. package/lib/typescript/src/main/index.web.d.ts.map +1 -1
  51. package/lib/typescript/src/types.d.ts +32 -0
  52. package/lib/typescript/src/types.d.ts.map +1 -1
  53. package/lib/typescript/src/video.d.ts +41 -0
  54. package/lib/typescript/src/video.d.ts.map +1 -0
  55. package/package.json +1 -1
  56. package/src/importDevice.ts +2 -2
  57. package/src/index.tsx +57 -3
  58. package/src/main/index.tsx +50 -0
  59. package/src/main/index.web.tsx +1 -0
  60. package/src/types.ts +89 -7
  61. package/src/video.ts +69 -0
@@ -1,15 +1,25 @@
1
1
  #pragma once
2
2
 
3
3
  #include "PlatformContext.h"
4
+ #include <functional>
4
5
  #include <string>
5
6
 
6
7
  namespace rnwgpu {
7
8
 
8
9
  class ApplePlatformContext : public PlatformContext {
9
10
  public:
10
- ApplePlatformContext();
11
+ // Resolves a React tag to its native view (an unretained
12
+ // UIView* / NSView* bridged to void*, or nullptr). Called on the main thread
13
+ // only. Provided by WebGPUModule from React Native's view registry.
14
+ using ViewLookup = std::function<void *(int)>;
15
+
16
+ explicit ApplePlatformContext(ViewLookup viewLookup = nullptr);
11
17
  ~ApplePlatformContext() = default;
12
18
 
19
+ void snapshotView(const ViewSnapshotRequest &request,
20
+ std::function<void(ImageData)> onSuccess,
21
+ std::function<void(std::string)> onError) override;
22
+
13
23
  wgpu::Surface makeSurface(wgpu::Instance instance, void *surface, int width,
14
24
  int height) override;
15
25
 
@@ -39,6 +49,9 @@ public:
39
49
  std::string writeTestVideoFile() override;
40
50
 
41
51
  VideoFrameHandle wrapNativeBuffer(void *pointer) override;
52
+
53
+ private:
54
+ ViewLookup _viewLookup;
42
55
  };
43
56
 
44
57
  } // namespace rnwgpu
@@ -10,10 +10,15 @@
10
10
  #import <ReactCommon/RCTTurboModule.h>
11
11
 
12
12
  #include "AppleVideoPlayer.h"
13
+ #import "RNWGUIKit.h"
13
14
 
14
15
  #include "RNWebGPUManager.h"
15
16
  #include "WebGPUModule.h"
16
17
 
18
+ #include <cmath>
19
+ #include <string>
20
+ #include <utility>
21
+
17
22
  namespace rnwgpu {
18
23
 
19
24
  void checkIfUsingSimulatorWithAPIValidation() {
@@ -30,10 +35,180 @@ void checkIfUsingSimulatorWithAPIValidation() {
30
35
  #endif
31
36
  }
32
37
 
33
- ApplePlatformContext::ApplePlatformContext() {
38
+ ApplePlatformContext::ApplePlatformContext(ViewLookup viewLookup)
39
+ : _viewLookup(std::move(viewLookup)) {
34
40
  checkIfUsingSimulatorWithAPIValidation();
35
41
  }
36
42
 
43
+ namespace {
44
+
45
+ // Rasterize `request` on the main thread into tightly packed, premultiplied
46
+ // BGRA8 pixels. Throws std::runtime_error with a user-facing message.
47
+ ImageData snapshotViewOnMainThread(const ApplePlatformContext::ViewLookup &lookup,
48
+ const ViewSnapshotRequest &request) {
49
+ if (!lookup) {
50
+ throw std::runtime_error(
51
+ "drawElementImageToTexture: no view registry is available");
52
+ }
53
+ RNWGPlatformView *view = (__bridge RNWGPlatformView *)lookup(request.viewTag);
54
+ if (view == nil) {
55
+ throw std::runtime_error(
56
+ "drawElementImageToTexture: no native view found for tag " +
57
+ std::to_string(request.viewTag) +
58
+ " (is the view mounted, and rendered with collapsable={false}?)");
59
+ }
60
+ const CGRect bounds = view.bounds;
61
+ if (bounds.size.width <= 0 || bounds.size.height <= 0) {
62
+ throw std::runtime_error(
63
+ "drawElementImageToTexture: the view has no size yet");
64
+ }
65
+
66
+ // Source rectangle in points (view space); the whole view by default.
67
+ const double sourceX = request.sourceX;
68
+ const double sourceY = request.sourceY;
69
+ const double sourceWidth =
70
+ request.sourceWidth > 0 ? request.sourceWidth : bounds.size.width;
71
+ const double sourceHeight =
72
+ request.sourceHeight > 0 ? request.sourceHeight : bounds.size.height;
73
+
74
+ // Natural pixel size: points times the screen scale the view is shown at.
75
+ #if !TARGET_OS_OSX
76
+ CGFloat scale = view.window.screen.scale;
77
+ if (scale <= 0) {
78
+ scale = UIScreen.mainScreen.scale;
79
+ }
80
+ #else
81
+ CGFloat scale = view.window.backingScaleFactor;
82
+ if (scale <= 0) {
83
+ scale = NSScreen.mainScreen.backingScaleFactor;
84
+ }
85
+ #endif
86
+ const uint32_t width =
87
+ request.width > 0
88
+ ? request.width
89
+ : static_cast<uint32_t>(std::llround(sourceWidth * scale));
90
+ const uint32_t height =
91
+ request.height > 0
92
+ ? request.height
93
+ : static_cast<uint32_t>(std::llround(sourceHeight * scale));
94
+ if (width == 0 || height == 0) {
95
+ throw std::runtime_error(
96
+ "drawElementImageToTexture: the source rectangle is empty");
97
+ }
98
+
99
+ // The view's bounds are drawn scaled so that the source rectangle covers
100
+ // the whole output.
101
+ const CGFloat scaleX = static_cast<CGFloat>(width) / sourceWidth;
102
+ const CGFloat scaleY = static_cast<CGFloat>(height) / sourceHeight;
103
+ const CGRect drawRect =
104
+ CGRectMake(-sourceX * scaleX, -sourceY * scaleY,
105
+ bounds.size.width * scaleX, bounds.size.height * scaleY);
106
+
107
+ CGImageRef cgImage = NULL;
108
+ #if !TARGET_OS_OSX
109
+ // drawViewHierarchyInRect renders what is on screen (including Metal /
110
+ // CAMetalLayer content), which is what "the element as displayed" means.
111
+ // afterScreenUpdates:YES flushes pending layout and layer changes first, so
112
+ // a snapshot taken right after a state change reflects that change.
113
+ UIGraphicsImageRendererFormat *format =
114
+ [UIGraphicsImageRendererFormat defaultFormat];
115
+ format.scale = 1.0;
116
+ format.opaque = NO;
117
+ format.preferredRange = UIGraphicsImageRendererFormatRangeStandard;
118
+ UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc]
119
+ initWithSize:CGSizeMake(width, height)
120
+ format:format];
121
+ // drawViewHierarchyInRect returns NO and draws nothing when the view is not
122
+ // in a window (e.g. a screen pushed under another one). The image would
123
+ // then be fully transparent, so treat it as a failure instead.
124
+ __block BOOL drawn = NO;
125
+ UIImage *image = [renderer
126
+ imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull context) {
127
+ drawn = [view drawViewHierarchyInRect:drawRect afterScreenUpdates:YES];
128
+ }];
129
+ if (!drawn) {
130
+ throw std::runtime_error(
131
+ "drawElementImageToTexture: the view could not be rendered (is it "
132
+ "attached to a window?)");
133
+ }
134
+ cgImage = image.CGImage;
135
+ if (cgImage != NULL) {
136
+ CGImageRetain(cgImage);
137
+ }
138
+ #else
139
+ NSBitmapImageRep *rep = [view bitmapImageRepForCachingDisplayInRect:bounds];
140
+ if (rep != nil) {
141
+ [view cacheDisplayInRect:bounds toBitmapImageRep:rep];
142
+ cgImage = rep.CGImage;
143
+ if (cgImage != NULL) {
144
+ CGImageRetain(cgImage);
145
+ }
146
+ }
147
+ #endif
148
+ if (cgImage == NULL) {
149
+ throw std::runtime_error(
150
+ "drawElementImageToTexture: the view could not be rendered");
151
+ }
152
+
153
+ // Normalize into premultiplied BGRA8 with no row padding, whatever the
154
+ // renderer produced.
155
+ ImageData result;
156
+ result.width = width;
157
+ result.height = height;
158
+ result.format = wgpu::TextureFormat::BGRA8Unorm;
159
+ result.premultiplied = true;
160
+ result.data.assign(static_cast<size_t>(width) * height * 4, 0);
161
+ CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
162
+ CGContextRef context = CGBitmapContextCreate(
163
+ result.data.data(), width, height, 8, static_cast<size_t>(width) * 4,
164
+ colorSpace, kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
165
+ CGColorSpaceRelease(colorSpace);
166
+ if (context == NULL) {
167
+ CGImageRelease(cgImage);
168
+ throw std::runtime_error(
169
+ "drawElementImageToTexture: could not allocate the pixel buffer");
170
+ }
171
+ #if !TARGET_OS_OSX
172
+ // The renderer already produced exactly width x height pixels.
173
+ const CGRect imageRect = CGRectMake(0, 0, width, height);
174
+ #else
175
+ // The cached display covers the whole view at the backing scale; draw it so
176
+ // that the source rectangle fills the output. CoreGraphics' origin is the
177
+ // bottom-left corner, hence the flipped vertical offset.
178
+ const CGRect imageRect = CGRectMake(
179
+ -sourceX * scaleX,
180
+ -(bounds.size.height - sourceY - sourceHeight) * scaleY,
181
+ bounds.size.width * scaleX, bounds.size.height * scaleY);
182
+ #endif
183
+ CGContextSetBlendMode(context, kCGBlendModeCopy);
184
+ CGContextDrawImage(context, imageRect, cgImage);
185
+ CGContextRelease(context);
186
+ CGImageRelease(cgImage);
187
+ return result;
188
+ }
189
+
190
+ } // namespace
191
+
192
+ void ApplePlatformContext::snapshotView(
193
+ const ViewSnapshotRequest &request,
194
+ std::function<void(ImageData)> onSuccess,
195
+ std::function<void(std::string)> onError) {
196
+ // Copies: the block outlives this call.
197
+ ViewLookup lookup = _viewLookup;
198
+ ViewSnapshotRequest snapshotRequest = request;
199
+ dispatch_async(dispatch_get_main_queue(), ^{
200
+ @autoreleasepool {
201
+ try {
202
+ onSuccess(snapshotViewOnMainThread(lookup, snapshotRequest));
203
+ } catch (const std::exception &error) {
204
+ onError(error.what());
205
+ } catch (...) {
206
+ onError("drawElementImageToTexture: unknown native error");
207
+ }
208
+ }
209
+ });
210
+ }
211
+
37
212
  wgpu::Surface ApplePlatformContext::makeSurface(wgpu::Instance instance,
38
213
  void *surface, int width,
39
214
  int height) {
@@ -3,6 +3,9 @@
3
3
  #import <AVFoundation/AVFoundation.h>
4
4
  #import <CoreVideo/CoreVideo.h>
5
5
 
6
+ #include <algorithm>
7
+ #include <atomic>
8
+ #include <memory>
6
9
  #include <stdexcept>
7
10
 
8
11
  namespace rnwgpu {
@@ -16,27 +19,27 @@ namespace {
16
19
  // range) means luma is 16..235, chroma is 16..240 (8-bit).
17
20
  // Reference: https://en.wikipedia.org/wiki/YCbCr (BT.601 / BT.709).
18
21
  static constexpr float kBT709LimitedToRgb[12] = {
19
- 1.164383f, 0.000000f, 1.792741f, -0.972945f, //
20
- 1.164383f, -0.213249f, -0.532909f, 0.301517f, //
21
- 1.164383f, 2.112402f, 0.000000f, -1.133402f, //
22
+ 1.164383f, 0.000000f, 1.792741f, -0.972945f, //
23
+ 1.164383f, -0.213249f, -0.532909f, 0.301517f, //
24
+ 1.164383f, 2.112402f, 0.000000f, -1.133402f, //
22
25
  };
23
26
  static constexpr float kBT601LimitedToRgb[12] = {
24
- 1.164383f, 0.000000f, 1.596027f, -0.874202f, //
25
- 1.164383f, -0.391762f, -0.812968f, 0.531668f, //
26
- 1.164383f, 2.017232f, 0.000000f, -1.085631f, //
27
+ 1.164383f, 0.000000f, 1.596027f, -0.874202f, //
28
+ 1.164383f, -0.391762f, -0.812968f, 0.531668f, //
29
+ 1.164383f, 2.017232f, 0.000000f, -1.085631f, //
27
30
  };
28
31
  static constexpr float kBT2020LimitedToRgb[12] = {
29
- 1.164383f, 0.000000f, 1.678674f, -0.915688f, //
30
- 1.164383f, -0.187326f, -0.650424f, 0.347459f, //
31
- 1.164383f, 2.141772f, 0.000000f, -1.148145f, //
32
+ 1.164383f, 0.000000f, 1.678674f, -0.915688f, //
33
+ 1.164383f, -0.187326f, -0.650424f, 0.347459f, //
34
+ 1.164383f, 2.141772f, 0.000000f, -1.148145f, //
32
35
  };
33
36
 
34
37
  // Pick the right YUV→RGB matrix from the pixel buffer's color attachments.
35
38
  // Falls back to BT.709 limited range (the right call for ≥720p H.264, which
36
39
  // is what AVPlayer hands us for Big Buck Bunny and most streamed media).
37
40
  static void fillYuvMatrix(CVPixelBufferRef pixelBuffer, float out[12]) {
38
- CFTypeRef matrixKey = CVBufferGetAttachment(
39
- pixelBuffer, kCVImageBufferYCbCrMatrixKey, nullptr);
41
+ CFTypeRef matrixKey =
42
+ CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, nullptr);
40
43
  const float *src = kBT709LimitedToRgb;
41
44
  if (matrixKey) {
42
45
  auto matrix = (CFStringRef)matrixKey;
@@ -53,28 +56,108 @@ static void fillYuvMatrix(CVPixelBufferRef pixelBuffer, float out[12]) {
53
56
  }
54
57
 
55
58
  // Map a CVPixelBuffer's pixel format to our VideoPixelFormat enum.
56
- static VideoPixelFormat pixelFormatFromCVPixelBuffer(
57
- CVPixelBufferRef pixelBuffer) {
59
+ static VideoPixelFormat
60
+ pixelFormatFromCVPixelBuffer(CVPixelBufferRef pixelBuffer) {
58
61
  OSType type = CVPixelBufferGetPixelFormatType(pixelBuffer);
59
62
  switch (type) {
60
- case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
61
- case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
62
- return VideoPixelFormat::NV12;
63
- default:
64
- return VideoPixelFormat::BGRA8;
63
+ case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
64
+ case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
65
+ return VideoPixelFormat::NV12;
66
+ default:
67
+ return VideoPixelFormat::BGRA8;
65
68
  }
66
69
  }
67
70
 
71
+ // The clockwise rotation, in degrees, that a track's preferredTransform
72
+ // applies to its frames: a video recorded in portrait stores landscape frames
73
+ // with a 90 or 270 degree transform.
74
+ static int rotationFromTransform(CGAffineTransform t) {
75
+ if (t.a == 0 && t.b == 1 && t.c == -1 && t.d == 0) {
76
+ return 90;
77
+ }
78
+ if (t.a == 0 && t.b == -1 && t.c == 1 && t.d == 0) {
79
+ return 270;
80
+ }
81
+ if (t.a == -1 && t.b == 0 && t.c == 0 && t.d == -1) {
82
+ return 180;
83
+ }
84
+ return 0;
85
+ }
86
+
87
+ // The state the AVFoundation callbacks update. It is shared with the blocks
88
+ // observing the asset and the item, so a callback that fires after the player
89
+ // was destroyed only touches memory that outlives it.
90
+ struct AppleVideoPlayerState {
91
+ std::atomic<bool> playing{false};
92
+ std::atomic<bool> loop{true};
93
+ std::atomic<double> duration{0};
94
+ std::atomic<double> frameRate{0};
95
+ std::atomic<uint32_t> width{0};
96
+ std::atomic<uint32_t> height{0};
97
+ std::atomic<int> rotation{0};
98
+ };
99
+
68
100
  class AppleVideoPlayer : public IVideoPlayer {
69
101
  public:
70
- AppleVideoPlayer(AVPlayer *player, AVPlayerItemVideoOutput *output,
71
- id loopObserver)
72
- : _player(player), _output(output), _loopObserver(loopObserver) {}
102
+ AppleVideoPlayer(AVPlayer *player, AVPlayerItem *item,
103
+ AVPlayerItemVideoOutput *output)
104
+ : _player(player), _output(output),
105
+ _state(std::make_shared<AppleVideoPlayerState>()) {
106
+ auto state = _state;
107
+
108
+ // The metadata loads asynchronously, so that a remote asset never blocks
109
+ // the calling thread; the getters report 0 until it has.
110
+ AVAsset *asset = item.asset;
111
+ #pragma clang diagnostic push
112
+ #pragma clang diagnostic ignored "-Wdeprecated-declarations"
113
+ [asset
114
+ loadValuesAsynchronouslyForKeys:@[ @"duration", @"tracks" ]
115
+ completionHandler:^{
116
+ if ([asset statusOfValueForKey:@"duration" error:nil] ==
117
+ AVKeyValueStatusLoaded) {
118
+ CMTime duration = asset.duration;
119
+ if (CMTIME_IS_NUMERIC(duration)) {
120
+ state->duration = CMTimeGetSeconds(duration);
121
+ }
122
+ }
123
+ if ([asset statusOfValueForKey:@"tracks" error:nil] ==
124
+ AVKeyValueStatusLoaded) {
125
+ AVAssetTrack *track =
126
+ [[asset tracksWithMediaType:AVMediaTypeVideo]
127
+ firstObject];
128
+ if (track) {
129
+ CGSize size = track.naturalSize;
130
+ state->width = static_cast<uint32_t>(size.width);
131
+ state->height = static_cast<uint32_t>(size.height);
132
+ state->rotation =
133
+ rotationFromTransform(track.preferredTransform);
134
+ state->frameRate = track.nominalFrameRate;
135
+ }
136
+ }
137
+ }];
138
+ #pragma clang diagnostic pop
139
+
140
+ // At the end of the stream, either loop or stop. The player keeps showing
141
+ // the last frame either way (actionAtItemEnd is None).
142
+ __weak AVPlayer *weakPlayer = player;
143
+ _endObserver = [[NSNotificationCenter defaultCenter]
144
+ addObserverForName:AVPlayerItemDidPlayToEndTimeNotification
145
+ object:item
146
+ queue:[NSOperationQueue mainQueue]
147
+ usingBlock:^(NSNotification * /*note*/) {
148
+ if (state->loop) {
149
+ [weakPlayer seekToTime:kCMTimeZero];
150
+ [weakPlayer play];
151
+ } else {
152
+ state->playing = false;
153
+ }
154
+ }];
155
+ }
73
156
 
74
157
  ~AppleVideoPlayer() override {
75
- if (_loopObserver) {
76
- [[NSNotificationCenter defaultCenter] removeObserver:_loopObserver];
77
- _loopObserver = nil;
158
+ if (_endObserver) {
159
+ [[NSNotificationCenter defaultCenter] removeObserver:_endObserver];
160
+ _endObserver = nil;
78
161
  }
79
162
  [_player pause];
80
163
  _player = nil;
@@ -105,13 +188,51 @@ public:
105
188
  }
106
189
  }
107
190
 
108
- void play() override { [_player play]; }
109
- void pause() override { [_player pause]; }
191
+ void play() override {
192
+ _state->playing = true;
193
+ [_player play];
194
+ }
195
+
196
+ void pause() override {
197
+ _state->playing = false;
198
+ [_player pause];
199
+ }
200
+
201
+ bool paused() override { return !_state->playing; }
202
+
203
+ double currentTime() override {
204
+ CMTime time = [_player currentTime];
205
+ return CMTIME_IS_NUMERIC(time) ? CMTimeGetSeconds(time) : 0;
206
+ }
207
+
208
+ // An exact seek: the output then holds the frame at that position, also
209
+ // while paused, so the next copyLatestFrame() returns it.
210
+ void seek(double seconds) override {
211
+ [_player seekToTime:CMTimeMakeWithSeconds(seconds, NSEC_PER_SEC)
212
+ toleranceBefore:kCMTimeZero
213
+ toleranceAfter:kCMTimeZero];
214
+ }
215
+
216
+ double duration() override { return _state->duration; }
217
+
218
+ double volume() override { return _player.volume; }
219
+ void setVolume(double volume) override {
220
+ _player.volume = static_cast<float>(std::clamp(volume, 0.0, 1.0));
221
+ }
222
+
223
+ bool loop() override { return _state->loop; }
224
+ void setLoop(bool loop) override { _state->loop = loop; }
225
+
226
+ uint32_t videoWidth() override { return _state->width; }
227
+ uint32_t videoHeight() override { return _state->height; }
228
+ int rotation() override { return _state->rotation; }
229
+ double frameRate() override { return _state->frameRate; }
110
230
 
111
231
  private:
112
232
  AVPlayer *_player;
113
233
  AVPlayerItemVideoOutput *_output;
114
- id _loopObserver;
234
+ std::shared_ptr<AppleVideoPlayerState> _state;
235
+ id _endObserver = nil;
115
236
  };
116
237
 
117
238
  } // namespace
@@ -142,8 +263,8 @@ VideoFrameHandle wrapCVPixelBuffer(CVPixelBufferRef pixelBuffer) {
142
263
  return handle;
143
264
  }
144
265
 
145
- std::unique_ptr<IVideoPlayer>
146
- createAppleVideoPlayer(const std::string &path, VideoPixelFormat format) {
266
+ std::unique_ptr<IVideoPlayer> createAppleVideoPlayer(const std::string &path,
267
+ VideoPixelFormat format) {
147
268
  NSString *nsPath = [NSString stringWithUTF8String:path.c_str()];
148
269
  NSURL *url;
149
270
  if ([nsPath hasPrefix:@"http://"] || [nsPath hasPrefix:@"https://"] ||
@@ -164,8 +285,8 @@ createAppleVideoPlayer(const std::string &path, VideoPixelFormat format) {
164
285
  // importExternalTexture. BGRA is the "decode + convert" path for the
165
286
  // single-plane SharedTextureMemory demo.
166
287
  OSType pixelFormat = format == VideoPixelFormat::NV12
167
- ? kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
168
- : kCVPixelFormatType_32BGRA;
288
+ ? kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
289
+ : kCVPixelFormatType_32BGRA;
169
290
  NSDictionary *outputSettings = @{
170
291
  (NSString *)kCVPixelBufferPixelFormatTypeKey : @(pixelFormat),
171
292
  (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{},
@@ -176,20 +297,11 @@ createAppleVideoPlayer(const std::string &path, VideoPixelFormat format) {
176
297
  [item addOutput:output];
177
298
 
178
299
  AVPlayer *player = [AVPlayer playerWithPlayerItem:item];
300
+ // Keep the last frame at the end of the stream; AppleVideoPlayer decides
301
+ // whether to loop.
179
302
  player.actionAtItemEnd = AVPlayerActionAtItemEndNone;
180
303
 
181
- // Loop on end-of-stream by seeking back to zero.
182
- __weak AVPlayer *weakPlayer = player;
183
- id loopObserver = [[NSNotificationCenter defaultCenter]
184
- addObserverForName:AVPlayerItemDidPlayToEndTimeNotification
185
- object:item
186
- queue:[NSOperationQueue mainQueue]
187
- usingBlock:^(NSNotification * /*note*/) {
188
- [weakPlayer seekToTime:kCMTimeZero];
189
- [weakPlayer play];
190
- }];
191
-
192
- return std::make_unique<AppleVideoPlayer>(player, output, loopObserver);
304
+ return std::make_unique<AppleVideoPlayer>(player, item, output);
193
305
  }
194
306
 
195
307
  std::string writeAppleTestVideoFile() {
@@ -231,8 +343,7 @@ std::string writeAppleTestVideoFile() {
231
343
  writerInput.expectsMediaDataInRealTime = NO;
232
344
 
233
345
  NSDictionary *bufferAttrs = @{
234
- (NSString *)kCVPixelBufferPixelFormatTypeKey :
235
- @(kCVPixelFormatType_32BGRA),
346
+ (NSString *)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA),
236
347
  (NSString *)kCVPixelBufferWidthKey : @(kWidth),
237
348
  (NSString *)kCVPixelBufferHeightKey : @(kHeight),
238
349
  (NSString *)kCVPixelBufferIOSurfacePropertiesKey : @{},
@@ -274,8 +385,7 @@ std::string writeAppleTestVideoFile() {
274
385
  for (int x = 0; x < kWidth; ++x) {
275
386
  uint8_t r = static_cast<uint8_t>((x + phase) & 0xFF);
276
387
  uint8_t g = static_cast<uint8_t>((y + phase * 2) & 0xFF);
277
- uint8_t b =
278
- static_cast<uint8_t>(((x + y + phase) & 0x40) ? 220 : 30);
388
+ uint8_t b = static_cast<uint8_t>(((x + y + phase) & 0x40) ? 220 : 30);
279
389
  row[x * 4 + 0] = b;
280
390
  row[x * 4 + 1] = g;
281
391
  row[x * 4 + 2] = r;
@@ -1,6 +1,7 @@
1
1
  #import "WebGPUModule.h"
2
2
  #include "ApplePlatformContext.h"
3
3
  #import "GPUCanvasContext.h"
4
+ #import "RNWGUIKit.h"
4
5
 
5
6
  #import <React/RCTBridge+Private.h>
6
7
  #import <React/RCTCallInvoker.h>
@@ -31,6 +32,11 @@ static std::shared_ptr<rnwgpu::RNWebGPUManager> webgpuManager;
31
32
  // calls setCallInvoker: during module initialization.
32
33
  @synthesize callInvoker = _callInvoker;
33
34
 
35
+ // Synthesize the view registry so the TurboModule infra injects it too. It
36
+ // resolves React tags to native views in both Bridge and Bridgeless mode
37
+ // (Fabric component views included), which drawElementImageToTexture needs.
38
+ @synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED;
39
+
34
40
  + (std::shared_ptr<rnwgpu::RNWebGPUManager>)getManager {
35
41
  return webgpuManager;
36
42
  }
@@ -74,8 +80,21 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) {
74
80
  return [NSNumber numberWithBool:NO];
75
81
  }
76
82
 
83
+ // Main-thread only (see ApplePlatformContext::ViewLookup). Weak: the module
84
+ // is invalidated on reload, after which lookups simply fail.
85
+ __weak WebGPUModule *weakSelf = self;
86
+ rnwgpu::ApplePlatformContext::ViewLookup viewLookup =
87
+ [weakSelf](int tag) -> void * {
88
+ WebGPUModule *strongSelf = weakSelf;
89
+ if (strongSelf == nil) {
90
+ return nullptr;
91
+ }
92
+ RNWGPlatformView *view =
93
+ [strongSelf.viewRegistry_DEPRECATED viewForReactTag:@(tag)];
94
+ return (__bridge void *)view;
95
+ };
77
96
  std::shared_ptr<rnwgpu::PlatformContext> platformContext =
78
- std::make_shared<rnwgpu::ApplePlatformContext>();
97
+ std::make_shared<rnwgpu::ApplePlatformContext>(std::move(viewLookup));
79
98
  webgpuManager = std::make_shared<rnwgpu::RNWebGPUManager>(runtime, jsInvoker,
80
99
  platformContext);
81
100
  return @true;
@@ -2,6 +2,7 @@
2
2
 
3
3
  #include <functional>
4
4
  #include <memory>
5
+ #include <mutex>
5
6
  #include <span>
6
7
  #include <string>
7
8
  #include <utility>
@@ -58,7 +59,8 @@ struct VideoFrameHandle {
58
59
  };
59
60
 
60
61
  // Platform-implemented video source that hands out fresh IOSurface /
61
- // AHardwareBuffer-backed frames as a video plays.
62
+ // AHardwareBuffer-backed frames as a video plays, with the playback controls
63
+ // of an HTMLMediaElement (times in seconds).
62
64
  class IVideoPlayer {
63
65
  public:
64
66
  virtual ~IVideoPlayer() = default;
@@ -70,6 +72,44 @@ public:
70
72
 
71
73
  virtual void play() = 0;
72
74
  virtual void pause() = 0;
75
+ virtual bool paused() = 0;
76
+
77
+ // Playback position and length, in seconds. The duration is 0 until the
78
+ // metadata of the media has loaded.
79
+ virtual double currentTime() = 0;
80
+ virtual void seek(double seconds) = 0;
81
+ virtual double duration() = 0;
82
+
83
+ virtual double volume() = 0;
84
+ virtual void setVolume(double volume) = 0;
85
+ virtual bool loop() = 0;
86
+ virtual void setLoop(bool loop) = 0;
87
+
88
+ // The geometry of the frames, 0 until the metadata has loaded: their coded
89
+ // size, the clockwise rotation (0, 90, 180 or 270) to apply when displaying
90
+ // them, and the nominal frame rate of the video track.
91
+ virtual uint32_t videoWidth() = 0;
92
+ virtual uint32_t videoHeight() = 0;
93
+ virtual int rotation() = 0;
94
+ virtual double frameRate() = 0;
95
+ };
96
+
97
+ // A request to rasterize a native React Native view (looked up by its React
98
+ // tag) for GPUQueue.drawElementImageToTexture.
99
+ //
100
+ // The source rectangle is expressed in the view's own coordinate space, in
101
+ // density-independent points (the units of React Native layout). A
102
+ // non-positive sourceWidth/sourceHeight means "the whole view". The
103
+ // destination size is in pixels: the platform rasterizes the source rectangle
104
+ // scaled to exactly width x height.
105
+ struct ViewSnapshotRequest {
106
+ int viewTag = 0;
107
+ double sourceX = 0;
108
+ double sourceY = 0;
109
+ double sourceWidth = 0;
110
+ double sourceHeight = 0;
111
+ uint32_t width = 0;
112
+ uint32_t height = 0;
73
113
  };
74
114
 
75
115
  class PlatformContext {
@@ -77,6 +117,31 @@ public:
77
117
  PlatformContext() = default;
78
118
  virtual ~PlatformContext() = default;
79
119
 
120
+ // The platform context of the currently installed RNWebGPUManager, for API
121
+ // objects (e.g. GPUQueue) that are created without a reference to it. Set on
122
+ // install, cleared on teardown; a dev reload replaces it.
123
+ static void setCurrent(std::shared_ptr<PlatformContext> context) {
124
+ std::lock_guard<std::mutex> lock(currentMutex());
125
+ currentSlot() = std::move(context);
126
+ }
127
+ static std::shared_ptr<PlatformContext> current() {
128
+ std::lock_guard<std::mutex> lock(currentMutex());
129
+ return currentSlot();
130
+ }
131
+
132
+ // Rasterize a native view into CPU pixels (see ViewSnapshotRequest). The
133
+ // implementation hops to the platform UI thread, so this is asynchronous:
134
+ // exactly one of onSuccess / onError is invoked, from an arbitrary thread
135
+ // (typically the UI thread). Callers must not touch JSI in the callbacks.
136
+ //
137
+ // The returned ImageData is tightly packed (no row padding), 4 bytes per
138
+ // pixel, `format` is rgba8unorm or bgra8unorm depending on what the platform
139
+ // rasterizer produces natively, and `premultiplied` reports its alpha
140
+ // representation.
141
+ virtual void snapshotView(const ViewSnapshotRequest &request,
142
+ std::function<void(ImageData)> onSuccess,
143
+ std::function<void(std::string)> onError) = 0;
144
+
80
145
  virtual wgpu::Surface makeSurface(wgpu::Instance instance, void *surface,
81
146
  int width, int height) = 0;
82
147
  virtual ImageData createImageBitmap(std::string blobId, double offset,
@@ -129,6 +194,16 @@ public:
129
194
  // and return its absolute path. Lets the SharedTextureMemory example play
130
195
  // a real decoded video without bundling an asset.
131
196
  virtual std::string writeTestVideoFile() = 0;
197
+
198
+ private:
199
+ static std::mutex &currentMutex() {
200
+ static std::mutex mutex;
201
+ return mutex;
202
+ }
203
+ static std::shared_ptr<PlatformContext> &currentSlot() {
204
+ static std::shared_ptr<PlatformContext> slot;
205
+ return slot;
206
+ }
132
207
  };
133
208
 
134
209
  } // namespace rnwgpu