ascii-chat 0.11.33
Video chat in your terminal
Loading...
Searching...
No Matches
platform/wasm/ffmpeg_decoder.c
Go to the documentation of this file.
1
12// clang-format off
13
14#ifdef __EMSCRIPTEN__
15
16#include <emscripten.h>
20#include <ascii-chat/log/log.h>
21#include <stdlib.h>
22#include <string.h>
23
24/* Full struct definition for the opaque ffmpeg_decoder_t */
25struct ffmpeg_decoder_t {
26 int decoder_id; // ffmpeg.wasm decoder instance ID
27 image_t *cached_frame; // Cached RGB frame for reuse
28 bool at_end; // EOF flag
29 double position; // Current playback position in seconds
30};
31
32/* ============================================================================
33 * JavaScript Bridges (EM_JS)
34 * ============================================================================ */
35
40EM_JS(int, js_ffmpeg_init, (), {
41 if (typeof Module !== 'undefined' && Module.FFmpeg) {
42 console.log('[WASM] ffmpeg.wasm already initialized');
43 return 1;
44 }
45 console.log('[WASM] Initializing ffmpeg.wasm');
46 // ffmpeg.wasm must be loaded by JavaScript before calling decoder functions
47 return 0; // Will be initialized by JavaScript
48});
49
54EM_JS(int, js_ffmpeg_decoder_create, (const char *path_cstr), {
55 if (!Module.FFmpeg || !Module.FFmpeg.isLoaded()) {
56 console.error('[WASM] ffmpeg.wasm not loaded');
57 return 0;
58 }
59
60 const path = UTF8ToString(path_cstr);
61 console.log('[WASM] Creating decoder for: ' + path);
62
63 try {
64 // This would be implemented by JavaScript ffmpeg.wasm wrapper
65 // Returns a decoder ID for future operations
66 if (typeof Module.ffmpegDecoderCreate === 'function') {
67 return Module.ffmpegDecoderCreate(path);
68 }
69 return 0;
70 } catch (err) {
71 console.error('[WASM] Decoder creation failed:', err);
72 return 0;
73 }
74});
75
79EM_JS(void, js_ffmpeg_decoder_destroy, (int decoder_id), {
80 if (!Module.FFmpeg || !Module.FFmpeg.isLoaded()) {
81 return;
82 }
83
84 try {
85 if (typeof Module.ffmpegDecoderDestroy === 'function') {
86 Module.ffmpegDecoderDestroy(decoder_id);
87 }
88 } catch (err) {
89 console.error('[WASM] Decoder destruction failed:', err);
90 }
91});
92
97EM_JS(int, js_ffmpeg_decoder_read_frame, (int decoder_id, uint8_t *out_rgba, int width, int height), {
98 if (!Module.FFmpeg || !Module.FFmpeg.isLoaded()) {
99 return 0;
100 }
101
102 try {
103 if (typeof Module.ffmpegDecoderReadFrame === 'function') {
104 const rgba_data = Module.ffmpegDecoderReadFrame(decoder_id, width, height);
105 if (rgba_data) {
106 // Copy RGBA data to WASM heap
107 const byteOffset = out_rgba;
108 for (let i = 0; i < rgba_data.length; i++) {
109 HEAPU8[byteOffset + i] = rgba_data[i];
110 }
111 return 1;
112 }
113 }
114 return 0;
115 } catch (err) {
116 console.error('[WASM] Frame read failed:', err);
117 return 0;
118 }
119});
120
124EM_JS(int, js_ffmpeg_decoder_at_end, (int decoder_id), {
125 try {
126 if (typeof Module.ffmpegDecoderAtEnd === 'function') {
127 return Module.ffmpegDecoderAtEnd(decoder_id) ? 1 : 0;
128 }
129 } catch (err) {
130 console.error('[WASM] at_end check failed:', err);
131 }
132 return 1;
133});
134
138EM_JS(int, js_ffmpeg_decoder_rewind, (int decoder_id), {
139 try {
140 if (typeof Module.ffmpegDecoderRewind === 'function') {
141 return Module.ffmpegDecoderRewind(decoder_id) ? 1 : 0;
142 }
143 } catch (err) {
144 console.error('[WASM] Rewind failed:', err);
145 }
146 return 0;
147});
148
152EM_JS(int, js_ffmpeg_decoder_has_audio, (int decoder_id), {
153 try {
154 if (typeof Module.ffmpegDecoderHasAudio === 'function') {
155 return Module.ffmpegDecoderHasAudio(decoder_id) ? 1 : 0;
156 }
157 } catch (err) {
158 console.error('[WASM] has_audio check failed:', err);
159 }
160 return 0;
161});
162
166EM_JS(double, js_ffmpeg_decoder_get_position, (int decoder_id), {
167 try {
168 if (typeof Module.ffmpegDecoderGetPosition === 'function') {
169 return Module.ffmpegDecoderGetPosition(decoder_id);
170 }
171 } catch (err) {
172 console.error('[WASM] get_position failed:', err);
173 }
174 return 0.0;
175});
176
177/* ============================================================================
178 * Public API Implementation
179 * ============================================================================ */
180
181ffmpeg_decoder_t *ffmpeg_decoder_create(const char *path) {
182 if (!path) {
183 return SET_ERRNO(ERROR_INVALID_PARAM, "Path must not be NULL"), NULL;
184 }
185
186 int decoder_id = js_ffmpeg_decoder_create(path);
187 if (decoder_id <= 0) {
188 return SET_ERRNO(ERROR_MEDIA_OPEN, "Failed to create ffmpeg decoder"), NULL;
189 }
190
191 ffmpeg_decoder_t *decoder = malloc(sizeof(ffmpeg_decoder_t));
192 if (!decoder) {
193 js_ffmpeg_decoder_destroy(decoder_id);
194 return SET_ERRNO(ERROR_MEMORY, "Failed to allocate decoder"), NULL;
195 }
196
197 decoder->decoder_id = decoder_id;
198 decoder->cached_frame = NULL;
199 decoder->at_end = false;
200 decoder->position = 0.0;
201
202 log_debug("WASM FFmpeg decoder created for: %s", path);
203 return decoder;
204}
205
207 return SET_ERRNO(ERROR_NOT_SUPPORTED, "stdin decoder not supported in WASM"), NULL;
208}
209
211 if (!decoder)
212 return;
213
214 js_ffmpeg_decoder_destroy(decoder->decoder_id);
215
216 if (decoder->cached_frame) {
217 image_destroy(decoder->cached_frame);
218 }
219
220 free(decoder);
221 log_debug("WASM FFmpeg decoder destroyed");
222}
223
225 if (!decoder) {
226 return SET_ERRNO(ERROR_INVALID_PARAM, "Decoder must not be NULL");
227 }
228 // No prefetching in WASM - frames are read on-demand
229 return ASCIICHAT_OK;
230}
231
233 (void)decoder;
234}
235
236void ffmpeg_decoder_set_exit_callback(ffmpeg_decoder_t *decoder, bool (*should_exit)(void *user_data),
237 void *user_data) {
238 (void)decoder;
239 (void)should_exit;
240 (void)user_data;
241}
242
244 if (!decoder)
245 return NULL;
246
247 if (decoder->at_end)
248 return NULL;
249
250 // Allocate cached frame if not already done
251 if (!decoder->cached_frame) {
252 // TODO: Get actual dimensions from decoder
253 decoder->cached_frame = image_new(1920, 1080);
254 if (!decoder->cached_frame) {
255 return NULL;
256 }
257 }
258
259 // Allocate RGBA staging buffer
260 int width = decoder->cached_frame->w;
261 int height = decoder->cached_frame->h;
262 uint8_t *rgba_buf = malloc(width * height * 4);
263 if (!rgba_buf) {
264 return NULL;
265 }
266
267 // Read frame from ffmpeg.wasm
268 int result = js_ffmpeg_decoder_read_frame(decoder->decoder_id, rgba_buf, width, height);
269 if (result <= 0) {
270 decoder->at_end = true;
271 free(rgba_buf);
272 return NULL;
273 }
274
275 // Convert RGBA to RGB24
276 for (int i = 0; i < width * height; i++) {
277 decoder->cached_frame->pixels[i].r = rgba_buf[i * 4 + 0];
278 decoder->cached_frame->pixels[i].g = rgba_buf[i * 4 + 1];
279 decoder->cached_frame->pixels[i].b = rgba_buf[i * 4 + 2];
280 }
281
282 free(rgba_buf);
283 decoder->position = js_ffmpeg_decoder_get_position(decoder->decoder_id);
284 return decoder->cached_frame;
285}
286
288 if (!decoder)
289 return true;
290
291 if (decoder->at_end)
292 return true;
293
294 int result = js_ffmpeg_decoder_at_end(decoder->decoder_id);
295 decoder->at_end = (result != 0);
296 return decoder->at_end;
297}
298
300 if (!decoder) {
301 return SET_ERRNO(ERROR_INVALID_PARAM, "Decoder must not be NULL");
302 }
303
304 int result = js_ffmpeg_decoder_rewind(decoder->decoder_id);
305 if (result <= 0) {
306 return SET_ERRNO(ERROR_MEDIA_SEEK, "Failed to rewind decoder");
307 }
308
309 decoder->at_end = false;
310 decoder->position = 0.0;
311 return ASCIICHAT_OK;
312}
313
315 if (!decoder)
316 return false;
317
318 int result = js_ffmpeg_decoder_has_audio(decoder->decoder_id);
319 return (result != 0);
320}
321
323 if (!decoder)
324 return false;
325 return true; // Assume video if decoder was created
326}
327
329 if (!decoder)
330 return 0.0;
331
332 return js_ffmpeg_decoder_get_position(decoder->decoder_id);
333}
334
335size_t ffmpeg_decoder_read_audio_samples(ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples) {
336 (void)decoder;
337 (void)buffer;
338 (void)num_samples;
339 // Audio decoding not yet implemented in ffmpeg.wasm bridge
340 return 0;
341}
342
344 (void)decoder;
345 (void)timestamp;
346 return SET_ERRNO(ERROR_MEDIA_SEEK, "Seeking not yet implemented in ffmpeg.wasm");
347}
348
350 (void)decoder;
351 return 0.0;
352}
353
355 (void)decoder;
356 return 24.0; // Default FPS
357}
358
359#endif // __EMSCRIPTEN__
360
361// clang-format on
⚠️‼️ Comprehensive thread-local error context system for ascii-chat
🎞️ FFmpeg-based media decoder for video and audio streams
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_MEDIA_SEEK
Definition error_codes.h:73
@ ERROR_MEMORY
Definition error_codes.h:56
@ ASCIICHAT_OK
Definition error_codes.h:51
@ ERROR_INVALID_PARAM
@ ERROR_MEDIA_OPEN
Definition error_codes.h:71
@ ERROR_NOT_SUPPORTED
Definition error_codes.h:74
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:548
bool ffmpeg_decoder_at_end(ffmpeg_decoder_t *decoder)
Check if decoder reached end of stream.
double ffmpeg_decoder_get_position(ffmpeg_decoder_t *decoder)
Get current playback position in seconds.
void ffmpeg_decoder_stop_prefetch(ffmpeg_decoder_t *decoder)
Stop the background frame prefetching thread.
image_t * ffmpeg_decoder_read_video_frame(ffmpeg_decoder_t *decoder)
Decode next video frame.
bool ffmpeg_decoder_has_video(ffmpeg_decoder_t *decoder)
Check if decoder has video stream.
double ffmpeg_decoder_get_video_fps(ffmpeg_decoder_t *decoder)
Get video frame rate.
asciichat_error_t ffmpeg_decoder_rewind(ffmpeg_decoder_t *decoder)
Seek to beginning of media.
bool ffmpeg_decoder_has_audio(ffmpeg_decoder_t *decoder)
Check if decoder has audio stream.
asciichat_error_t ffmpeg_decoder_start_prefetch(ffmpeg_decoder_t *decoder)
Start the background frame prefetching thread.
asciichat_error_t ffmpeg_decoder_seek_to_timestamp(ffmpeg_decoder_t *decoder, double timestamp_sec)
Seek to specific timestamp in media.
void ffmpeg_decoder_set_exit_callback(ffmpeg_decoder_t *decoder, bool(*should_exit_callback)(void *), void *user_data)
Set exit signal callback for graceful shutdown during I/O.
void ffmpeg_decoder_destroy(ffmpeg_decoder_t *decoder)
Destroy FFmpeg decoder and free resources.
ffmpeg_decoder_t * ffmpeg_decoder_create(const char *path)
Create FFmpeg decoder from file path.
size_t ffmpeg_decoder_read_audio_samples(ffmpeg_decoder_t *decoder, float *buffer, size_t num_samples)
Decode audio samples.
ffmpeg_decoder_t * ffmpeg_decoder_create_stdin(void)
Create FFmpeg decoder from stdin.
double ffmpeg_decoder_get_duration(ffmpeg_decoder_t *decoder)
Get media duration in seconds.
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.
📝 Logging API with multiple log levels and terminal output control
bool should_exit(void)
Definition misc.c:131
FFmpeg decoder state for video and audio decoding.
Image structure.
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);} })