mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-09-02 02:34:29 +00:00
camera: vphoned guest-side vcam vsock listener + shm writer
Guest-side counterpart to VPhoneCameraServer. vphoned runs inside
the VM and now hosts an extra vsock listener on port 1338 that
receives BGRA frames from the host and writes them into a memory-
mapped shm region at
/var/jb/var/mobile/Library/vphone-vcam-frame.shm with a packed
header (seq monotonic, width/height, bytes_per_row, pixel_format,
timestamp_ns, frame_index, pixels_length).
vphoned_vcam.h packed shm header layout + filename const.
vphoned_vcam.m VPVcamServer: accept loop, header parse,
seq-bump write, file-based debug log for
first-boot post-mortem.
vphoned.m Boot the vcam server alongside the existing
port-1337 control daemon.
Pairs with the host VPhoneCameraServer (1338 client) and the guest
libvcamcaptured (shm reader) added in the following commits.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
committed by
zqxwce
co-authored by
Claude Opus 4.7
parent
0223ad139c
commit
c8de9c9cb7
@@ -37,6 +37,7 @@
|
||||
#import "vphoned_protocol.h"
|
||||
#import "vphoned_settings.h"
|
||||
#import "vphoned_url.h"
|
||||
#import "vphoned_vcam.h"
|
||||
|
||||
#ifndef AF_VSOCK
|
||||
#define AF_VSOCK 40
|
||||
@@ -479,6 +480,7 @@ int main(int argc, char *argv[]) {
|
||||
|
||||
gClipboardAvailable = vp_clipboard_load();
|
||||
gAppsAvailable = vp_apps_load();
|
||||
vp_vcam_start();
|
||||
|
||||
int sock = socket(AF_VSOCK, SOCK_STREAM, 0);
|
||||
if (sock < 0) {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* vphoned_vcam — receive virtual-camera frames over vsock and publish
|
||||
* them into a shared-memory file that libvcamcaptured (inside
|
||||
* cameracaptured) maps for read access.
|
||||
*
|
||||
* vphoned runs as root and has AF_VSOCK access; the cameracaptured
|
||||
* sandbox does not, so libvcamcaptured can't open its own vsock socket.
|
||||
* Putting the listener here is the cleanest workaround.
|
||||
*/
|
||||
|
||||
#ifndef VPHONED_VCAM_H
|
||||
#define VPHONED_VCAM_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Path of the shared frame file (read by libvcamcaptured).
|
||||
* Lives under /var/jb so cameracaptured's sandbox allows reads.
|
||||
*/
|
||||
#ifndef VPHONED_VCAM_SHM_PATH
|
||||
#define VPHONED_VCAM_SHM_PATH \
|
||||
"/var/jb/var/mobile/Library/vphone-vcam-frame.shm"
|
||||
#endif
|
||||
#ifndef VPHONED_VCAM_VSOCK_PORT
|
||||
#define VPHONED_VCAM_VSOCK_PORT 1338
|
||||
#endif
|
||||
#ifndef VPHONED_VCAM_NOTIFY_NAME
|
||||
#define VPHONED_VCAM_NOTIFY_NAME "com.vphone.vcam.frame"
|
||||
#endif
|
||||
|
||||
/* Shared-memory layout. seq increments by 2 per frame; odd values mean
|
||||
* a write is in progress. Readers re-check seq after copying to detect
|
||||
* mid-write tearing. */
|
||||
typedef struct __attribute__((packed)) {
|
||||
uint64_t seq;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t bytes_per_row;
|
||||
uint32_t pixel_format; /* 4cc */
|
||||
uint32_t _reserved;
|
||||
uint64_t timestamp_ns;
|
||||
uint64_t frame_index;
|
||||
uint32_t pixels_length;
|
||||
uint32_t _pad;
|
||||
/* pixels start at offset 64; capacity = total mmap size - 64. */
|
||||
} vphoned_vcam_shm_header_t;
|
||||
|
||||
#define VPHONED_VCAM_SHM_HEADER_SIZE 64
|
||||
#define VPHONED_VCAM_SHM_MAX_PIXELS (8 * 1024 * 1024) /* 8 MiB */
|
||||
#define VPHONED_VCAM_SHM_TOTAL_SIZE \
|
||||
(VPHONED_VCAM_SHM_HEADER_SIZE + VPHONED_VCAM_SHM_MAX_PIXELS)
|
||||
|
||||
/* Starts the listener on a background thread. Idempotent. */
|
||||
void vp_vcam_start(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* VPHONED_VCAM_H */
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* vphoned_vcam — vsock 1338 -> shared mmap frame publisher.
|
||||
*
|
||||
* Wire protocol (matches host VPhoneCameraServer.swift):
|
||||
* uint32 LE total_payload_length
|
||||
* uint32 LE header_json_length
|
||||
* bytes JSON header { w, h, bpr, fmt, ts }
|
||||
* bytes raw pixel data (width*height aligned by bpr)
|
||||
*
|
||||
* On each successful receive, the frame is written into the shm file
|
||||
* with a seq-counter discipline and a notify_post() fires so any
|
||||
* libvcamcaptured-mapped reader can pick it up immediately.
|
||||
*/
|
||||
|
||||
#import "vphoned_vcam.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <notify.h>
|
||||
#include <pthread.h>
|
||||
#include <stdatomic.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef AF_VSOCK
|
||||
#define AF_VSOCK 40
|
||||
#endif
|
||||
#ifndef VMADDR_CID_ANY
|
||||
#define VMADDR_CID_ANY 0xFFFFFFFFu
|
||||
#endif
|
||||
|
||||
struct vp_sockaddr_vm {
|
||||
__uint8_t svm_len;
|
||||
sa_family_t svm_family;
|
||||
__uint16_t svm_reserved1;
|
||||
__uint32_t svm_port;
|
||||
__uint32_t svm_cid;
|
||||
};
|
||||
|
||||
static pthread_once_t s_start_once = PTHREAD_ONCE_INIT;
|
||||
static uint8_t *s_shm_base = NULL;
|
||||
static int s_notify_token = -1;
|
||||
|
||||
#define VVC_LOG_PATH "/var/jb/var/mobile/Library/vphone-vcam.log"
|
||||
|
||||
__attribute__((format(printf, 1, 2)))
|
||||
static void vvc_logf(const char *fmt, ...) {
|
||||
FILE *fp = fopen(VVC_LOG_PATH, "a");
|
||||
if (!fp) return;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
vfprintf(fp, fmt, ap);
|
||||
va_end(ap);
|
||||
fputc('\n', fp);
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
static int open_shm(void) {
|
||||
/* Truncate to total size each fresh open so a stale half-written file
|
||||
* from a previous boot doesn't confuse readers. */
|
||||
int fd = open(VPHONED_VCAM_SHM_PATH, O_RDWR | O_CREAT, 0644);
|
||||
if (fd < 0) {
|
||||
vvc_logf("vphoned_vcam: open(%s) failed: %s",
|
||||
VPHONED_VCAM_SHM_PATH, strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
if (ftruncate(fd, VPHONED_VCAM_SHM_TOTAL_SIZE) < 0) {
|
||||
vvc_logf("vphoned_vcam: ftruncate failed: %s", strerror(errno));
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
/* Make sure other processes can map this file read-only. */
|
||||
fchmod(fd, 0644);
|
||||
void *base = mmap(NULL, VPHONED_VCAM_SHM_TOTAL_SIZE,
|
||||
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||
close(fd);
|
||||
if (base == MAP_FAILED) {
|
||||
vvc_logf("vphoned_vcam: mmap failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
/* Zero the header on first init so seq starts at 0. */
|
||||
memset(base, 0, VPHONED_VCAM_SHM_HEADER_SIZE);
|
||||
s_shm_base = (uint8_t *)base;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static ssize_t read_full(int fd, void *buf, size_t n) {
|
||||
uint8_t *p = (uint8_t *)buf;
|
||||
size_t got = 0;
|
||||
while (got < n) {
|
||||
ssize_t r = read(fd, p + got, n - got);
|
||||
if (r > 0) { got += (size_t)r; continue; }
|
||||
if (r == 0) return 0;
|
||||
if (errno == EINTR) continue;
|
||||
return -1;
|
||||
}
|
||||
return (ssize_t)got;
|
||||
}
|
||||
|
||||
static void publish_frame(uint32_t w, uint32_t h, uint32_t bpr,
|
||||
uint32_t fmt, uint64_t ts_ns,
|
||||
const uint8_t *pixels, size_t pixel_len) {
|
||||
if (!s_shm_base) return;
|
||||
if (pixel_len > VPHONED_VCAM_SHM_MAX_PIXELS) {
|
||||
vvc_logf("vphoned_vcam: frame too large: %zu", pixel_len);
|
||||
return;
|
||||
}
|
||||
vphoned_vcam_shm_header_t *hdr = (vphoned_vcam_shm_header_t *)s_shm_base;
|
||||
uint8_t *dst = s_shm_base + VPHONED_VCAM_SHM_HEADER_SIZE;
|
||||
|
||||
uint64_t prev_seq = atomic_load_explicit(
|
||||
(_Atomic uint64_t *)&hdr->seq, memory_order_acquire);
|
||||
/* Mark write in progress (odd seq). */
|
||||
uint64_t writing_seq = (prev_seq | 1ull) + 2ull;
|
||||
atomic_store_explicit((_Atomic uint64_t *)&hdr->seq, writing_seq,
|
||||
memory_order_release);
|
||||
|
||||
hdr->width = w;
|
||||
hdr->height = h;
|
||||
hdr->bytes_per_row = bpr;
|
||||
hdr->pixel_format = fmt;
|
||||
hdr->timestamp_ns = ts_ns;
|
||||
hdr->frame_index += 1;
|
||||
hdr->pixels_length = (uint32_t)pixel_len;
|
||||
memcpy(dst, pixels, pixel_len);
|
||||
|
||||
/* Mark write done (even seq). */
|
||||
atomic_store_explicit((_Atomic uint64_t *)&hdr->seq, writing_seq + 1ull,
|
||||
memory_order_release);
|
||||
|
||||
if (s_notify_token >= 0) {
|
||||
notify_post(VPHONED_VCAM_NOTIFY_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_client(int fd) {
|
||||
vvc_logf("vphoned_vcam: client connected fd=%d", fd);
|
||||
uint64_t frames = 0;
|
||||
for (;;) {
|
||||
uint32_t total_len = 0, header_len = 0;
|
||||
if (read_full(fd, &total_len, 4) <= 0) break;
|
||||
if (read_full(fd, &header_len, 4) <= 0) break;
|
||||
if (total_len < 4 || header_len + 4 > total_len ||
|
||||
total_len > VPHONED_VCAM_SHM_MAX_PIXELS + 4096) {
|
||||
vvc_logf("vphoned_vcam: framing error total=%u header=%u",
|
||||
total_len, header_len);
|
||||
break;
|
||||
}
|
||||
uint8_t *header_buf = (uint8_t *)malloc(header_len);
|
||||
if (!header_buf) break;
|
||||
if (read_full(fd, header_buf, header_len) <= 0) {
|
||||
free(header_buf);
|
||||
break;
|
||||
}
|
||||
size_t pixel_len = (size_t)total_len - 4 - header_len;
|
||||
uint8_t *pixel_buf = (uint8_t *)malloc(pixel_len);
|
||||
if (!pixel_buf) { free(header_buf); break; }
|
||||
if (read_full(fd, pixel_buf, pixel_len) <= 0) {
|
||||
free(header_buf);
|
||||
free(pixel_buf);
|
||||
break;
|
||||
}
|
||||
NSData *hd = [NSData dataWithBytesNoCopy:header_buf
|
||||
length:header_len
|
||||
freeWhenDone:NO];
|
||||
NSError *jerr = nil;
|
||||
NSDictionary *hdict = [NSJSONSerialization JSONObjectWithData:hd
|
||||
options:0
|
||||
error:&jerr];
|
||||
uint32_t w = (uint32_t)[hdict[@"w"] unsignedIntValue];
|
||||
uint32_t h = (uint32_t)[hdict[@"h"] unsignedIntValue];
|
||||
uint32_t bpr = (uint32_t)[hdict[@"bpr"] unsignedIntValue];
|
||||
uint32_t fmt = (uint32_t)[hdict[@"fmt"] unsignedIntValue];
|
||||
uint64_t ts = (uint64_t)[hdict[@"ts"] unsignedLongLongValue];
|
||||
free(header_buf);
|
||||
|
||||
if (!hdict || jerr || w == 0 || h == 0 || bpr == 0 ||
|
||||
pixel_len < (size_t)bpr * h) {
|
||||
vvc_logf("vphoned_vcam: invalid frame w=%u h=%u bpr=%u pixel_len=%zu jerr=%s",
|
||||
w, h, bpr, pixel_len,
|
||||
jerr ? jerr.localizedDescription.UTF8String : "(none)");
|
||||
free(pixel_buf);
|
||||
break;
|
||||
}
|
||||
publish_frame(w, h, bpr, fmt, ts, pixel_buf, pixel_len);
|
||||
free(pixel_buf);
|
||||
|
||||
frames++;
|
||||
if ((frames & 29) == 1) {
|
||||
vvc_logf("vphoned_vcam: published frame #%llu w=%u h=%u bpr=%u",
|
||||
(unsigned long long)frames, w, h, bpr);
|
||||
}
|
||||
}
|
||||
vvc_logf("vphoned_vcam: client disconnected (%llu frames)",
|
||||
(unsigned long long)frames);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
static void *listener_thread(__unused void *unused) {
|
||||
if (open_shm() < 0) return NULL;
|
||||
|
||||
/* Register the notify name so notify_post() actually delivers. */
|
||||
if (notify_register_check(VPHONED_VCAM_NOTIFY_NAME,
|
||||
&s_notify_token) != NOTIFY_STATUS_OK) {
|
||||
s_notify_token = -1;
|
||||
}
|
||||
|
||||
int srv = socket(AF_VSOCK, SOCK_STREAM, 0);
|
||||
if (srv < 0) {
|
||||
vvc_logf("vphoned_vcam: socket(AF_VSOCK) failed: %s", strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
int one = 1;
|
||||
setsockopt(srv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
|
||||
struct vp_sockaddr_vm addr = {
|
||||
.svm_len = sizeof(addr),
|
||||
.svm_family = AF_VSOCK,
|
||||
.svm_port = VPHONED_VCAM_VSOCK_PORT,
|
||||
.svm_cid = VMADDR_CID_ANY,
|
||||
};
|
||||
if (bind(srv, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
vvc_logf("vphoned_vcam: bind(%d) failed: %s",
|
||||
VPHONED_VCAM_VSOCK_PORT, strerror(errno));
|
||||
close(srv);
|
||||
return NULL;
|
||||
}
|
||||
if (listen(srv, 2) < 0) {
|
||||
vvc_logf("vphoned_vcam: listen failed: %s", strerror(errno));
|
||||
close(srv);
|
||||
return NULL;
|
||||
}
|
||||
vvc_logf("vphoned_vcam: listening on vsock %d, shm=%s",
|
||||
VPHONED_VCAM_VSOCK_PORT, VPHONED_VCAM_SHM_PATH);
|
||||
|
||||
for (;;) {
|
||||
int fd = accept(srv, NULL, NULL);
|
||||
if (fd < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
vvc_logf("vphoned_vcam: accept failed: %s", strerror(errno));
|
||||
sleep(1);
|
||||
continue;
|
||||
}
|
||||
@autoreleasepool {
|
||||
handle_client(fd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void start_listener_once(void) {
|
||||
pthread_t thr;
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
int rc = pthread_create(&thr, &attr, listener_thread, NULL);
|
||||
pthread_attr_destroy(&attr);
|
||||
if (rc != 0) {
|
||||
vvc_logf("vphoned_vcam: pthread_create failed: %d", rc);
|
||||
}
|
||||
}
|
||||
|
||||
void vp_vcam_start(void) {
|
||||
vvc_logf("vphoned_vcam: vp_vcam_start called (pid=%d)", getpid());
|
||||
pthread_once(&s_start_once, start_listener_once);
|
||||
}
|
||||
Reference in New Issue
Block a user