ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
webcam_wasm.c
Go to the documentation of this file.
1
17// clang-format off
18
19#ifdef __EMSCRIPTEN__
20
21#include <emscripten.h>
22#include <stdio.h>
23#include <string.h>
29#include <ascii-chat/log/log.h>
30
31/* ============================================================================
32 * Context Structure
33 * ============================================================================
34 */
35
40struct webcam_context_t {
41 int width;
42 int height;
43 uint8_t *rgba_buf;
44 image_t *cached_frame;
45};
46
47/* ============================================================================
48 * JavaScript Bridges (EM_JS)
49 * ============================================================================
50 */
51
57// clang-format off
58EM_JS(int, js_webcam_init, (int width, int height), {
59 // Check if Module.webcamVideo exists (should be set up by JavaScript)
60 if (!Module.webcamVideo) {
61 console.error('[WASM Webcam] Module.webcamVideo not found - JavaScript must initialize it first');
62 return -1;
63 }
64
65 // Ensure canvas exists and is properly sized
66 if (!Module.webcamCanvas) {
67 Module.webcamCanvas = document.createElement('canvas');
68 Module.webcamCanvas.style.display = 'none';
69 document.body.appendChild(Module.webcamCanvas);
70 }
71
72 Module.webcamCanvas.width = width;
73 Module.webcamCanvas.height = height;
74
75 console.log('[WASM Webcam] Initialized at ' + width + 'x' + height);
76 return 1;
77});
78// clang-format on
79
85// clang-format off
86EM_JS(int, js_webcam_read_frame, (uint8_t *rgba_buf, int width, int height), {
87 try {
88 if (!Module.webcamVideo || !Module.webcamStream || Module.webcamVideo.readyState < 2) {
89 console.warn('[WASM Webcam] Video not ready');
90 return 0;
91 }
92
93 const canvas = Module.webcamCanvas;
94 const ctx = canvas.getContext('2d');
95
96 // Draw current video frame to canvas
97 ctx.drawImage(Module.webcamVideo, 0, 0, width, height);
98
99 // Get pixel data as RGBA
100 const imageData = ctx.getImageData(0, 0, width, height);
101 const data = imageData.data;
102
103 // Copy into WASM heap (RGBA format, 4 bytes per pixel)
104 const byteOffset = rgba_buf;
105 for (let i = 0; i < data.length; i++) {
106 HEAPU8[byteOffset + i] = data[i];
107 }
108
109 return 1;
110 } catch (err) {
111 console.error('[WASM Webcam] Frame read failed:', err);
112 return 0;
113 }
114});
115// clang-format on
116
120// clang-format off
121EM_JS(void, js_webcam_cleanup, (), {
122 try {
123 if (Module.webcamStream) {
124 Module.webcamStream.getTracks().forEach(track => track.stop());
125 Module.webcamStream = null;
126 }
127 if (Module.webcamVideo) {
128 Module.webcamVideo.srcObject = null;
129 if (Module.webcamVideo.parentNode) {
130 Module.webcamVideo.parentNode.removeChild(Module.webcamVideo);
131 }
132 Module.webcamVideo = null;
133 }
134 if (Module.webcamCanvas) {
135 if (Module.webcamCanvas.parentNode) {
136 Module.webcamCanvas.parentNode.removeChild(Module.webcamCanvas);
137 }
138 Module.webcamCanvas = null;
139 }
140 } catch (err) {
141 console.error('[WASM Webcam] Cleanup failed:', err);
142 }
143});
144// clang-format on
145
150// clang-format off
151EM_JS(int, js_webcam_enumerate_devices, (), {
152 try {
153 return navigator.mediaDevices.enumerateDevices()
154 .then(devices =>
155 {
156 const videoDevices = devices.filter(d => d.kind === 'videoinput');
157 return videoDevices.length;
158 })
159 .catch(err => {
160 console.error('[WASM Webcam] Enumeration failed:', err);
161 return 1; // Assume at least the default camera
162 });
163 } catch (err) {
164 console.error('[WASM Webcam] Enumeration error:', err);
165 return 1;
166 }
167});
168// clang-format on
169
170/* ============================================================================
171 * Helper: RGBA to RGB24 Conversion
172 * ============================================================================
173 */
174
179static void rgba_to_rgb24(const uint8_t *rgba_buf, int width, int height, rgb_pixel_t *rgb_pixels) {
180 int pixel_count = width * height;
181 for (int i = 0; i < pixel_count; i++) {
182 // RGBA input: 4 bytes per pixel
183 // RGB output: 3 bytes per pixel (packed)
184 rgb_pixels[i].r = rgba_buf[i * 4 + 0];
185 rgb_pixels[i].g = rgba_buf[i * 4 + 1];
186 rgb_pixels[i].b = rgba_buf[i * 4 + 2];
187 // alpha (rgba_buf[i * 4 + 3]) ignored
188 }
189}
190
191/* ============================================================================
192 * Public API Implementation
193 * ============================================================================
194 */
195
196asciichat_error_t webcam_list_devices(webcam_device_info_t **devices, unsigned int *count) {
197 if (!devices || !count) {
198 return SET_ERRNO(ERROR_INVALID_PARAM, "devices and count must not be NULL");
199 }
200
201 // In browser, we report a single "Browser Webcam" device
202 // (actual enumeration would require async Promise handling)
204 if (!*devices) {
205 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate device list");
206 }
207
208 (*devices)[0].index = 0;
209 snprintf((*devices)[0].name, WEBCAM_DEVICE_NAME_MAX, "Browser Webcam");
210 *count = 1;
211
212 return ASCIICHAT_OK;
213}
214
216 SAFE_FREE(devices);
217}
218
219asciichat_error_t webcam_init_context(webcam_context_t **ctx, unsigned short int device_index) {
220 if (!ctx) {
221 return SET_ERRNO(ERROR_INVALID_PARAM, "ctx must not be NULL");
222 }
223
224 // Only support device 0 in browser
225 if (device_index != 0) {
226 return SET_ERRNO(ERROR_WEBCAM, "Browser webcam only supports device index 0");
227 }
228
229 // Allocate context
230 *ctx = SAFE_MALLOC(sizeof(struct webcam_context_t), struct webcam_context_t *);
231 if (!*ctx) {
232 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate webcam context");
233 }
234
235 // Default resolution
236 int width = 640;
237 int height = 480;
238
239 // Try to get preferred resolution from options
240 const int opt_width = GET_OPTION(width);
241 const int opt_height = GET_OPTION(height);
242 if (opt_width > 0 && opt_height > 0) {
243 width = opt_width;
244 height = opt_height;
245 }
246
247 (*ctx)->width = width;
248 (*ctx)->height = height;
249
250 // Allocate RGBA staging buffer (4 bytes per pixel)
251 size_t rgba_size = (size_t)width * height * 4;
252 (*ctx)->rgba_buf = SAFE_MALLOC(rgba_size, uint8_t *);
253 if (!(*ctx)->rgba_buf) {
254 SAFE_FREE(*ctx);
255 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate RGBA buffer");
256 }
257
258 // Allocate RGB24 output image
259 (*ctx)->cached_frame = image_new(width, height);
260 if (!(*ctx)->cached_frame) {
261 SAFE_FREE((*ctx)->rgba_buf);
262 SAFE_FREE(*ctx);
263 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate output image");
264 }
265
266 // Initialize browser webcam (synchronous call wrapping async promise)
267 int init_result = js_webcam_init(width, height);
268 if (init_result < 0) {
269 image_destroy((*ctx)->cached_frame);
270 SAFE_FREE((*ctx)->rgba_buf);
271 SAFE_FREE(*ctx);
272 return SET_ERRNO(ERROR_WEBCAM, "Failed to initialize browser webcam");
273 }
274
275 log_debug("WASM webcam initialized: %dx%d", width, height);
276 return ASCIICHAT_OK;
277}
278
280 if (!ctx) {
281 return;
282 }
283
284 js_webcam_cleanup();
285
286 if (ctx->cached_frame) {
287 image_destroy(ctx->cached_frame);
288 }
289
290 SAFE_FREE(ctx->rgba_buf);
291 SAFE_FREE(ctx);
292
293 log_debug("WASM webcam cleaned up");
294}
295
297 // No-op for WASM - no blocking operations to interrupt
298 (void)ctx;
299}
300
302 if (!ctx || !ctx->cached_frame) {
303 return NULL;
304 }
305
306 // Capture frame from canvas into RGBA buffer
307 int read_result = js_webcam_read_frame(ctx->rgba_buf, ctx->width, ctx->height);
308 if (read_result <= 0) {
309 log_warn_every(100, "Failed to read webcam frame");
310 return NULL;
311 }
312
313 // Convert RGBA to RGB24 in-place into the cached image
314 rgba_to_rgb24(ctx->rgba_buf, ctx->width, ctx->height, ctx->cached_frame->pixels);
315
316 return ctx->cached_frame;
317}
318
320 // WASM doesn't have a background thread model like other platforms
321 // Fall back to synchronous read
322 return webcam_read_context(ctx);
323}
324
325asciichat_error_t webcam_get_dimensions(webcam_context_t *ctx, int *width, int *height) {
326 if (!ctx || !width || !height) {
327 return SET_ERRNO(ERROR_INVALID_PARAM, "ctx, width, and height must not be NULL");
328 }
329
330 *width = ctx->width;
331 *height = ctx->height;
332
333 return ASCIICHAT_OK;
334}
335
337 (void)error_code;
338 log_error("Webcam initialization failed. Check that:");
339 log_error("1. HTTPS is enabled (or using localhost)");
340 log_error("2. Browser has camera permissions granted");
341 log_error("3. Camera device is not in use by another application");
342}
343
344#endif // __EMSCRIPTEN__
345
346// clang-format on
Platform abstraction layer umbrella header providing unified cross-platform API.
asciichat_error_t error_code
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
#define SAFE_FREE(ptr)
Definition common.h:376
#define SAFE_MALLOC(size, cast)
Definition common.h:264
unsigned char uint8_t
Definition common.h:56
#define SET_ERRNO(code, context_msg,...)
Set error code with custom context message and log it, returning the error code.
asciichat_error_t
Error and exit codes - unified status values (0-255)
Definition error_codes.h:49
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ ERROR_WEBCAM
Definition error_codes.h:64
#define log_error(...)
Log an ERROR message.
Definition log/log.h:587
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
#define GET_OPTION(field)
Safely get a specific option field (lock-free read)
void image_destroy(image_t *p)
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:150
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:74
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.
⚙️ Unified options parsing system for ascii-chat with builder pattern and lock-free access
📝 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:708
Image structure.
RGB pixel structure.
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:86
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);} })