@geastack/apple 0.2.8
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/LICENSE +202 -0
- package/README.md +54 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +3076 -0
- package/generated/AVFoundation.d.ts +112 -0
- package/generated/ActivityKit.d.ts +7 -0
- package/generated/AppKit.d.ts +268 -0
- package/generated/CoreGraphics.d.ts +18 -0
- package/generated/CoreLocation.d.ts +28 -0
- package/generated/CoreMedia.d.ts +8 -0
- package/generated/Dispatch.d.ts +2 -0
- package/generated/Foundation.d.ts +13 -0
- package/generated/MapKit.d.ts +28 -0
- package/generated/Metal.d.ts +91 -0
- package/generated/MetalKit.d.ts +22 -0
- package/generated/Photos.d.ts +5 -0
- package/generated/QuartzCore.d.ts +12 -0
- package/generated/SwiftData.d.ts +11 -0
- package/generated/UIKit.d.ts +187 -0
- package/package.json +123 -0
- package/runtime/AVFoundation.js +136 -0
- package/runtime/ActivityKit.js +18 -0
- package/runtime/AppKit.js +313 -0
- package/runtime/CoreGraphics.js +15 -0
- package/runtime/CoreLocation.js +30 -0
- package/runtime/CoreMedia.js +7 -0
- package/runtime/Dispatch.js +11 -0
- package/runtime/Foundation.js +21 -0
- package/runtime/MapKit.js +22 -0
- package/runtime/Metal.js +126 -0
- package/runtime/MetalKit.js +15 -0
- package/runtime/Photos.js +9 -0
- package/runtime/QuartzCore.js +9 -0
- package/runtime/SwiftData.js +29 -0
- package/runtime/UIKit.js +242 -0
- package/runtime/empty.js +1 -0
- package/targets/ios/Info.plist.in +25 -0
- package/targets/ios/README.md +46 -0
- package/targets/ios/build-ios.sh +395 -0
- package/targets/ios/detect-development-team.mjs +109 -0
- package/targets/ios/generate-resident-entry.mjs +67 -0
- package/targets/ios/generate-resident-registry.mjs +162 -0
- package/targets/ios/generate-xcode-project.mjs +520 -0
- package/targets/ios/main/canvas_view.h +7 -0
- package/targets/ios/main/canvas_view.mm +94 -0
- package/targets/ios/main/font_registry.h +17 -0
- package/targets/ios/main/font_registry.mm +129 -0
- package/targets/ios/main/image_bridge.h +13 -0
- package/targets/ios/main/image_bridge.mm +53 -0
- package/targets/ios/main/ios_apps.c +28 -0
- package/targets/ios/main/ios_camera.mm +446 -0
- package/targets/ios/main/ios_display.mm +344 -0
- package/targets/ios/main/ios_main.mm +811 -0
- package/targets/ios/main/ios_memory.cpp +55 -0
- package/targets/ios/main/ios_renderer.h +20 -0
- package/targets/ios/main/ios_renderer.mm +27 -0
- package/targets/ios/main/ios_root_background.cpp +20 -0
- package/targets/ios/main/ios_root_background.h +11 -0
- package/targets/ios/main/ios_sensors.mm +244 -0
- package/targets/ios/main/ios_timers.mm +53 -0
- package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
- package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
- package/targets/ios/main/renderer/native_button.mm +179 -0
- package/targets/ios/main/renderer/native_label.mm +161 -0
- package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
- package/targets/ios/main/renderer/view_reconciler.mm +274 -0
- package/targets/macos/Info.plist.in +26 -0
- package/targets/macos/README.md +131 -0
- package/targets/macos/build-child-lifecycle.sh +119 -0
- package/targets/macos/build-macos.sh +2156 -0
- package/targets/macos/check-module-graph-freshness.mjs +82 -0
- package/targets/macos/compiler-input-fingerprint.mjs +32 -0
- package/targets/macos/finalize-apple-native-output.mjs +77 -0
- package/targets/macos/generate-resident-entry.mjs +80 -0
- package/targets/macos/generate-resident-registry.mjs +141 -0
- package/targets/macos/heavy-build-lock.sh +176 -0
- package/targets/macos/main/canvas_view.h +9 -0
- package/targets/macos/main/canvas_view.mm +176 -0
- package/targets/macos/main/color_convert.h +18 -0
- package/targets/macos/main/color_convert.mm +39 -0
- package/targets/macos/main/font_registry.h +35 -0
- package/targets/macos/main/font_registry.mm +350 -0
- package/targets/macos/main/image_bridge.h +16 -0
- package/targets/macos/main/image_bridge.mm +52 -0
- package/targets/macos/main/macos_app_platform.mm +95 -0
- package/targets/macos/main/macos_apps.c +31 -0
- package/targets/macos/main/macos_device_control.mm +56 -0
- package/targets/macos/main/macos_display.mm +484 -0
- package/targets/macos/main/macos_host_device_control.mm +95 -0
- package/targets/macos/main/macos_main.mm +839 -0
- package/targets/macos/main/macos_memory.cpp +31 -0
- package/targets/macos/main/macos_native_shell.h +31 -0
- package/targets/macos/main/macos_native_shell.mm +396 -0
- package/targets/macos/main/macos_network.mm +253 -0
- package/targets/macos/main/macos_renderer.h +39 -0
- package/targets/macos/main/macos_renderer.mm +1969 -0
- package/targets/macos/main/macos_sensors.cpp +148 -0
- package/targets/macos/main/macos_smoke_app.cpp +65 -0
- package/targets/macos/main/macos_timers.mm +52 -0
- package/targets/macos/main/press_bridge.h +21 -0
- package/targets/macos/main/press_bridge.mm +72 -0
- package/targets/macos/main/thermalright_hid_display.h +14 -0
- package/targets/macos/main/thermalright_hid_display.mm +995 -0
- package/targets/macos/native-artifact-provenance.mjs +421 -0
- package/targets/macos/order-compile-edges.mjs +64 -0
- package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
- package/targets/macos/prune-generated-modules.mjs +147 -0
- package/targets/macos/sync-resources.mjs +153 -0
- package/targets/shared/analyze-audio-capture.mjs +131 -0
- package/targets/shared/apple_audio.mm +1194 -0
- package/targets/shared/apple_audio_selftest.mm +602 -0
- package/targets/shared/apple_audio_testing.h +34 -0
- package/targets/shared/build-audio-selftest.sh +50 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#import <CoreText/CoreText.h>
|
|
2
|
+
#import <Foundation/Foundation.h>
|
|
3
|
+
#import <UIKit/UIKit.h>
|
|
4
|
+
|
|
5
|
+
#include "font_registry.h"
|
|
6
|
+
#include "graphics/font.h"
|
|
7
|
+
|
|
8
|
+
#include <cmath>
|
|
9
|
+
#include <mutex>
|
|
10
|
+
|
|
11
|
+
namespace gea::ios {
|
|
12
|
+
namespace {
|
|
13
|
+
|
|
14
|
+
std::once_flag g_registerFontsOnce;
|
|
15
|
+
|
|
16
|
+
void registerFontURL(NSURL *url)
|
|
17
|
+
{
|
|
18
|
+
if (!url) return;
|
|
19
|
+
CFErrorRef err = NULL;
|
|
20
|
+
if (CTFontManagerRegisterFontsForURL((__bridge CFURLRef)url, kCTFontManagerScopeProcess, &err)) return;
|
|
21
|
+
if (!err) return;
|
|
22
|
+
NSError *error = CFBridgingRelease(err);
|
|
23
|
+
if (error.code != kCTFontManagerErrorAlreadyRegistered) {
|
|
24
|
+
NSLog(@"[gea] failed to register font %@: %@", url.lastPathComponent, error.localizedDescription);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
void registerBundledFontsWithExtension(NSString *extension)
|
|
29
|
+
{
|
|
30
|
+
NSArray<NSURL *> *urls = [[NSBundle mainBundle] URLsForResourcesWithExtension:extension subdirectory:nil];
|
|
31
|
+
NSArray<NSURL *> *fontURLs = urls ?: @[];
|
|
32
|
+
for (NSURL *url in fontURLs) registerFontURL(url);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
void ensureBundledFontsRegistered()
|
|
36
|
+
{
|
|
37
|
+
std::call_once(g_registerFontsOnce, [] {
|
|
38
|
+
registerBundledFontsWithExtension(@"ttf");
|
|
39
|
+
registerBundledFontsWithExtension(@"otf");
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
UIFont *fontFromPostScriptNameForFamily(NSString *family, CGFloat size)
|
|
44
|
+
{
|
|
45
|
+
NSDictionary *attrs = @{
|
|
46
|
+
(__bridge NSString *)kCTFontFamilyNameAttribute: family,
|
|
47
|
+
(__bridge NSString *)kCTFontSizeAttribute: @(size),
|
|
48
|
+
};
|
|
49
|
+
CTFontDescriptorRef desc = CTFontDescriptorCreateWithAttributes((__bridge CFDictionaryRef)attrs);
|
|
50
|
+
CTFontRef ct = CTFontCreateWithFontDescriptor(desc, size, NULL);
|
|
51
|
+
CFRelease(desc);
|
|
52
|
+
if (!ct) return nil;
|
|
53
|
+
|
|
54
|
+
CFStringRef psName = CTFontCopyPostScriptName(ct);
|
|
55
|
+
CFRelease(ct);
|
|
56
|
+
if (!psName) return nil;
|
|
57
|
+
|
|
58
|
+
UIFont *font = [UIFont fontWithName:(__bridge NSString *)psName size:size];
|
|
59
|
+
CFRelease(psName);
|
|
60
|
+
return font;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
UIFont *fontForFamily(NSString *family, CGFloat size)
|
|
64
|
+
{
|
|
65
|
+
if (!family || family.length == 0) return nil;
|
|
66
|
+
|
|
67
|
+
for (NSString *fontName in [UIFont fontNamesForFamilyName:family]) {
|
|
68
|
+
UIFont *font = [UIFont fontWithName:fontName size:size];
|
|
69
|
+
if (font) return font;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
UIFont *direct = [UIFont fontWithName:family size:size];
|
|
73
|
+
if (direct) return direct;
|
|
74
|
+
|
|
75
|
+
return fontFromPostScriptNameForFamily(family, size);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
} // namespace
|
|
79
|
+
|
|
80
|
+
UIFont *fontForId(int fontId, CGFloat sizePt)
|
|
81
|
+
{
|
|
82
|
+
const CGFloat size = sizePt > 0 ? sizePt : [UIFont systemFontSize];
|
|
83
|
+
ensureBundledFontsRegistered();
|
|
84
|
+
|
|
85
|
+
const char *familyName = gea::framework::graphics::FontRegistry::familyName(fontId);
|
|
86
|
+
NSString *family = familyName && familyName[0] ? [NSString stringWithUTF8String:familyName] : nil;
|
|
87
|
+
UIFont *font = fontForFamily(family, size);
|
|
88
|
+
return font ?: [UIFont systemFontOfSize:size];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
} // namespace gea::ios
|
|
92
|
+
|
|
93
|
+
extern "C" bool gea_host_measure_text(const char *text,
|
|
94
|
+
int maxWidth,
|
|
95
|
+
int fontId,
|
|
96
|
+
int fontSize,
|
|
97
|
+
int *outWidth,
|
|
98
|
+
int *outHeight)
|
|
99
|
+
{
|
|
100
|
+
if (!outWidth || !outHeight || fontId < 0) return false;
|
|
101
|
+
if (!text || !text[0]) {
|
|
102
|
+
*outWidth = 0;
|
|
103
|
+
*outHeight = 0;
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@autoreleasepool {
|
|
108
|
+
UIFont *font = gea::ios::fontForId(fontId, fontSize);
|
|
109
|
+
if (!font) return false;
|
|
110
|
+
|
|
111
|
+
NSString *str = [NSString stringWithUTF8String:text] ?: @"";
|
|
112
|
+
NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
|
|
113
|
+
paragraph.lineBreakMode = NSLineBreakByWordWrapping;
|
|
114
|
+
NSDictionary *attrs = @{
|
|
115
|
+
NSFontAttributeName: font,
|
|
116
|
+
NSParagraphStyleAttributeName: paragraph,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
|
120
|
+
label.numberOfLines = 0;
|
|
121
|
+
label.lineBreakMode = NSLineBreakByWordWrapping;
|
|
122
|
+
label.attributedText = [[NSAttributedString alloc] initWithString:str attributes:attrs];
|
|
123
|
+
const CGFloat boundedMax = maxWidth > 0 ? static_cast<CGFloat>(maxWidth) : CGFLOAT_MAX;
|
|
124
|
+
const CGSize measured = [label sizeThatFits:CGSizeMake(boundedMax, CGFLOAT_MAX)];
|
|
125
|
+
*outWidth = static_cast<int>(std::ceil(measured.width) + 1.0);
|
|
126
|
+
*outHeight = static_cast<int>(std::ceil(measured.height) + 1.0);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
|
|
3
|
+
#include "image_bridge.h"
|
|
4
|
+
#include "image.h"
|
|
5
|
+
#include "pixel.h"
|
|
6
|
+
|
|
7
|
+
#include <cstring>
|
|
8
|
+
|
|
9
|
+
namespace gea::ios {
|
|
10
|
+
|
|
11
|
+
UIImage *imageForId(int imageId)
|
|
12
|
+
{
|
|
13
|
+
using namespace gea::framework::graphics;
|
|
14
|
+
if (imageId < 0) return nil;
|
|
15
|
+
auto &store = ImageStore::instance();
|
|
16
|
+
const int w = store.width(imageId);
|
|
17
|
+
const int h = store.height(imageId);
|
|
18
|
+
if (w <= 0 || h <= 0) return nil;
|
|
19
|
+
|
|
20
|
+
const std::size_t pixelCount = static_cast<std::size_t>(w) * h;
|
|
21
|
+
NSMutableData *rgba = [NSMutableData dataWithLength:pixelCount * 4];
|
|
22
|
+
std::uint8_t *dst = static_cast<std::uint8_t *>(rgba.mutableBytes);
|
|
23
|
+
|
|
24
|
+
// On iOS pixel::native_t is RGBA8888 (bytes R,G,B,A in memory), so the store's
|
|
25
|
+
// native pixels are already the full-colour source — a straight copy. This is
|
|
26
|
+
// what makes PNG images and camera captures render at full colour, not 16-bit.
|
|
27
|
+
const pixel::native_t *full = store.currentPixels(imageId);
|
|
28
|
+
if (!full) return nil;
|
|
29
|
+
std::memcpy(dst, full, pixelCount * sizeof(pixel::native_t));
|
|
30
|
+
|
|
31
|
+
CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
|
|
32
|
+
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)rgba);
|
|
33
|
+
const uint32_t bitmapInfo = static_cast<uint32_t>(kCGImageAlphaLast) |
|
|
34
|
+
static_cast<uint32_t>(kCGBitmapByteOrder32Big);
|
|
35
|
+
CGImageRef image = CGImageCreate(static_cast<std::size_t>(w),
|
|
36
|
+
static_cast<std::size_t>(h),
|
|
37
|
+
8,
|
|
38
|
+
32,
|
|
39
|
+
static_cast<std::size_t>(w) * 4,
|
|
40
|
+
cs,
|
|
41
|
+
bitmapInfo,
|
|
42
|
+
provider,
|
|
43
|
+
nullptr,
|
|
44
|
+
false,
|
|
45
|
+
kCGRenderingIntentDefault);
|
|
46
|
+
UIImage *result = image ? [UIImage imageWithCGImage:image scale:1.0 orientation:UIImageOrientationUp] : nil;
|
|
47
|
+
if (image) CGImageRelease(image);
|
|
48
|
+
CGDataProviderRelease(provider);
|
|
49
|
+
CGColorSpaceRelease(cs);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
} // namespace gea::ios
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#include <string.h>
|
|
2
|
+
|
|
3
|
+
int gea_embedded_apps_launch(const char *app_id);
|
|
4
|
+
int gea_embedded_apps_return_to_launcher_on_reset(void);
|
|
5
|
+
void gea_embedded_apps_start_launcher_button_task(void);
|
|
6
|
+
const char *gea_embedded_apps_get_current_id(void);
|
|
7
|
+
|
|
8
|
+
static char s_current_app_id[64] = "";
|
|
9
|
+
|
|
10
|
+
int gea_embedded_apps_launch(const char *app_id)
|
|
11
|
+
{
|
|
12
|
+
if (!app_id || !app_id[0]) return 0;
|
|
13
|
+
strncpy(s_current_app_id, app_id, sizeof(s_current_app_id) - 1);
|
|
14
|
+
s_current_app_id[sizeof(s_current_app_id) - 1] = '\0';
|
|
15
|
+
return 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
int gea_embedded_apps_return_to_launcher_on_reset(void)
|
|
19
|
+
{
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
void gea_embedded_apps_start_launcher_button_task(void) {}
|
|
24
|
+
|
|
25
|
+
const char *gea_embedded_apps_get_current_id(void)
|
|
26
|
+
{
|
|
27
|
+
return s_current_app_id;
|
|
28
|
+
}
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// iOS implementation of the cross-platform gea::platform::camera::Camera backend.
|
|
2
|
+
//
|
|
3
|
+
// Preview uses the NativeOverlay model: an AVCaptureVideoPreviewLayer is added
|
|
4
|
+
// as a sublayer of the on-screen gea framebuffer view (gea_ios_camera_host_view)
|
|
5
|
+
// and positioned to the <camera> node's computed rect each frame by the runtime
|
|
6
|
+
// (CameraRenderer -> positionPreviewLayer). A parallel AVCaptureVideoDataOutput
|
|
7
|
+
// keeps the latest frame so capture() can return a still synchronously, matching
|
|
8
|
+
// the other platforms. Video recording is deferred (startRecording returns false).
|
|
9
|
+
//
|
|
10
|
+
// This does NOT touch the existing direct-AVFoundation path used by
|
|
11
|
+
// examples/apps/ios-device-showcase — it's an additive backend so the unified
|
|
12
|
+
// <camera> element / Camera facade work on iOS too.
|
|
13
|
+
|
|
14
|
+
#import <AVFoundation/AVFoundation.h>
|
|
15
|
+
#import <CoreVideo/CoreVideo.h>
|
|
16
|
+
#import <UIKit/UIKit.h>
|
|
17
|
+
|
|
18
|
+
#include "camera.h"
|
|
19
|
+
#include "canvas.h"
|
|
20
|
+
#include "display.h"
|
|
21
|
+
#include "image.h"
|
|
22
|
+
#include "pixel.h"
|
|
23
|
+
|
|
24
|
+
#include <algorithm>
|
|
25
|
+
#include <cstdint>
|
|
26
|
+
#include <cstdlib>
|
|
27
|
+
#include <cstring>
|
|
28
|
+
#include <mutex>
|
|
29
|
+
#include <string>
|
|
30
|
+
#include <vector>
|
|
31
|
+
|
|
32
|
+
namespace pixel = gea::framework::graphics::pixel;
|
|
33
|
+
|
|
34
|
+
// Exposed by ios_main.mm — the UIView hosting the gea framebuffer on screen.
|
|
35
|
+
extern "C" UIView *gea_ios_camera_host_view(void);
|
|
36
|
+
|
|
37
|
+
// Receives video frames so capture() can grab the most recent one synchronously.
|
|
38
|
+
@interface GeaCameraFrameSink : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
|
|
39
|
+
@end
|
|
40
|
+
|
|
41
|
+
namespace gea::platform::camera {
|
|
42
|
+
|
|
43
|
+
namespace {
|
|
44
|
+
|
|
45
|
+
struct IosCameraState {
|
|
46
|
+
AVCaptureSession *session = nil;
|
|
47
|
+
AVCaptureDevice *device = nil;
|
|
48
|
+
AVCaptureDeviceInput *input = nil;
|
|
49
|
+
AVCaptureVideoDataOutput *dataOutput = nil;
|
|
50
|
+
AVCaptureVideoPreviewLayer *previewLayer = nil;
|
|
51
|
+
GeaCameraFrameSink *frameSink = nil;
|
|
52
|
+
dispatch_queue_t sampleQueue = nullptr;
|
|
53
|
+
|
|
54
|
+
bool open = false;
|
|
55
|
+
bool mirror = false;
|
|
56
|
+
int width = 0;
|
|
57
|
+
int height = 0;
|
|
58
|
+
Facing facing = Facing::Back;
|
|
59
|
+
|
|
60
|
+
// Latest frame as full-colour RGBA8888 (bytes R,G,B,A), guarded for
|
|
61
|
+
// synchronous capture().
|
|
62
|
+
std::mutex frameMutex;
|
|
63
|
+
std::vector<std::uint32_t> latestFrame;
|
|
64
|
+
int latestWidth = 0;
|
|
65
|
+
int latestHeight = 0;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
IosCameraState &state()
|
|
69
|
+
{
|
|
70
|
+
static IosCameraState s;
|
|
71
|
+
return s;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
AVCaptureDevicePosition positionForHint(const std::string &hint, Facing &facingOut)
|
|
75
|
+
{
|
|
76
|
+
if (hint == "front") {
|
|
77
|
+
facingOut = Facing::Front;
|
|
78
|
+
return AVCaptureDevicePositionFront;
|
|
79
|
+
}
|
|
80
|
+
facingOut = Facing::Back;
|
|
81
|
+
return AVCaptureDevicePositionBack;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
void storeLatestFrame(CVImageBufferRef imageBuffer)
|
|
85
|
+
{
|
|
86
|
+
if (!imageBuffer) return;
|
|
87
|
+
CVPixelBufferLockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
|
88
|
+
const int w = static_cast<int>(CVPixelBufferGetWidth(imageBuffer));
|
|
89
|
+
const int h = static_cast<int>(CVPixelBufferGetHeight(imageBuffer));
|
|
90
|
+
const std::uint8_t *base = static_cast<const std::uint8_t *>(CVPixelBufferGetBaseAddress(imageBuffer));
|
|
91
|
+
const std::size_t stride = CVPixelBufferGetBytesPerRow(imageBuffer);
|
|
92
|
+
if (base && w > 0 && h > 0) {
|
|
93
|
+
auto &s = state();
|
|
94
|
+
std::lock_guard<std::mutex> lock(s.frameMutex);
|
|
95
|
+
s.latestFrame.resize(static_cast<std::size_t>(w) * h);
|
|
96
|
+
s.latestWidth = w;
|
|
97
|
+
s.latestHeight = h;
|
|
98
|
+
// AVCaptureVideoDataOutput is configured for 32BGRA (bytes B,G,R,A);
|
|
99
|
+
// pack to full-colour RGBA8888 (bytes R,G,B,A) — no 565 quantization.
|
|
100
|
+
for (int y = 0; y < h; ++y) {
|
|
101
|
+
const std::uint8_t *srcRow = base + static_cast<std::size_t>(y) * stride;
|
|
102
|
+
std::uint32_t *dstRow = s.latestFrame.data() + static_cast<std::size_t>(y) * w;
|
|
103
|
+
for (int x = 0; x < w; ++x) {
|
|
104
|
+
const std::uint8_t *p = srcRow + static_cast<std::size_t>(x) * 4;
|
|
105
|
+
dstRow[x] = pixel::packRgba8888(p[2], p[1], p[0], 255); // B,G,R,A -> R,G,B,A
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
s.width = w;
|
|
109
|
+
s.height = h;
|
|
110
|
+
}
|
|
111
|
+
CVPixelBufferUnlockBaseAddress(imageBuffer, kCVPixelBufferLock_ReadOnly);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
CGRect canvasRectToViewPoints(UIView *host, int x, int y, int w, int h)
|
|
115
|
+
{
|
|
116
|
+
auto *canvas = gea::platform::display::Display::canvas();
|
|
117
|
+
const CGRect bounds = host.bounds;
|
|
118
|
+
if (!canvas || canvas->width() <= 0 || canvas->height() <= 0) return bounds;
|
|
119
|
+
const CGFloat scale = std::min(bounds.size.width / static_cast<CGFloat>(canvas->width()),
|
|
120
|
+
bounds.size.height / static_cast<CGFloat>(canvas->height()));
|
|
121
|
+
const CGFloat destW = static_cast<CGFloat>(canvas->width()) * scale;
|
|
122
|
+
const CGFloat destH = static_cast<CGFloat>(canvas->height()) * scale;
|
|
123
|
+
const CGFloat originX = bounds.origin.x + (bounds.size.width - destW) * 0.5;
|
|
124
|
+
const CGFloat originY = bounds.origin.y + (bounds.size.height - destH) * 0.5;
|
|
125
|
+
return CGRectMake(originX + static_cast<CGFloat>(x) * scale,
|
|
126
|
+
originY + static_cast<CGFloat>(y) * scale,
|
|
127
|
+
static_cast<CGFloat>(w) * scale,
|
|
128
|
+
static_cast<CGFloat>(h) * scale);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
bool withLockedConfig(void (^body)(AVCaptureDevice *))
|
|
132
|
+
{
|
|
133
|
+
AVCaptureDevice *device = state().device;
|
|
134
|
+
if (!device) return false;
|
|
135
|
+
NSError *error = nil;
|
|
136
|
+
if (![device lockForConfiguration:&error]) return false;
|
|
137
|
+
body(device);
|
|
138
|
+
[device unlockForConfiguration];
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
} // namespace
|
|
143
|
+
|
|
144
|
+
bool Camera::isAvailable()
|
|
145
|
+
{
|
|
146
|
+
return [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo] != nil;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
bool Camera::hasPermission()
|
|
150
|
+
{
|
|
151
|
+
return [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusAuthorized;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
bool Camera::requestPermission()
|
|
155
|
+
{
|
|
156
|
+
// Synchronous facade: kick off the prompt; callers poll hasPermission().
|
|
157
|
+
if ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo] == AVAuthorizationStatusNotDetermined) {
|
|
158
|
+
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL){
|
|
159
|
+
}];
|
|
160
|
+
}
|
|
161
|
+
return hasPermission();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
bool Camera::open(const std::string &facingHint, int preferredWidth, int preferredHeight)
|
|
165
|
+
{
|
|
166
|
+
(void)preferredWidth;
|
|
167
|
+
(void)preferredHeight;
|
|
168
|
+
close();
|
|
169
|
+
auto &s = state();
|
|
170
|
+
|
|
171
|
+
Facing facing = Facing::Back;
|
|
172
|
+
const AVCaptureDevicePosition position = positionForHint(facingHint, facing);
|
|
173
|
+
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera
|
|
174
|
+
mediaType:AVMediaTypeVideo
|
|
175
|
+
position:position];
|
|
176
|
+
if (!device) device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
|
|
177
|
+
if (!device) return false;
|
|
178
|
+
|
|
179
|
+
NSError *error = nil;
|
|
180
|
+
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:device error:&error];
|
|
181
|
+
if (!input) return false;
|
|
182
|
+
|
|
183
|
+
AVCaptureSession *session = [[AVCaptureSession alloc] init];
|
|
184
|
+
if ([session canSetSessionPreset:AVCaptureSessionPresetHigh]) session.sessionPreset = AVCaptureSessionPresetHigh;
|
|
185
|
+
if (![session canAddInput:input]) return false;
|
|
186
|
+
[session addInput:input];
|
|
187
|
+
|
|
188
|
+
AVCaptureVideoDataOutput *dataOutput = [[AVCaptureVideoDataOutput alloc] init];
|
|
189
|
+
dataOutput.videoSettings = @{ (id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA) };
|
|
190
|
+
dataOutput.alwaysDiscardsLateVideoFrames = YES;
|
|
191
|
+
GeaCameraFrameSink *sink = [[GeaCameraFrameSink alloc] init];
|
|
192
|
+
dispatch_queue_t queue = dispatch_queue_create("com.gea.camera.frames", DISPATCH_QUEUE_SERIAL);
|
|
193
|
+
[dataOutput setSampleBufferDelegate:sink queue:queue];
|
|
194
|
+
if ([session canAddOutput:dataOutput]) [session addOutput:dataOutput];
|
|
195
|
+
|
|
196
|
+
AVCaptureVideoPreviewLayer *previewLayer = [AVCaptureVideoPreviewLayer layerWithSession:session];
|
|
197
|
+
previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;
|
|
198
|
+
|
|
199
|
+
s.session = session;
|
|
200
|
+
s.device = device;
|
|
201
|
+
s.input = input;
|
|
202
|
+
s.dataOutput = dataOutput;
|
|
203
|
+
s.frameSink = sink;
|
|
204
|
+
s.sampleQueue = queue;
|
|
205
|
+
s.previewLayer = previewLayer;
|
|
206
|
+
s.facing = facing;
|
|
207
|
+
s.mirror = (facing == Facing::Front);
|
|
208
|
+
s.open = true;
|
|
209
|
+
|
|
210
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
211
|
+
UIView *host = gea_ios_camera_host_view();
|
|
212
|
+
if (host && state().previewLayer) {
|
|
213
|
+
state().previewLayer.frame = host.bounds;
|
|
214
|
+
[host.layer addSublayer:state().previewLayer];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
[session startRunning];
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
void Camera::close()
|
|
222
|
+
{
|
|
223
|
+
auto &s = state();
|
|
224
|
+
if (s.session) [s.session stopRunning];
|
|
225
|
+
AVCaptureVideoPreviewLayer *layer = s.previewLayer;
|
|
226
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
227
|
+
[layer removeFromSuperlayer];
|
|
228
|
+
});
|
|
229
|
+
s.session = nil;
|
|
230
|
+
s.device = nil;
|
|
231
|
+
s.input = nil;
|
|
232
|
+
s.dataOutput = nil;
|
|
233
|
+
s.frameSink = nil;
|
|
234
|
+
s.sampleQueue = nullptr;
|
|
235
|
+
s.previewLayer = nil;
|
|
236
|
+
s.open = false;
|
|
237
|
+
{
|
|
238
|
+
std::lock_guard<std::mutex> lock(s.frameMutex);
|
|
239
|
+
s.latestFrame.clear();
|
|
240
|
+
s.latestWidth = 0;
|
|
241
|
+
s.latestHeight = 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
bool Camera::isOpen() { return state().open; }
|
|
246
|
+
int Camera::width() { return state().width; }
|
|
247
|
+
int Camera::height() { return state().height; }
|
|
248
|
+
int Camera::orientation() { return 0; }
|
|
249
|
+
Facing Camera::currentFacing() { return state().facing; }
|
|
250
|
+
|
|
251
|
+
std::string Camera::currentFacingString()
|
|
252
|
+
{
|
|
253
|
+
switch (state().facing) {
|
|
254
|
+
case Facing::Front: return "front";
|
|
255
|
+
case Facing::External: return "external";
|
|
256
|
+
case Facing::Back:
|
|
257
|
+
default: return "back";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
int Camera::deviceCount()
|
|
262
|
+
{
|
|
263
|
+
int count = 0;
|
|
264
|
+
if ([AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera
|
|
265
|
+
mediaType:AVMediaTypeVideo
|
|
266
|
+
position:AVCaptureDevicePositionBack]) {
|
|
267
|
+
++count;
|
|
268
|
+
}
|
|
269
|
+
if ([AVCaptureDevice defaultDeviceWithDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera
|
|
270
|
+
mediaType:AVMediaTypeVideo
|
|
271
|
+
position:AVCaptureDevicePositionFront]) {
|
|
272
|
+
++count;
|
|
273
|
+
}
|
|
274
|
+
return count > 0 ? count : 1;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
DeviceInfo Camera::deviceAt(int index)
|
|
278
|
+
{
|
|
279
|
+
if (index == 1) return {"front", Facing::Front, 0, 0};
|
|
280
|
+
return {"back", Facing::Back, 0, 0};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
int Camera::previewMode() { return 1; } // NativeOverlay
|
|
284
|
+
|
|
285
|
+
bool Camera::fillPreview(gea::framework::graphics::pixel::native_t *, int, int, int, bool)
|
|
286
|
+
{
|
|
287
|
+
// NativeOverlay: the AVCaptureVideoPreviewLayer renders the preview; the
|
|
288
|
+
// runtime never asks us to fill a framebuffer.
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// NativeOverlay mode: the AVCaptureVideoPreviewLayer is positioned/composited by
|
|
293
|
+
// the iOS renderer, so there is no framework-driven overlay present step here.
|
|
294
|
+
void Camera::presentNativeOverlay() {}
|
|
295
|
+
|
|
296
|
+
void Camera::positionPreviewLayer(int x, int y, int width, int height)
|
|
297
|
+
{
|
|
298
|
+
AVCaptureVideoPreviewLayer *layer = state().previewLayer;
|
|
299
|
+
if (!layer) return;
|
|
300
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
301
|
+
UIView *host = gea_ios_camera_host_view();
|
|
302
|
+
if (!host) return;
|
|
303
|
+
if (layer.superlayer != host.layer) [host.layer addSublayer:layer];
|
|
304
|
+
[CATransaction begin];
|
|
305
|
+
[CATransaction setDisableActions:YES];
|
|
306
|
+
layer.hidden = NO;
|
|
307
|
+
layer.frame = canvasRectToViewPoints(host, x, y, width, height);
|
|
308
|
+
[CATransaction commit];
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
void Camera::hidePreviewLayer()
|
|
313
|
+
{
|
|
314
|
+
AVCaptureVideoPreviewLayer *layer = state().previewLayer;
|
|
315
|
+
if (!layer) return;
|
|
316
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
317
|
+
layer.hidden = YES;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
int Camera::capture(bool mirror)
|
|
322
|
+
{
|
|
323
|
+
auto &s = state();
|
|
324
|
+
std::lock_guard<std::mutex> lock(s.frameMutex);
|
|
325
|
+
if (s.latestFrame.empty() || s.latestWidth <= 0 || s.latestHeight <= 0) return -1;
|
|
326
|
+
const int w = s.latestWidth;
|
|
327
|
+
const int h = s.latestHeight;
|
|
328
|
+
const bool mir = mirror || s.mirror;
|
|
329
|
+
auto *pixels = static_cast<std::uint32_t *>(std::malloc(sizeof(std::uint32_t) * w * h));
|
|
330
|
+
if (!pixels) return -1;
|
|
331
|
+
for (int y = 0; y < h; ++y) {
|
|
332
|
+
const std::uint32_t *src = s.latestFrame.data() + static_cast<std::size_t>(y) * w;
|
|
333
|
+
std::uint32_t *dst = pixels + static_cast<std::size_t>(y) * w;
|
|
334
|
+
if (mir) {
|
|
335
|
+
for (int x = 0; x < w; ++x) dst[x] = src[w - 1 - x];
|
|
336
|
+
} else {
|
|
337
|
+
std::memcpy(dst, src, sizeof(std::uint32_t) * w);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// pixels is RGBA8888, which is exactly pixel::native_t on iOS — register it as
|
|
341
|
+
// a native buffer and transfer ownership to the store (freed on dispose).
|
|
342
|
+
const int id = gea::framework::graphics::ImageStore::instance().registerBuffer(pixels, w, h, -1, /*takeOwnership=*/true);
|
|
343
|
+
if (id < 0) std::free(pixels);
|
|
344
|
+
return id;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
bool Camera::startRecording(const std::string &, double) { return false; } // movie: later
|
|
348
|
+
double Camera::stopRecording() { return -1.0; }
|
|
349
|
+
bool Camera::isRecording() { return false; }
|
|
350
|
+
|
|
351
|
+
void Camera::setFlash(const std::string &mode) { setTorch(mode, 0.0); }
|
|
352
|
+
|
|
353
|
+
void Camera::setZoom(double factor)
|
|
354
|
+
{
|
|
355
|
+
withLockedConfig(^(AVCaptureDevice *device) {
|
|
356
|
+
const CGFloat clamped = std::max<CGFloat>(1.0, std::min<CGFloat>(static_cast<CGFloat>(factor),
|
|
357
|
+
device.activeFormat.videoMaxZoomFactor));
|
|
358
|
+
device.videoZoomFactor = clamped;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
void Camera::setMirror(bool mirror) { state().mirror = mirror; }
|
|
363
|
+
|
|
364
|
+
void Camera::setExposure(const std::string &mode, double bias, double iso, double durationMs)
|
|
365
|
+
{
|
|
366
|
+
(void)iso;
|
|
367
|
+
(void)durationMs;
|
|
368
|
+
withLockedConfig(^(AVCaptureDevice *device) {
|
|
369
|
+
if (mode == "locked" || mode == "manual") {
|
|
370
|
+
if ([device isExposureModeSupported:AVCaptureExposureModeLocked]) device.exposureMode = AVCaptureExposureModeLocked;
|
|
371
|
+
} else if ([device isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) {
|
|
372
|
+
device.exposureMode = AVCaptureExposureModeContinuousAutoExposure;
|
|
373
|
+
}
|
|
374
|
+
if (bias != 0.0) {
|
|
375
|
+
const float clamped = std::max(device.minExposureTargetBias,
|
|
376
|
+
std::min(device.maxExposureTargetBias, static_cast<float>(bias)));
|
|
377
|
+
[device setExposureTargetBias:clamped completionHandler:nil];
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
void Camera::setWhiteBalance(const std::string &mode, double temperatureK, double tint)
|
|
383
|
+
{
|
|
384
|
+
withLockedConfig(^(AVCaptureDevice *device) {
|
|
385
|
+
if (mode == "locked") {
|
|
386
|
+
if (temperatureK > 0.0 &&
|
|
387
|
+
[device isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeLocked]) {
|
|
388
|
+
AVCaptureWhiteBalanceTemperatureAndTintValues values;
|
|
389
|
+
values.temperature = static_cast<float>(temperatureK);
|
|
390
|
+
values.tint = static_cast<float>(tint);
|
|
391
|
+
AVCaptureWhiteBalanceGains gains = [device deviceWhiteBalanceGainsForTemperatureAndTintValues:values];
|
|
392
|
+
const CGFloat maxGain = device.maxWhiteBalanceGain;
|
|
393
|
+
gains.redGain = std::max<CGFloat>(1.0, std::min<CGFloat>(gains.redGain, maxGain));
|
|
394
|
+
gains.greenGain = std::max<CGFloat>(1.0, std::min<CGFloat>(gains.greenGain, maxGain));
|
|
395
|
+
gains.blueGain = std::max<CGFloat>(1.0, std::min<CGFloat>(gains.blueGain, maxGain));
|
|
396
|
+
[device setWhiteBalanceModeLockedWithDeviceWhiteBalanceGains:gains completionHandler:nil];
|
|
397
|
+
} else if ([device isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeLocked]) {
|
|
398
|
+
device.whiteBalanceMode = AVCaptureWhiteBalanceModeLocked;
|
|
399
|
+
}
|
|
400
|
+
} else if ([device isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance]) {
|
|
401
|
+
device.whiteBalanceMode = AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
void Camera::setFocus(const std::string &mode, double pointX, double pointY)
|
|
407
|
+
{
|
|
408
|
+
withLockedConfig(^(AVCaptureDevice *device) {
|
|
409
|
+
if ((pointX != 0.0 || pointY != 0.0) && device.isFocusPointOfInterestSupported) {
|
|
410
|
+
device.focusPointOfInterest = CGPointMake(static_cast<CGFloat>(pointX), static_cast<CGFloat>(pointY));
|
|
411
|
+
}
|
|
412
|
+
if (mode == "locked") {
|
|
413
|
+
if ([device isFocusModeSupported:AVCaptureFocusModeLocked]) device.focusMode = AVCaptureFocusModeLocked;
|
|
414
|
+
} else if ([device isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {
|
|
415
|
+
device.focusMode = AVCaptureFocusModeContinuousAutoFocus;
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
void Camera::setTorch(const std::string &mode, double level)
|
|
421
|
+
{
|
|
422
|
+
withLockedConfig(^(AVCaptureDevice *device) {
|
|
423
|
+
if (!device.hasTorch) return;
|
|
424
|
+
if (mode == "on") {
|
|
425
|
+
const float lvl = level > 0.0 ? static_cast<float>(level) : AVCaptureMaxAvailableTorchLevel;
|
|
426
|
+
[device setTorchModeOnWithLevel:lvl error:nil];
|
|
427
|
+
} else if (mode == "auto") {
|
|
428
|
+
if ([device isTorchModeSupported:AVCaptureTorchModeAuto]) device.torchMode = AVCaptureTorchModeAuto;
|
|
429
|
+
} else {
|
|
430
|
+
device.torchMode = AVCaptureTorchModeOff;
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
} // namespace gea::platform::camera
|
|
436
|
+
|
|
437
|
+
@implementation GeaCameraFrameSink
|
|
438
|
+
- (void)captureOutput:(AVCaptureOutput *)output
|
|
439
|
+
didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
|
|
440
|
+
fromConnection:(AVCaptureConnection *)connection
|
|
441
|
+
{
|
|
442
|
+
(void)output;
|
|
443
|
+
(void)connection;
|
|
444
|
+
gea::platform::camera::storeLatestFrame(CMSampleBufferGetImageBuffer(sampleBuffer));
|
|
445
|
+
}
|
|
446
|
+
@end
|