#define _POSIX_C_SOURCE 200809L #include "app.h" #include #include #include #include #include #include static void configure_runtime_environment(void) { char runtime_dir[64]; char pulse_socket[96]; const char *display; if (!getenv("XDG_RUNTIME_DIR") || getenv("XDG_RUNTIME_DIR")[0] == '\0') { snprintf(runtime_dir, sizeof(runtime_dir), "/run/user/%u", (unsigned int) getuid()); if (access(runtime_dir, R_OK | W_OK | X_OK) == 0) { setenv("XDG_RUNTIME_DIR", runtime_dir, 1); } } display = getenv("DISPLAY"); if ((!display || display[0] == '\0') && access("/tmp/.X11-unix/X0", R_OK | W_OK) == 0) { setenv("DISPLAY", ":0", 1); } if (getenv("XDG_RUNTIME_DIR") && getenv("XDG_RUNTIME_DIR")[0] != '\0' && (!getenv("PULSE_SERVER") || getenv("PULSE_SERVER")[0] == '\0')) { snprintf(pulse_socket, sizeof(pulse_socket), "%s/pulse/native", getenv("XDG_RUNTIME_DIR")); if (access(pulse_socket, R_OK | W_OK) == 0) { setenv("PULSE_SERVER", pulse_socket, 0); } } } static void log_runtime_environment(const char *phase) { const char *display = getenv("DISPLAY"); const char *wayland_display = getenv("WAYLAND_DISPLAY"); const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); const char *pulse_server = getenv("PULSE_SERVER"); const char *video_driver = SDL_GetCurrentVideoDriver(); fprintf(stderr, "%s: DISPLAY=%s WAYLAND_DISPLAY=%s XDG_RUNTIME_DIR=%s PULSE_SERVER=%s SDL_VIDEODRIVER=%s\n", phase ? phase : "startup", display && display[0] != '\0' ? display : "", wayland_display && wayland_display[0] != '\0' ? wayland_display : "", runtime_dir && runtime_dir[0] != '\0' ? runtime_dir : "", pulse_server && pulse_server[0] != '\0' ? pulse_server : "", video_driver && video_driver[0] != '\0' ? video_driver : ""); } static void destroy_video_texture(App *app) { if (app->video_texture) { SDL_DestroyTexture(app->video_texture); app->video_texture = NULL; app->texture_width = 0; app->texture_height = 0; } } static void begin_startup_handoff(App *app) { if (!app) { return; } app->startup_handoff_active = 1; app->startup_handoff_until = 0; } static int update_video_texture(App *app) { FrameData frame = {0}; double sync_clock; double lead_tolerance; sync_clock = player_get_sync_clock(&app->player, time(NULL)); lead_tolerance = app->startup_handoff_active ? 0.220 : 0.035; if (!player_consume_synced_frame(&app->player, &frame, sync_clock, lead_tolerance)) { if (!app->startup_handoff_active || !player_consume_latest_frame(&app->player, &frame)) { return 0; } } if (!app->video_texture || app->texture_width != frame.width || app->texture_height != frame.height) { destroy_video_texture(app); app->video_texture = SDL_CreateTexture(app->renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, frame.width, frame.height); if (!app->video_texture) { frame_data_free(&frame); return -1; } app->texture_width = frame.width; app->texture_height = frame.height; SDL_SetTextureBlendMode(app->video_texture, SDL_BLENDMODE_NONE); } SDL_UpdateYUVTexture(app->video_texture, NULL, frame.plane_y, frame.pitch_y, frame.plane_u, frame.pitch_u, frame.plane_v, frame.pitch_v); frame_data_free(&frame); if (app->startup_handoff_active && app->startup_handoff_until != 0 && SDL_GetTicks() >= app->startup_handoff_until) { app->startup_handoff_active = 0; app->startup_handoff_until = 0; } return 0; } static void tune_relative(App *app, int delta) { int next_index; if (app->channels.count == 0) { return; } next_index = app->player.current_index; if (next_index < 0) { next_index = 0; } next_index = (next_index + delta + app->channels.count) % app->channels.count; destroy_video_texture(app); begin_startup_handoff(app); player_tune(&app->player, next_index); } static void handle_event(App *app, const SDL_Event *event) { if (event->type == SDL_QUIT) { app->running = 0; return; } if (event->type != SDL_KEYDOWN || event->key.repeat) { return; } switch (event->key.keysym.sym) { case SDLK_ESCAPE: if (app->mode == MODE_GUIDE) { app->mode = MODE_FULLSCREEN; } else { app->running = 0; } break; case SDLK_TAB: app->mode = app->mode == MODE_FULLSCREEN ? MODE_GUIDE : MODE_FULLSCREEN; break; case SDLK_UP: tune_relative(app, -1); break; case SDLK_DOWN: tune_relative(app, 1); break; default: break; } } int app_init(App *app) { memset(app, 0, sizeof(*app)); app->running = 1; app->mode = MODE_FULLSCREEN; app->last_blackout_state = 1; app->app_start_time = time(NULL); configure_runtime_environment(); log_runtime_environment("startup-before-sdl"); if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_EVENTS) != 0) { fprintf(stderr, "SDL init failed: %s\n", SDL_GetError()); return -1; } log_runtime_environment("startup-after-sdl"); if (SDL_GetCurrentVideoDriver() && strcmp(SDL_GetCurrentVideoDriver(), "offscreen") == 0) { fprintf(stderr, "SDL selected offscreen video driver unexpectedly; refusing to continue without a visible display.\n"); return -1; } if (TTF_Init() != 0) { fprintf(stderr, "SDL_ttf init failed: %s\n", TTF_GetError()); return -1; } app->window = SDL_CreateWindow("Passport-C Media Player", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, WINDOW_WIDTH, WINDOW_HEIGHT, SDL_WINDOW_SHOWN); app->renderer = SDL_CreateRenderer(app->window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); if (!app->window || !app->renderer) { fprintf(stderr, "SDL window or renderer creation failed: %s\n", SDL_GetError()); return -1; } if (ui_load_fonts(&app->fonts) != 0) { fprintf(stderr, "Unable to load a fallback font.\n"); return -1; } if (channel_list_load(&app->channels, "./media") < 0) { fprintf(stderr, "Unable to scan ./media.\n"); return -1; } if (player_init(&app->player, &app->channels, app->app_start_time) != 0) { fprintf(stderr, "Unable to initialize player.\n"); return -1; } if (app->channels.count > 0) { begin_startup_handoff(app); player_tune(&app->player, 0); } return 0; } void app_run(App *app) { SDL_Event event; while (app->running) { int in_blackout; while (SDL_PollEvent(&event)) { handle_event(app, &event); } if (update_video_texture(app) != 0) { app->running = 0; break; } in_blackout = player_is_in_blackout(&app->player); if (app->last_blackout_state && !in_blackout) { app->startup_handoff_active = 1; app->startup_handoff_until = SDL_GetTicks() + 400; player_set_catchup_until(&app->player, app->startup_handoff_until); } app->last_blackout_state = in_blackout; if (app->channels.count == 0) { ui_render_no_media(app->renderer, &app->fonts); } else if (app->mode == MODE_GUIDE) { if (!in_blackout) { player_resume_audio(&app->player); } ui_render_guide(app->renderer, in_blackout ? NULL : app->video_texture, app->texture_width, app->texture_height, &app->fonts, &app->channels, app->player.current_index, app->app_start_time, time(NULL)); } else { if (!in_blackout) { player_resume_audio(&app->player); } ui_render_fullscreen(app->renderer, in_blackout ? NULL : app->video_texture, app->texture_width, app->texture_height); } SDL_RenderPresent(app->renderer); } } void app_destroy(App *app) { if (!app) { return; } player_destroy(&app->player); channel_list_destroy(&app->channels); destroy_video_texture(app); ui_destroy_fonts(&app->fonts); if (app->renderer) { SDL_DestroyRenderer(app->renderer); } if (app->window) { SDL_DestroyWindow(app->window); } TTF_Quit(); SDL_Quit(); }