96 {
97 if (!config || !config->run_fn) {
98 return SET_ERRNO(ERROR_INVALID_PARAM, "config or run_fn is NULL");
99 }
100
101
102 g_current_config = config;
103
104
105 g_render_should_exit = display_should_exit_adapter;
106
107 asciichat_error_t result = ASCIICHAT_OK;
108
109
111 session_capture_ctx_t *capture = NULL;
113 audio_context_t *audio_ctx = NULL;
115 bool audio_available = false;
116
117
118
119
120
121 log_debug("session_client_like_run(): Setting up terminal and logging");
122
123
125 log_debug("terminal_should_force_stderr()=%d", should_force_stderr);
126
127 if (should_force_stderr) {
128
129
131 }
132
133
134
135
136
137 log_debug("session_client_like_run(): Validating keepawake options");
138
139
140 bool enable_ka = GET_OPTION(enable_keepawake);
141 bool disable_ka = GET_OPTION(disable_keepawake);
142 log_debug("enable_keepawake=%d, disable_keepawake=%d", enable_ka, disable_ka);
143
144 if (enable_ka && disable_ka) {
145 result = SET_ERRNO(ERROR_INVALID_PARAM, "--keepawake and --no-keepawake are mutually exclusive");
146 goto cleanup;
147 }
148
149 log_debug("session_client_like_run(): Enabling keepawake if needed");
150
151
152 if (!disable_ka) {
154 }
155
156 log_debug("session_client_like_run(): Keepawake setup complete");
157
158
159
160
161
162 log_debug("session_client_like_run(): Creating temporary display for splash");
163
165 if (temp_display) {
167
168
169 const char *media_url = GET_OPTION(media_url);
170 const char *media_file = GET_OPTION(media_file);
171 bool has_media = (media_url && strlen(media_url) > 0) || (media_file && strlen(media_file) > 0);
172
173
174 if (!has_media && !GET_OPTION(snapshot_mode)) {
176 }
177 }
178
179
180
181
182
183 if (!GET_OPTION(snapshot_mode)) {
185 }
186
187
188
189
190
191 session_capture_config_t capture_config = {0};
192 capture_config.resize_for_network = false;
193 capture_config.should_exit_callback = capture_should_exit_adapter;
194 capture_config.callback_data = NULL;
195
196
197 int user_fps = GET_OPTION(fps);
198 bool fps_explicitly_set = user_fps > 0;
199
200
201 const char *media_url_val = GET_OPTION(media_url);
202 const char *media_file_val = GET_OPTION(media_file);
203
204 if (media_url_val && strlen(media_url_val) > 0) {
205
206 log_info("Using network URL: %s (webcam disabled)", media_url_val);
207 capture_config.type = MEDIA_SOURCE_FILE;
208 capture_config.path = media_url_val;
209 capture_config.loop = false;
210
211 if (fps_explicitly_set) {
212 capture_config.target_fps = (uint32_t)user_fps;
213 log_info("Using user-specified FPS: %u", capture_config.target_fps);
214 } else {
215
217 if (probe_source) {
219 log_info("Detected HTTP stream video FPS: %.1f", url_fps);
220 if (url_fps > 0.0) {
221 capture_config.target_fps = (uint32_t)(url_fps + 0.5);
222 } else {
223 log_warn("FPS detection failed for HTTP stream, using default 60 FPS");
224 capture_config.target_fps = 60;
225 }
226 } else {
227 log_warn("Failed to create probe source for HTTP stream, using default 60 FPS");
228 capture_config.target_fps = 60;
229 }
230 }
231 } else if (media_file_val && strlen(media_file_val) > 0) {
232
233 if (strcmp(media_file_val, "-") == 0) {
234 log_info("Using stdin for media streaming (webcam disabled)");
235 capture_config.type = MEDIA_SOURCE_STDIN;
236 capture_config.path = NULL;
237 capture_config.target_fps = fps_explicitly_set ? (uint32_t)user_fps : 60;
238 capture_config.loop = false;
239 } else {
240 log_info("Using media file: %s (webcam disabled)", media_file_val);
241 capture_config.type = MEDIA_SOURCE_FILE;
242 capture_config.path = media_file_val;
243 capture_config.loop = GET_OPTION(media_loop);
244
245 if (fps_explicitly_set) {
246 capture_config.target_fps = (uint32_t)user_fps;
247 log_info("Using user-specified FPS: %u", capture_config.target_fps);
248 } else {
249
251 if (probe_source) {
253 log_info("Detected file video FPS: %.1f", file_fps);
254 if (file_fps > 0.0) {
255 capture_config.target_fps = (uint32_t)(file_fps + 0.5);
256 } else {
257 log_warn("FPS detection failed, using default 60 FPS");
258 capture_config.target_fps = 60;
259 }
260 } else {
261 log_warn("Failed to create probe source for FPS detection, using default 60 FPS");
262 capture_config.target_fps = 60;
263 }
264 }
265 }
266 } else if (GET_OPTION(test_pattern)) {
267
268 log_info("Using test pattern");
269 capture_config.type = MEDIA_SOURCE_TEST;
270 capture_config.path = NULL;
271 capture_config.target_fps = fps_explicitly_set ? (uint32_t)user_fps : 60;
272 capture_config.loop = false;
273 } else {
274
275 log_info("Using local webcam");
276 capture_config.type = MEDIA_SOURCE_WEBCAM;
277 capture_config.path = NULL;
278 capture_config.target_fps = fps_explicitly_set ? (uint32_t)user_fps : 60;
279 capture_config.loop = false;
280 }
281
282
283 capture_config.initial_seek_timestamp = GET_OPTION(media_seek_timestamp);
284
285
286 if (probe_source) {
288 probe_source = NULL;
289 }
290
291
292
293
294
295
296
297
298 bool is_network_mode = (config->tcp_client != NULL || config->websocket_client != NULL);
299
300 if (is_network_mode) {
301
302 log_debug("Network mode detected - using network capture (no local media source)");
303 int fps = GET_OPTION(fps);
305 if (!capture) {
306 log_fatal("Failed to initialize network capture context");
307 result = ERROR_MEDIA_INIT;
308 goto cleanup;
309 }
310 if (fps > 0) {
311 log_debug("Network capture FPS set to %d from options", fps);
312 }
313 } else {
314
315 log_debug("Mirror mode detected - using mirror capture with local media source");
317 if (!capture) {
318 log_fatal("Failed to initialize mirror capture source");
319 result = ERROR_MEDIA_INIT;
320 goto cleanup;
321 }
322 }
323
324
325
326
327
328
329 bool should_init_audio = true;
330 if (GET_OPTION(snapshot_mode) && GET_OPTION(snapshot_delay) == 0.0) {
331 should_init_audio = false;
332 log_debug("Skipping audio initialization for immediate snapshot");
333 }
334
335
336 if (should_init_audio && capture_config.type == MEDIA_SOURCE_FILE && capture_config.path) {
339 audio_available = true;
340
341
342 audio_ctx = SAFE_MALLOC(sizeof(audio_context_t), audio_context_t *);
343 if (audio_ctx) {
344 *audio_ctx = (audio_context_t){0};
346
348 audio_ctx->media_source = media_source;
349
350 if (media_source) {
352 }
353
354
356 audio_ctx->playback_only = !should_enable_mic;
357
358
359 if (audio_ctx->playback_buffer) {
360 audio_ctx->playback_buffer->jitter_buffer_enabled = false;
361 atomic_store(&audio_ctx->playback_buffer->jitter_buffer_filled, true);
362 }
363
364
366
367 log_debug("Audio context initialized");
368 } else {
369 log_warn("Failed to initialize audio context");
371 SAFE_FREE(audio_ctx);
372 audio_ctx = NULL;
373 audio_available = false;
374 }
375 }
376 }
377 }
378
379
380
381
382
383 session_display_config_t display_config = {0};
384 display_config.snapshot_mode = GET_OPTION(snapshot_mode);
385 display_config.palette_type = GET_OPTION(palette_type);
386 display_config.custom_palette = GET_OPTION(palette_custom_set) ? GET_OPTION(palette_custom) : NULL;
387 display_config.color_mode = TERM_COLOR_AUTO;
388 display_config.enable_audio_playback = audio_available;
389 display_config.audio_ctx = audio_ctx;
390 display_config.should_exit_callback = display_should_exit_adapter;
391 display_config.callback_data = NULL;
392
394 if (!display) {
395 log_fatal("Failed to initialize display");
396 result = ERROR_DISPLAY;
397 goto cleanup;
398 }
399
400
401
402
403
404 log_debug("About to call splash_intro_done()");
406 log_debug("splash_intro_done() returned");
407
408 if (temp_display) {
410 temp_display = NULL;
411 }
412
413
414
415
416
417 log_debug("About to check audio context for duplex");
418 if (audio_ctx && audio_available) {
420 log_info("Audio playback started");
421 } else {
422 log_warn("Failed to start audio duplex");
424 SAFE_FREE(audio_ctx);
425 audio_ctx = NULL;
426 audio_available = false;
427 }
428 }
429
430
431
432
433
434 log_debug("session_client_like_run(): Setting up network transports");
435
436
437 const char *server_address = GET_OPTION(address);
439
440 if (is_websocket) {
441 log_debug("WebSocket URL detected: %s", server_address);
443 if (!g_websocket_client) {
444 log_error("Failed to create WebSocket client");
445 result = ERROR_NETWORK;
446 goto cleanup;
447 }
448 } else {
449 log_debug("Using TCP client for server: %s:%d", server_address ? server_address : "localhost", GET_OPTION(port));
451 if (!g_tcp_client) {
452 log_error("Failed to create TCP client");
453 result = ERROR_NETWORK;
454 goto cleanup;
455 }
456 }
457
458
459
460
461
462
463 int attempt = 0;
464 int max_attempts = config->max_reconnect_attempts;
465
466 while (true) {
467 attempt++;
468
469 log_debug("About to call config->run_fn() (attempt %d)", attempt);
470 result = config->run_fn(capture, display, config->run_user_data);
471 log_debug("config->run_fn() returned with result=%d", result);
472
473
474 if (result == ASCIICHAT_OK) {
475 break;
476 }
477
478
479 bool should_retry = false;
480
481 if (max_attempts != 0) {
482
483 if (config->should_reconnect_callback) {
484 should_retry = config->should_reconnect_callback(result, attempt, config->reconnect_user_data);
485 } else {
486 should_retry = true;
487 }
488
489
490 if (should_retry && max_attempts > 0 && attempt >= max_attempts) {
491 log_debug("Reached maximum reconnection attempts (%d), giving up", max_attempts);
492 should_retry = false;
493 }
494 }
495
496
498 break;
499 }
500
501
502 if (max_attempts == -1) {
503 log_info("Connection failed, retrying...");
504 } else if (max_attempts > 0) {
505 log_info("Connection failed, retrying (attempt %d/%d)...", attempt + 1, max_attempts);
506 }
507
508
509 if (config->reconnect_delay_ms > 0) {
511 }
512
513
514 }
515
516
517
518
519
520cleanup:
521
523
524
525 if (g_websocket_client) {
526 log_debug("Destroying WebSocket client");
528 }
529 if (g_tcp_client) {
530 log_debug("Destroying TCP client");
532 }
533
534
535 log_debug("Terminating PortAudio device resources");
537
538
539 if (audio_ctx) {
542 SAFE_FREE(audio_ctx);
543 audio_ctx = NULL;
544 }
545
546
547 if (display) {
549 display = NULL;
550 }
551
552
553 if (capture) {
555 capture = NULL;
556 }
557
558
559 if (probe_source) {
561 probe_source = NULL;
562 }
563
564
566
567
569
570
571 log_debug("Disabling keepawake");
573
574
576 const char newline = '\n';
578 }
579
581
582 return result;
583}
size_t platform_write_all(int fd, const void *buf, size_t count)
Write all data to file descriptor with automatic retry on transient errors.
asciichat_error_t audio_init(audio_context_t *ctx)
asciichat_error_t audio_start_duplex(audio_context_t *ctx)
void audio_destroy(audio_context_t *ctx)
asciichat_error_t audio_stop_duplex(audio_context_t *ctx)
void audio_terminate_portaudio_final(void)
Terminate PortAudio and free all device resources.
bool audio_should_enable_microphone(audio_source_t source, bool has_media_audio)
tcp_client_t * tcp_client_create(void)
Create and initialize TCP client.
void tcp_client_destroy(tcp_client_t **client_ptr)
Destroy TCP client and free resources.
websocket_client_t * websocket_client_create(void)
Create and initialize WebSocket client.
void websocket_client_destroy(websocket_client_t **client_ptr)
Destroy WebSocket client and free resources.
void session_capture_set_audio_context(session_capture_ctx_t *ctx, void *audio_ctx)
session_capture_ctx_t * session_mirror_capture_create(const session_capture_config_t *config)
session_capture_ctx_t * session_network_capture_create(uint32_t target_fps)
void * session_capture_get_media_source(session_capture_ctx_t *ctx)
void session_capture_destroy(session_capture_ctx_t *ctx)
void session_display_destroy(session_display_ctx_t *ctx)
session_display_ctx_t * session_display_create(const session_display_config_t *config)
void platform_disable_keepawake(void)
asciichat_error_t platform_enable_keepawake(void)
void log_set_force_stderr(bool enabled)
void log_set_terminal_output(bool enabled)
void session_log_buffer_destroy(void)
bool media_source_has_audio(media_source_t *source)
void media_source_destroy(media_source_t *source)
double media_source_get_video_fps(media_source_t *source)
void media_source_set_audio_context(media_source_t *source, void *audio_ctx)
media_source_t * media_source_create(media_source_type_t type, const char *path)
int splash_intro_done(void)
int splash_intro_start(session_display_ctx_t *ctx)
Internal session display context structure.
bool url_is_websocket(const char *url)
void webcam_destroy(void)