ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
webcam_wasm.c
Go to the documentation of this file.
1
16
17// clang-format off
18
19#ifdef __EMSCRIPTEN__
20
21#include <emscripten.h>
22#include <stdio.h>
23#include <string.h>
26#include <libplatform/compiler.h>
27#include <libplatform/system.h>
28#include <libplatform/terminal.h>
31#include <ascii-chat/log/log.h>
32
33/* ============================================================================
34 * Context Structure
35 * ============================================================================
36 */
37
41struct webcam_context_t {
42 int width;
43 int height;
44 uint8_t *rgba_buf;
45 image_t *cached_frame;
46};
47
48/* ============================================================================
49 * JavaScript Bridges (EM_JS)
50 * ============================================================================
51 */
52
58// clang-format off
59EM_JS(int, js_webcam_init, (int width, int height), {
60 // Check if Module.webcamVideo exists (should be set up by JavaScript)
61 if (!Module.webcamVideo) {
62 console.error('[WASM Webcam] Module.webcamVideo not found - JavaScript must initialize it first');
63 return -1;
64 }
65
66 // Ensure canvas exists and is properly sized
67 if (!Module.webcamCanvas) {
68 Module.webcamCanvas = document.createElement('canvas');
69 Module.webcamCanvas.style.display = 'none';
70 document.body.appendChild(Module.webcamCanvas);
71 }
72
73 Module.webcamCanvas.width = width;
74 Module.webcamCanvas.height = height;
75
76 console.log('[WASM Webcam] Initialized at ' + width + 'x' + height);
77 return 1;
78});
79// clang-format on
80
86// clang-format off
87EM_JS(int, js_webcam_read_frame, (uint8_t *rgba_buf, int width, int height), {
88 try {
89 if (!Module.webcamVideo || !Module.webcamStream || Module.webcamVideo.readyState < 2) {
90 console.warn('[WASM Webcam] Video not ready');
91 return 0;
92 }
93
94 const canvas = Module.webcamCanvas;
95 const ctx = canvas.getContext('2d');
96
97 // Draw current video frame to canvas
98 ctx.drawImage(Module.webcamVideo, 0, 0, width, height);
99
100 // Get pixel data as RGBA
101 const imageData = ctx.getImageData(0, 0, width, height);
102 const data = imageData.data;
103
104 // Copy into WASM heap (RGBA format, 4 bytes per pixel)
105 const byteOffset = rgba_buf;
106 for (let i = 0; i < data.length; i++) {
107 HEAPU8[byteOffset + i] = data[i];
108 }
109
110 return 1;
111 } catch (err) {
112 console.error('[WASM Webcam] Frame read failed:', err);
113 return 0;
114 }
115});
116// clang-format on
117
121// clang-format off
122EM_JS(void, js_webcam_cleanup, (), {
123 try {
124 if (Module.webcamStream) {
125 Module.webcamStream.getTracks().forEach(track => track.stop());
126 Module.webcamStream = null;
127 }
128 if (Module.webcamVideo) {
129 Module.webcamVideo.srcObject = null;
130 if (Module.webcamVideo.parentNode) {
131 Module.webcamVideo.parentNode.removeChild(Module.webcamVideo);
132 }
133 Module.webcamVideo = null;
134 }
135 if (Module.webcamCanvas) {
136 if (Module.webcamCanvas.parentNode) {
137 Module.webcamCanvas.parentNode.removeChild(Module.webcamCanvas);
138 }
139 Module.webcamCanvas = null;
140 }
141 } catch (err) {
142 console.error('[WASM Webcam] Cleanup failed:', err);
143 }
144});
145// clang-format on
146
151// clang-format off
152EM_JS(int, js_webcam_enumerate_devices, (), {
153 try {
154 return navigator.mediaDevices.enumerateDevices()
155 .then(devices =>
156 {
157 const videoDevices = devices.filter(d => d.kind === 'videoinput');
158 return videoDevices.length;
159 })
160 .catch(err => {
161 console.error('[WASM Webcam] Enumeration failed:', err);
162 return 1; // Assume at least the default camera
163 });
164 } catch (err) {
165 console.error('[WASM Webcam] Enumeration error:', err);
166 return 1;
167 }
168});
169// clang-format on
170
171/* ============================================================================
172 * Helper: RGBA to RGB24 Conversion
173 * ============================================================================
174 */
175
180static void rgba_to_rgb24(const uint8_t *rgba_buf, int width, int height, rgb_pixel_t *rgb_pixels) {
181 int pixel_count = width * height;
182 for (int i = 0; i < pixel_count; i++) {
183 // RGBA input: 4 bytes per pixel
184 // RGB output: 3 bytes per pixel (packed)
185 rgb_pixels[i].r = rgba_buf[i * 4 + 0];
186 rgb_pixels[i].g = rgba_buf[i * 4 + 1];
187 rgb_pixels[i].b = rgba_buf[i * 4 + 2];
188 // alpha (rgba_buf[i * 4 + 3]) ignored
189 }
190}
191
192/* ============================================================================
193 * Public API Implementation
194 * ============================================================================
195 */
196
197asciichat_error_t webcam_list_devices(webcam_device_info_t **devices, unsigned int *count) {
198 if (!devices || !count) {
199 return SET_ERRNO(ERROR_INVALID_PARAM, "devices and count must not be NULL");
200 }
201
202 // In browser, we report a single "Browser Webcam" device
203 // (actual enumeration would require async Promise handling)
205 if (!*devices) {
206 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate device list");
207 }
208
209 (*devices)[0].index = 0;
210 snprintf((*devices)[0].name, WEBCAM_DEVICE_NAME_MAX, "Browser Webcam");
211 *count = 1;
212
213 return ASCIICHAT_OK;
214}
215
217 SAFE_FREE(devices);
218}
219
220asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index) {
221 if (!ctx) {
222 return SET_ERRNO(ERROR_INVALID_PARAM, "ctx must not be NULL");
223 }
224
225 // Only support device 0 in browser
226 if (device_index != 0) {
227 return SET_ERRNO(ERROR_WEBCAM, "Browser webcam only supports device index 0");
228 }
229
230 // Allocate context
231 *ctx = SAFE_MALLOC(sizeof(struct webcam_context_t), struct webcam_context_t *);
232 if (!*ctx) {
233 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate webcam context");
234 }
235
236 // Default resolution
237 int width = 640;
238 int height = 480;
239
240 // Try to get preferred resolution from options
241 const int opt_width = GET_OPTION(width);
242 const int opt_height = GET_OPTION(height);
243 if (opt_width > 0 && opt_height > 0) {
244 width = opt_width;
245 height = opt_height;
246 }
247
248 (*ctx)->width = width;
249 (*ctx)->height = height;
250
251 // Allocate RGBA staging buffer (4 bytes per pixel)
252 size_t rgba_size = (size_t)width * height * 4;
253 (*ctx)->rgba_buf = SAFE_MALLOC(rgba_size, uint8_t *);
254 if (!(*ctx)->rgba_buf) {
255 SAFE_FREE(*ctx);
256 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate RGBA buffer");
257 }
258
259 // Allocate RGB24 output image
260 (*ctx)->cached_frame = image_new(width, height);
261 if (!(*ctx)->cached_frame) {
262 SAFE_FREE((*ctx)->rgba_buf);
263 SAFE_FREE(*ctx);
264 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate output image");
265 }
266
267 // Initialize browser webcam (synchronous call wrapping async promise)
268 int init_result = js_webcam_init(width, height);
269 if (init_result < 0) {
270 image_destroy((*ctx)->cached_frame);
271 SAFE_FREE((*ctx)->rgba_buf);
272 SAFE_FREE(*ctx);
273 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize browser webcam");
274 }
275
276 log_debug("WASM webcam initialized: %dx%d", width, height);
277 return ASCIICHAT_OK;
278}
279
281 if (!ctx) {
282 return;
283 }
284
285 js_webcam_cleanup();
286
287 if (ctx->cached_frame) {
288 image_destroy(ctx->cached_frame);
289 }
290
291 SAFE_FREE(ctx->rgba_buf);
292 SAFE_FREE(ctx);
293
294 log_debug("WASM webcam cleaned up");
295}
296
298 // No-op for WASM - no blocking operations to interrupt
299 (void)ctx;
300}
301
303 if (!ctx || !ctx->cached_frame) {
304 return NULL;
305 }
306
307 // Capture frame from canvas into RGBA buffer
308 int read_result = js_webcam_read_frame(ctx->rgba_buf, ctx->width, ctx->height);
309 if (read_result <= 0) {
310 log_warn_every(US_PER_SEC_INT, "Failed to read webcam frame");
311 return NULL;
312 }
313
314 // Convert RGBA to RGB24 in-place into the cached image
315 rgba_to_rgb24(ctx->rgba_buf, ctx->width, ctx->height, ctx->cached_frame->pixels);
316
317 return ctx->cached_frame;
318}
319
321 // WASM doesn't have a background thread model like other platforms
322 // Fall back to synchronous read
323 return webcam_read_context(ctx);
324}
325
326EM_JS(double, js_webcam_get_fps, (), {
327 const track = Module.webcamStream ?.getVideoTracks()[0];
328 return track ?.getSettings().frameRate || 0;
329});
330
331double webcam_get_fps(webcam_context_t *ctx) {
332 return ctx ? js_webcam_get_fps() : 0.0;
333}
334
336 if (!ctx || !width || !height) {
337 return SET_ERRNO(ERROR_INVALID_PARAM, "ctx, width, and height must not be NULL");
338 }
339
340 *width = ctx->width;
341 *height = ctx->height;
342
343 return ASCIICHAT_OK;
344}
345
347 (void)error_code;
348 log_error("Webcam initialization failed. Check that:");
349 log_error("1. HTTPS is enabled (or using localhost)");
350 log_error("2. Browser has camera permissions granted");
351 log_error("3. Camera device is not in use by another application");
352}
353
354#endif // __EMSCRIPTEN__
355
356// clang-format on
Synchronized per-thread pending error stacks and value snapshots. SET_ERRNO pushes,...
#define SET_ERRNO(code,...)
unsigned int height
Definition fft.c:23
unsigned int width
Definition fft.c:23
#define SAFE_FREE(ptr)
Definition common.h:377
#define SAFE_MALLOC(size, cast)
Definition common.h:265
unsigned char uint8_t
Definition common.h:60
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44
@ ERROR_MEMORY
Definition error_codes.h:51
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_INVALID_PARAM
@ ERROR_WEBCAM
Definition error_codes.h:59
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define US_PER_SEC_INT
Definition time.h:173
#define GET_OPTION(field)
Safely get a specific option field (lock-free read).
Definition options.h:1336
void image_destroy(image_t *image)
Destroy an image allocated with image_new().
image_t * image_new(size_t width, size_t height)
Create a new image with standard allocation.
asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index)
Initialize webcam context for advanced operations.
asciichat_error_t webcam_list_devices(webcam_device_info_t **out_devices, unsigned int *out_count)
Enumerate available webcam devices.
image_t * webcam_read_async(webcam_context_t *ctx)
Get the most recent frame from async camera thread (non-blocking).
void webcam_free_device_list(webcam_device_info_t *devices)
Free device list returned by webcam_list_devices().
void webcam_flush_context(webcam_context_t *ctx)
Flush/interrupt pending read operations on webcam context.
void webcam_print_init_error_help(asciichat_error_t error_code)
Print helpful error diagnostics for webcam initialization failures.
struct webcam_context_t webcam_context_t
Opaque webcam context structure.
Definition webcam.h:152
asciichat_error_t webcam_get_dimensions(webcam_context_t *ctx, int *width, int *height)
Get webcam frame dimensions.
#define WEBCAM_DEVICE_NAME_MAX
Maximum length of webcam device name.
Definition webcam.h:76
image_t * webcam_read_context(webcam_context_t *ctx)
Capture a frame from webcam context.
void webcam_cleanup_context(webcam_context_t *ctx)
Clean up webcam context and release resources.
📝 Logging API with multiple log levels and terminal output control
#define log_warn_every(interval_us, fmt,...)
Rate-limited WARN logging.
Definition log/log.h:755
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
🔤 String Manipulation and Shell Escaping Utilities
SIMD-friendly storage for one RGB pixel. r, g, and b hold the color channels and padding keeps the la...
uint8_t b
Blue color component (0-255).
uint8_t g
Green color component (0-255).
uint8_t r
Red color component (0-255).
Webcam device information structure.
Definition webcam.h:88
double webcam_get_fps(webcam_context_t *ctx)
EM_JS(void, js_console_log,(int level, const char *message), { const msg=UTF8ToString(message);const levelNames=[ 'DEV', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL'];const levelName=(level >=0 &&level<=5) ? levelNames[level] :'???^;const msgLen=msg.length;const hasTimestamp=msg.startsWith('[') &&msg.includes(':') &&msg.includes('.') &&msg[msg.indexOf(']')+1]===' ';const debugInfo=`[WASM_MSG_LEN=${msgLen}, HAS_TS=${hasTimestamp}]`;switch(level) { case 0:case 1:case 2:console.log(debugInfo, msg);break;case 3:console.warn(debugInfo, msg);break;case 4:case 5:console.error(debugInfo, msg);break;default:console.log(debugInfo, msg);} })