From 0a250b05f3488ec0040b361bc8562724cc1fb4b9 Mon Sep 17 00:00:00 2001 From: markmental Date: Fri, 27 Mar 2026 22:59:37 -0400 Subject: [PATCH] Milestone, fix black screen and initial frame issues --- Makefile | 6 +- README.md | 2 +- src/app.c | 128 +++++++++-- src/app.h | 3 + src/app.o | Bin 6072 -> 10392 bytes src/frame_queue.c | 41 +++- src/frame_queue.h | 13 +- src/frame_queue.o | Bin 3520 -> 4432 bytes src/player.c | 548 +++++++++++++++++++++++++++++++++++++++++++--- src/player.h | 14 ++ src/player.o | Bin 10968 -> 21792 bytes 11 files changed, 708 insertions(+), 47 deletions(-) diff --git a/Makefile b/Makefile index cf24809..dd2d9d9 100644 --- a/Makefile +++ b/Makefile @@ -4,8 +4,8 @@ CFLAGS ?= -O2 -Wall -Wextra -Wpedantic $(CSTD) SDL_CFLAGS := $(shell pkg-config --cflags SDL2 SDL2_ttf 2>/dev/null) SDL_LIBS := $(shell pkg-config --libs SDL2 SDL2_ttf 2>/dev/null) -FFMPEG_CFLAGS := $(shell pkg-config --cflags libavformat libavcodec libswscale libavutil 2>/dev/null) -FFMPEG_LIBS := $(shell pkg-config --libs libavformat libavcodec libswscale libavutil 2>/dev/null) +FFMPEG_CFLAGS := $(shell pkg-config --cflags libavformat libavcodec libswscale libswresample libavutil 2>/dev/null) +FFMPEG_LIBS := $(shell pkg-config --libs libavformat libavcodec libswscale libswresample libavutil 2>/dev/null) MULTIARCH_CFLAGS := $(if $(wildcard /usr/include/i386-linux-gnu/SDL2/_real_SDL_config.h),-I/usr/include/i386-linux-gnu,) ifeq ($(strip $(SDL_CFLAGS)),) @@ -14,7 +14,7 @@ SDL_LIBS := $(shell sdl2-config --libs 2>/dev/null) -lSDL2_ttf endif ifeq ($(strip $(FFMPEG_LIBS)),) -FFMPEG_LIBS := -lavformat -lavcodec -lswscale -lavutil +FFMPEG_LIBS := -lavformat -lavcodec -lswscale -lswresample -lavutil endif SRC := $(wildcard src/*.c) diff --git a/README.md b/README.md index 4332f83..8051d5e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Install development packages for SDL2, SDL2_ttf, and FFmpeg headers before build Typical Debian or Ubuntu packages: ```bash -sudo apt install build-essential libsdl2-dev libsdl2-ttf-dev libavformat-dev libavcodec-dev libswscale-dev libavutil-dev +sudo apt install build-essential libsdl2-dev libsdl2-ttf-dev libavformat-dev libavcodec-dev libswscale-dev libswresample-dev libavutil-dev ``` Then build and run: diff --git a/src/app.c b/src/app.c index e3f060f..1d8357a 100644 --- a/src/app.c +++ b/src/app.c @@ -1,9 +1,57 @@ +#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); @@ -13,17 +61,33 @@ static void destroy_video_texture(App *app) { } } +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; - if (!player_consume_latest_frame(&app->player, &frame)) { - return 0; + 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_RGBA32, + SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, frame.width, frame.height); @@ -36,14 +100,22 @@ static int update_video_texture(App *app) { SDL_SetTextureBlendMode(app->video_texture, SDL_BLENDMODE_NONE); } - SDL_UpdateTexture(app->video_texture, NULL, frame.pixels, frame.stride); + 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); - return 0; -} -static void render_black(App *app) { - SDL_SetRenderDrawColor(app->renderer, 0, 0, 0, 255); - SDL_RenderClear(app->renderer); + 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) { @@ -58,6 +130,8 @@ static void tune_relative(App *app, int delta) { 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); } @@ -97,13 +171,23 @@ 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; @@ -138,6 +222,7 @@ int app_init(App *app) { } if (app->channels.count > 0) { + begin_startup_handoff(app); player_tune(&app->player, 0); } @@ -148,6 +233,8 @@ void app_run(App *app) { SDL_Event event; while (app->running) { + int in_blackout; + while (SDL_PollEvent(&event)) { handle_event(app, &event); } @@ -157,13 +244,22 @@ void app_run(App *app) { 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 (player_is_in_blackout(&app->player)) { - render_black(app); } else if (app->mode == MODE_GUIDE) { + if (!in_blackout) { + player_resume_audio(&app->player); + } ui_render_guide(app->renderer, - app->video_texture, + in_blackout ? NULL : app->video_texture, app->texture_width, app->texture_height, &app->fonts, @@ -172,7 +268,13 @@ void app_run(App *app) { app->app_start_time, time(NULL)); } else { - ui_render_fullscreen(app->renderer, app->video_texture, app->texture_width, app->texture_height); + 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); diff --git a/src/app.h b/src/app.h index 214c4f4..edfcdb9 100644 --- a/src/app.h +++ b/src/app.h @@ -16,6 +16,9 @@ typedef struct App { int texture_height; AppMode mode; int running; + int startup_handoff_active; + int last_blackout_state; + Uint32 startup_handoff_until; time_t app_start_time; ChannelList channels; Player player; diff --git a/src/app.o b/src/app.o index f4227c4e06a001746a54df2e68f92547830cc44d..d0b92bbb184bd94f588d12afa5356355032e086f 100644 GIT binary patch literal 10392 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 literal 6072 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 diff --git a/src/frame_queue.c b/src/frame_queue.c index 4122e4a..0a49fd7 100644 --- a/src/frame_queue.c +++ b/src/frame_queue.c @@ -1,5 +1,6 @@ #include "frame_queue.h" +#include #include #include @@ -8,7 +9,7 @@ void frame_data_free(FrameData *frame) { return; } - free(frame->pixels); + av_freep(&frame->buffer); memset(frame, 0, sizeof(*frame)); } @@ -110,3 +111,41 @@ int frame_queue_pop_latest(FrameQueue *queue, FrameData *out) { SDL_UnlockMutex(queue->mutex); return 1; } + +int frame_queue_peek_first(FrameQueue *queue, FrameData *out) { + if (!queue || !out || !queue->mutex) { + return 0; + } + + SDL_LockMutex(queue->mutex); + if (queue->count == 0) { + SDL_UnlockMutex(queue->mutex); + return 0; + } + + *out = queue->frames[queue->head]; + SDL_UnlockMutex(queue->mutex); + return 1; +} + +int frame_queue_pop_first(FrameQueue *queue, FrameData *out) { + if (!queue || !out || !queue->mutex) { + return 0; + } + + SDL_LockMutex(queue->mutex); + if (queue->count == 0) { + SDL_UnlockMutex(queue->mutex); + return 0; + } + + *out = queue->frames[queue->head]; + memset(&queue->frames[queue->head], 0, sizeof(queue->frames[queue->head])); + queue->head = (queue->head + 1) % FRAME_QUEUE_CAPACITY; + queue->count -= 1; + if (queue->count == 0) { + queue->head = 0; + } + SDL_UnlockMutex(queue->mutex); + return 1; +} diff --git a/src/frame_queue.h b/src/frame_queue.h index d028bc8..cfde523 100644 --- a/src/frame_queue.h +++ b/src/frame_queue.h @@ -3,13 +3,18 @@ #include -#define FRAME_QUEUE_CAPACITY 4 +#define FRAME_QUEUE_CAPACITY 8 typedef struct FrameData { - unsigned char *pixels; + unsigned char *buffer; + unsigned char *plane_y; + unsigned char *plane_u; + unsigned char *plane_v; int width; int height; - int stride; + int pitch_y; + int pitch_u; + int pitch_v; double pts_seconds; } FrameData; @@ -26,6 +31,8 @@ void frame_queue_destroy(FrameQueue *queue); void frame_queue_clear(FrameQueue *queue); int frame_queue_push(FrameQueue *queue, FrameData *frame); int frame_queue_pop_latest(FrameQueue *queue, FrameData *out); +int frame_queue_peek_first(FrameQueue *queue, FrameData *out); +int frame_queue_pop_first(FrameQueue *queue, FrameData *out); void frame_data_free(FrameData *frame); #endif diff --git a/src/frame_queue.o b/src/frame_queue.o index db9520678bf84209ba07b218bb79d132d6774ba6..3eb478dcb57bff165aa8e25a558096212ffb0188 100644 GIT binary patch literal 4432 zcmbtWZ){Ul6u++>u2OujGk%x>vJxg?2{fJZK?pG|?bsVkM8|Yw%%*g>GYEbp|8> z%xi&XZL$O~5|QXv#Y7V#BuH$nGCrtTVoXp&T!Mz-jADj31)1mEKi%E4Rih{AyXXGy zIp?19yXW3*e@pv%hoTT63VDRg&IC%xW_4B%P&Ggn5I51YGwG!rx^-&;!MOHQL926W zbs-P@yzujPm$Zg3A@Dtb7HA(wdp*D3)m+d=Tc;^(eW{Ngayd{3BlGIC-t^lG#d-jW z*PK)OTQt0GnP|dv^BgtTj(wBT0;Q$ z*D_~d+NoQAbm-;}Ezisd z=d$RN?^3Kw#p#CP#A2mnUa4&=v8Wb}2pL*@ll^awQU^>@&z+~)m>;>id#)h@TLXVO z1xh0o>PdH(`PXyjkOm}q9%Cs-S(1SKUY9=V#K6ctWeq2c@>}`bEaiA|q{4n$W*Kxy za7&wYqq9TL-qbQ0%z;js$=eW znEr}h90y`2v^s)|;YR48fQPBjN&zoZ{h<1>(SllCO(T53kbb@N>jwsr1Cx@ga26=3 zRBtYH_XM{=X{;ycp80L_Apd_j&RiY~F$;>6bN=xeixy_lDz7~N9mmd#Hs9n?^WvNi zjE~SZYo+`FdDax=Zw1WTtlKhw_X9-p4dyb9tQWP+ufSZ;nQ6rCg8soo@+&;%cePDl z=&_Hnly+r&N=p*BgD?c}5_(XOfMjGBX+=I~ciFc$m^(ptn2f^t^L}qv zuKW!_=v}$vMckmpVnWtIOBA%vv-Y%9$L1_z*t zW!?p3PL5jUBtTN0*@BkYk5+P$ zmmn25N17xaCQTAglqQL%-BRhabF}i@5#C1@oerL#v2O(6HNfhf;EL6qGdW#NKa*9e?{ zJ>ZM@S`&`zRgdY|rn=Ub=-R`4ZC82Cu~l`omk{5=*NQl9P(247VYS|LhE=cQ_~*q<9!&r2Bhpfjj? zfymPsS@o<_U2SehM0J%ee1f@*!w;pS>=P{eI2#mjf=$3F?B1l-hp5^Du4EPXEJ7cU zBlwwHM=x`P)I;tz>WC^-u@+3XaKEX)`n_KEYjtl19d-oAu)7NrM)PWdA#2FbGnLkz z75;P3!0=uPr)LOQ1N=M{f-L9u$TJE(R~T2ruabRR6}$^@-1i+AQHpFJ&%#(K&h9Gs zVZbrY0nTNV$2n2O9($uw{PR4{bsmTI4q%^EvA@ji{q#>Y?;F5waD1c&6cE}IFuEZw z=INhDVM5Ow)p#m8U_^Ee8^eY##%I0JbTkrA83u_yN5?_gEYg?gOOuXpdnA-HqG{vN z;k2<^nun5!-Z}QMej}Qq2JOk%&RO%$M1OfMr4i{hhSI6z9_kThI*o~2@;M74I}HvG z?O;j3_8ooCB%=Luk{C=5M*1P!q4ZqXFm^`beJMK()(mBIk@4vb`6J0zsNMRdkN;EsDVXY)bGQLJ_!lkB&09jEnb0tP6YbUVLk_7w>|X=PB&P zyU=CB#k)X!Lwg3&(d{s%Q*7KJw3N{w^}$_F6Cb`1#J7EDi1=d3fdL}{SR$D=e65>0 zR}Q74v7N+6&5a%KfWQmG)BE3p7b0Nlc-LeJ@X1o!g8i*s2zJ;L!k)Vkmz!zJkth)C z4crIs97;VK$TAoa=WWeVLSnXk#R8{UOvwt+Fy?>tZw6nCDf$OgDq?>xpNm24FUB>n zAar4aqxmoMe~kNcn#v&#bASAwOcuePz&3zViGM#nRv-oXW&Gp2Df|Wh7~Z}&y3}*S ri+q2)lgP2%AJ5g!JwRj}7eJQS^0!S%9oSUzzs~dDN~6qv?EZfO07!-2 delta 1475 zcmZ`(UuauZ7{BMvl2#Tg5!M&of1AJhlYhxuR?AI9d`jj^o{eF&mV^ufrGrR#%YWo&jMe&2VKEuemI&-wnH z-}!yNdz1S8-pJyZ>V?WXhDSyY3ujBX2bv1lEdSl(5sgPC%xrB|e#VB%3-JSQV+l;y zS8s^%n{QMr5LDN-P4Ow65{`{6zHeWkc7En-QS_j)sFc_C=P)OVEnYa?X>^MPaEso_ zY4`m+c(Xi#322-7SyZp|2(}^IM+mcvm0Qe1OX(%%{8+t_s5ZB=nteqZulZ;WA=ypl zw83*jir0DJ7l?&(Tez3*sIvwsy3jp? zVu%@HZ~q-wIL7Lm7M}|w->Y8R&gynU8?TFss5U^TCXXmsoJ^WLk~UE;i$?-USRf_b zi^$H-ktVD?4@p6O%F9k$M{#`A0s%`$dzAmx-Yp?B+hbisN>eR|m^wszeX7-J`FQ8R zYi)dBf7`0b8Er84AVUp60B4gl=!ZDt-0IfgW=8|dEhO=-qQ?uDVYjqGG7mevU%b1t z)3krn#(xkq%gFi}*4X583Vgk=r9p@Z5(1<_P1;yF{NUy>K%k8y{>&s}rgU%MKZfq~ zTJ9+#^RK}z#|N{VcYU_soiz6^A3vK}_49C#l5xLE;yZ5AcbGi~Lo28`PvpV7TgD)p z^88X-zS4Wh7{%O^hH0xu41GeL?|n2@4jD14W|-EBp&txSLIA#@KP`FRuDu#!A2iHU zDiq$-EaN^qpq|DPhWWUmKNUut+REBKXluZOwnxtQZ5vj%pbKt-E)jm#h)t0D1oX-& zV_5Fi(?KEw2Zm);kFta;!@8UNG_KM#14DxfQ^qfCf}00~$*n|BY14`Ru-H3(u z>3*uhgxdz=^mNpV;wi|yJz`hs-)sO=2)=O#)`?5`Eam~EWj4!JVZ-9epHd4 YLnQkAXhWuWLYk3S@Ku}$>}AaS1Jp8{LI3~& diff --git a/src/player.c b/src/player.c index 3083f67..ed30848 100644 --- a/src/player.c +++ b/src/player.c @@ -1,10 +1,16 @@ #include "player.h" #include +#include #include +#include +#include +#include +#include #include #include #include +#include #include #include #include @@ -28,23 +34,304 @@ static int should_stop(Player *player) { return player ? SDL_AtomicGet(&player->stop_requested) : 1; } +static double player_audio_bytes_per_second(const Player *player) { + if (!player || player->audio_spec.freq <= 0 || player->audio_spec.channels <= 0) { + return 0.0; + } + + return (double) player->audio_spec.freq * (double) player->audio_spec.channels * (double) sizeof(int16_t); +} + +static void player_reset_clock_state(Player *player) { + if (!player || !player->clock_mutex) { + return; + } + + SDL_LockMutex(player->clock_mutex); + player->latest_audio_end_pts = 0.0; + player->audio_started = 0; + player->preroll_ready = 0; + SDL_UnlockMutex(player->clock_mutex); +} + +static void player_mark_preroll_ready(Player *player) { + Uint32 minimum_release; + int already_ready; + + if (!player || !player->clock_mutex) { + return; + } + + minimum_release = SDL_GetTicks() + 120; + SDL_LockMutex(player->clock_mutex); + already_ready = player->preroll_ready; + player->preroll_ready = 1; + SDL_UnlockMutex(player->clock_mutex); + if (!already_ready && player->tuning_blackout_until < minimum_release) { + player->tuning_blackout_until = minimum_release; + } +} + +static void player_clear_audio(Player *player) { + if (!player || !player->audio_mutex) { + return; + } + + SDL_LockMutex(player->audio_mutex); + if (player->audio_device != 0) { + SDL_ClearQueuedAudio(player->audio_device); + SDL_PauseAudioDevice(player->audio_device, 1); + } + SDL_UnlockMutex(player->audio_mutex); + player_reset_clock_state(player); +} + +static void player_close_audio(Player *player) { + if (!player || !player->audio_mutex) { + return; + } + + SDL_LockMutex(player->audio_mutex); + if (player->audio_device != 0) { + SDL_ClearQueuedAudio(player->audio_device); + SDL_CloseAudioDevice(player->audio_device); + player->audio_device = 0; + memset(&player->audio_spec, 0, sizeof(player->audio_spec)); + } + SDL_UnlockMutex(player->audio_mutex); +} + +static int player_ensure_audio_backend(Player *player) { + static const char *driver_candidates[] = { + "pulseaudio", + "pipewire", + "alsa" + }; + const char *current_driver; + char error_buffer[256] = {0}; + + if (!player) { + return -1; + } + + current_driver = SDL_GetCurrentAudioDriver(); + if (current_driver && current_driver[0] != '\0') { + snprintf(player->audio_driver, sizeof(player->audio_driver), "%s", current_driver); + return 0; + } + + for (size_t i = 0; i < sizeof(driver_candidates) / sizeof(driver_candidates[0]); ++i) { + if (SDL_AudioInit(driver_candidates[i]) == 0) { + snprintf(player->audio_driver, sizeof(player->audio_driver), "%s", driver_candidates[i]); + fprintf(stderr, "audio: using SDL audio driver %s\n", player->audio_driver); + return 0; + } + + if (error_buffer[0] == '\0') { + snprintf(error_buffer, + sizeof(error_buffer), + "%s audio init failed: %s", + driver_candidates[i], + SDL_GetError()); + } + SDL_AudioQuit(); + } + + player_set_error(player, error_buffer[0] != '\0' ? error_buffer : "Unable to initialize any SDL audio driver"); + return -1; +} + +static int player_ensure_audio_device(Player *player) { + SDL_AudioSpec desired; + SDL_AudioSpec obtained; + int result = 0; + + if (!player || !player->audio_mutex) { + return -1; + } + + if (player_ensure_audio_backend(player) != 0) { + return -1; + } + + SDL_zero(desired); + desired.freq = 48000; + desired.format = AUDIO_S16SYS; + desired.channels = 2; + desired.samples = 1024; + desired.callback = NULL; + + SDL_LockMutex(player->audio_mutex); + if (player->audio_device == 0) { + player->audio_device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0); + if (player->audio_device == 0) { + result = -1; + } else { + player->audio_spec = obtained; + } + } + SDL_UnlockMutex(player->audio_mutex); + + if (result != 0) { + player_set_error(player, SDL_GetError()); + } + + return result; +} + +static int queue_audio_frame(Player *player, + SwrContext **swr_context, + AVCodecContext *audio_codec_context, + AVRational audio_time_base, + AVFrame *audio_frame, + int *queued_any_audio) { + uint8_t **converted_data = NULL; + int out_rate; + int out_channels; + enum AVSampleFormat out_format; + AVChannelLayout out_layout; + int swr_result; + int max_samples; + int sample_count; + int buffer_size; + int queued_limit; + int line_size = 0; + int result; + double audio_pts; + double frame_duration; + + if (!player || !audio_codec_context || !audio_frame) { + return -1; + } + + if (player_ensure_audio_device(player) != 0) { + return -1; + } + + out_rate = player->audio_spec.freq; + out_channels = player->audio_spec.channels; + out_format = AV_SAMPLE_FMT_S16; + av_channel_layout_default(&out_layout, out_channels); + + if (!*swr_context) { + swr_result = swr_alloc_set_opts2(swr_context, + &out_layout, + out_format, + out_rate, + &audio_codec_context->ch_layout, + audio_codec_context->sample_fmt, + audio_codec_context->sample_rate, + 0, + NULL); + if (swr_result < 0 || !*swr_context || swr_init(*swr_context) < 0) { + av_channel_layout_uninit(&out_layout); + player_set_error(player, "Unable to initialize audio resampler"); + return -1; + } + } + + max_samples = (int) av_rescale_rnd(swr_get_delay(*swr_context, audio_codec_context->sample_rate) + audio_frame->nb_samples, + out_rate, + audio_codec_context->sample_rate, + AV_ROUND_UP); + + if (av_samples_alloc_array_and_samples(&converted_data, + &line_size, + out_channels, + max_samples, + out_format, + 0) < 0) { + av_channel_layout_uninit(&out_layout); + player_set_error(player, "Unable to allocate audio buffer"); + return -1; + } + + sample_count = swr_convert(*swr_context, + converted_data, + max_samples, + (const uint8_t * const *) audio_frame->extended_data, + audio_frame->nb_samples); + if (sample_count < 0) { + av_freep(&converted_data[0]); + av_freep(&converted_data); + av_channel_layout_uninit(&out_layout); + player_set_error(player, "Audio resample failed"); + return -1; + } + + buffer_size = av_samples_get_buffer_size(&line_size, out_channels, sample_count, out_format, 1); + if (buffer_size <= 0) { + av_freep(&converted_data[0]); + av_freep(&converted_data); + av_channel_layout_uninit(&out_layout); + return 0; + } + + audio_pts = audio_frame->best_effort_timestamp == AV_NOPTS_VALUE + ? NAN + : audio_frame->best_effort_timestamp * av_q2d(audio_time_base); + frame_duration = (double) sample_count / (double) out_rate; + + queued_limit = (int) (player_audio_bytes_per_second(player) * 0.12); + while (!should_stop(player) && (int) SDL_GetQueuedAudioSize(player->audio_device) > queued_limit) { + SDL_Delay(2); + } + + result = SDL_QueueAudio(player->audio_device, converted_data[0], (Uint32) buffer_size); + av_freep(&converted_data[0]); + av_freep(&converted_data); + + if (result != 0) { + av_channel_layout_uninit(&out_layout); + player_set_error(player, SDL_GetError()); + return -1; + } + + SDL_LockMutex(player->clock_mutex); + if (!isnan(audio_pts)) { + player->latest_audio_end_pts = audio_pts + frame_duration; + } else { + player->latest_audio_end_pts += frame_duration; + } + SDL_UnlockMutex(player->clock_mutex); + + if (queued_any_audio) { + *queued_any_audio = 1; + } + + av_channel_layout_uninit(&out_layout); + return 0; +} + static int decode_thread_main(void *userdata) { DecoderThreadArgs *args = (DecoderThreadArgs *) userdata; Player *player = args->player; const Channel *channel = args->channel; AVFormatContext *format_context = NULL; AVCodecContext *codec_context = NULL; + AVCodecContext *audio_codec_context = NULL; AVPacket *packet = NULL; AVFrame *decoded_frame = NULL; + AVFrame *audio_frame = NULL; struct SwsContext *sws_context = NULL; + SwrContext *swr_context = NULL; const AVCodec *codec = NULL; + const AVCodec *audio_codec = NULL; int video_stream_index = -1; + int audio_stream_index = -1; int rc = -1; - int rgb_stride = 0; double seek_seconds = 0.0; AVRational time_base; - Uint64 wall_base_ms = 0; - int first_frame = 1; + AVRational audio_time_base = {0}; + int queued_any_audio = 0; + int queued_first_video = 0; + + #define MAYBE_MARK_PREROLL_READY() \ + do { \ + if (queued_first_video && (queued_any_audio || !player->has_audio_stream)) { \ + player_mark_preroll_ready(player); \ + } \ + } while (0) free(args); @@ -61,7 +348,8 @@ static int decode_thread_main(void *userdata) { for (unsigned int i = 0; i < format_context->nb_streams; ++i) { if (format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream_index = (int) i; - break; + } else if (format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && audio_stream_index < 0) { + audio_stream_index = (int) i; } } @@ -88,21 +376,41 @@ static int decode_thread_main(void *userdata) { goto cleanup; } + if (audio_stream_index >= 0) { + audio_codec = avcodec_find_decoder(format_context->streams[audio_stream_index]->codecpar->codec_id); + if (audio_codec) { + audio_codec_context = avcodec_alloc_context3(audio_codec); + if (!audio_codec_context) { + player_set_error(player, "Unable to allocate audio decoder context"); + goto cleanup; + } + + if (avcodec_parameters_to_context(audio_codec_context, format_context->streams[audio_stream_index]->codecpar) < 0 || + avcodec_open2(audio_codec_context, audio_codec, NULL) < 0) { + player_set_error(player, "Unable to initialize audio decoder"); + goto cleanup; + } + + audio_time_base = format_context->streams[audio_stream_index]->time_base; + } + } + decoded_frame = av_frame_alloc(); + audio_frame = av_frame_alloc(); packet = av_packet_alloc(); - if (!decoded_frame || !packet) { + if (!decoded_frame || !audio_frame || !packet) { player_set_error(player, "Unable to allocate FFmpeg frame buffers"); goto cleanup; } - rgb_stride = codec_context->width * 4; time_base = format_context->streams[video_stream_index]->time_base; + player->has_audio_stream = audio_codec_context != NULL; sws_context = sws_getContext(codec_context->width, codec_context->height, codec_context->pix_fmt, codec_context->width, codec_context->height, - AV_PIX_FMT_RGBA, + AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, @@ -117,20 +425,51 @@ static int decode_thread_main(void *userdata) { int64_t seek_target = (int64_t) (seek_seconds / av_q2d(time_base)); avformat_seek_file(format_context, video_stream_index, INT64_MIN, seek_target, INT64_MAX, 0); avcodec_flush_buffers(codec_context); + if (audio_codec_context) { + avcodec_flush_buffers(audio_codec_context); + } + player_clear_audio(player); } while (!should_stop(player)) { if (av_read_frame(format_context, packet) < 0) { if (channel->duration_seconds > 0.0) { seek_seconds = 0.0; - wall_base_ms = SDL_GetTicks64(); avformat_seek_file(format_context, video_stream_index, INT64_MIN, 0, INT64_MAX, 0); avcodec_flush_buffers(codec_context); + if (audio_codec_context) { + avcodec_flush_buffers(audio_codec_context); + } + player_clear_audio(player); continue; } break; } + if (packet->stream_index == audio_stream_index && audio_codec_context) { + if (avcodec_send_packet(audio_codec_context, packet) >= 0) { + while (!should_stop(player)) { + int receive_audio = avcodec_receive_frame(audio_codec_context, audio_frame); + if (receive_audio == AVERROR(EAGAIN) || receive_audio == AVERROR_EOF) { + break; + } + if (receive_audio < 0) { + player_set_error(player, "Audio decode failed"); + av_packet_unref(packet); + goto cleanup; + } + if (queue_audio_frame(player, &swr_context, audio_codec_context, audio_time_base, audio_frame, &queued_any_audio) != 0) { + av_packet_unref(packet); + goto cleanup; + } + MAYBE_MARK_PREROLL_READY(); + av_frame_unref(audio_frame); + } + } + av_packet_unref(packet); + continue; + } + if (packet->stream_index != video_stream_index) { av_packet_unref(packet); continue; @@ -144,7 +483,6 @@ static int decode_thread_main(void *userdata) { while (!should_stop(player)) { double frame_seconds; - int delay_ms; int receive = avcodec_receive_frame(codec_context, decoded_frame); if (receive == AVERROR(EAGAIN) || receive == AVERROR_EOF) { break; @@ -158,33 +496,34 @@ static int decode_thread_main(void *userdata) { ? seek_seconds : decoded_frame->best_effort_timestamp * av_q2d(time_base); - if (first_frame) { - wall_base_ms = SDL_GetTicks64(); - first_frame = 0; - } - - delay_ms = (int) (((frame_seconds - seek_seconds) * 1000.0) - (double) (SDL_GetTicks64() - wall_base_ms)); - if (delay_ms > 1 && delay_ms < 250) { - SDL_Delay((Uint32) delay_ms); - } - { FrameData frame = {0}; uint8_t *dest_data[4] = {0}; int dest_linesize[4] = {0}; + int image_size; frame.width = codec_context->width; frame.height = codec_context->height; - frame.stride = rgb_stride; frame.pts_seconds = frame_seconds; - frame.pixels = malloc((size_t) frame.stride * (size_t) frame.height); - if (!frame.pixels) { + image_size = av_image_alloc(dest_data, + dest_linesize, + frame.width, + frame.height, + AV_PIX_FMT_YUV420P, + 1); + if (image_size < 0) { player_set_error(player, "Unable to allocate frame buffer"); goto cleanup; } - dest_data[0] = frame.pixels; - dest_linesize[0] = frame.stride; + frame.buffer = dest_data[0]; + frame.plane_y = dest_data[0]; + frame.plane_u = dest_data[1]; + frame.plane_v = dest_data[2]; + frame.pitch_y = dest_linesize[0]; + frame.pitch_u = dest_linesize[1]; + frame.pitch_v = dest_linesize[2]; + sws_scale(sws_context, (const uint8_t *const *) decoded_frame->data, decoded_frame->linesize, @@ -193,6 +532,10 @@ static int decode_thread_main(void *userdata) { dest_data, dest_linesize); frame_queue_push(&player->frame_queue, &frame); + if (!queued_first_video) { + queued_first_video = 1; + } + MAYBE_MARK_PREROLL_READY(); } } } @@ -206,16 +549,28 @@ cleanup: if (decoded_frame) { av_frame_free(&decoded_frame); } + if (audio_frame) { + av_frame_free(&audio_frame); + } if (codec_context) { avcodec_free_context(&codec_context); } + if (audio_codec_context) { + avcodec_free_context(&audio_codec_context); + } if (format_context) { avformat_close_input(&format_context); } if (sws_context) { sws_freeContext(sws_context); } + if (swr_context) { + swr_free(&swr_context); + } + player->has_audio_stream = 0; return rc; + + #undef MAYBE_MARK_PREROLL_READY } static void player_stop_thread(Player *player) { @@ -237,12 +592,25 @@ int player_init(Player *player, const ChannelList *channels, time_t app_start_ti player->channels = channels; player->current_index = -1; player->app_start_time = app_start_time; + player->audio_mutex = SDL_CreateMutex(); + player->clock_mutex = SDL_CreateMutex(); player->error_mutex = SDL_CreateMutex(); - if (!player->error_mutex) { + if (!player->audio_mutex || !player->clock_mutex || !player->error_mutex) { + if (player->audio_mutex) { + SDL_DestroyMutex(player->audio_mutex); + } + if (player->clock_mutex) { + SDL_DestroyMutex(player->clock_mutex); + } + if (player->error_mutex) { + SDL_DestroyMutex(player->error_mutex); + } return -1; } if (frame_queue_init(&player->frame_queue) != 0) { + SDL_DestroyMutex(player->audio_mutex); + SDL_DestroyMutex(player->clock_mutex); SDL_DestroyMutex(player->error_mutex); memset(player, 0, sizeof(*player)); return -1; @@ -257,7 +625,14 @@ void player_destroy(Player *player) { } player_stop_thread(player); + player_close_audio(player); frame_queue_destroy(&player->frame_queue); + if (player->audio_mutex) { + SDL_DestroyMutex(player->audio_mutex); + } + if (player->clock_mutex) { + SDL_DestroyMutex(player->clock_mutex); + } if (player->error_mutex) { SDL_DestroyMutex(player->error_mutex); } @@ -273,9 +648,12 @@ int player_tune(Player *player, int channel_index) { player_stop_thread(player); frame_queue_clear(&player->frame_queue); + player_clear_audio(player); + player->has_audio_stream = 0; SDL_AtomicSet(&player->stop_requested, 0); player->current_index = channel_index; player->tuning_blackout_until = SDL_GetTicks() + 200; + player->catchup_until = player->tuning_blackout_until + 350; player_set_error(player, ""); args = malloc(sizeof(*args)); @@ -304,6 +682,52 @@ int player_consume_latest_frame(Player *player, FrameData *out) { return frame_queue_pop_latest(&player->frame_queue, out); } +int player_consume_synced_frame(Player *player, FrameData *out, double clock_seconds, double lead_tolerance) { + FrameData candidate = {0}; + FrameData selected = {0}; + int found = 0; + double late_tolerance; + + if (!player || !out) { + return 0; + } + + late_tolerance = SDL_GetTicks() < player->catchup_until ? 0.180 : 0.090; + + while (frame_queue_peek_first(&player->frame_queue, &candidate)) { + if (candidate.pts_seconds < clock_seconds - late_tolerance) { + if (!frame_queue_pop_first(&player->frame_queue, &candidate)) { + break; + } + frame_data_free(&candidate); + memset(&candidate, 0, sizeof(candidate)); + continue; + } + + if (candidate.pts_seconds > clock_seconds + lead_tolerance) { + break; + } + + if (!frame_queue_pop_first(&player->frame_queue, &candidate)) { + break; + } + + if (found) { + frame_data_free(&selected); + } + selected = candidate; + memset(&candidate, 0, sizeof(candidate)); + found = 1; + } + + if (found) { + *out = selected; + return 1; + } + + return 0; +} + double player_live_position(const Player *player, time_t now) { if (!player || !player->channels || player->current_index < 0 || player->current_index >= player->channels->count) { return 0.0; @@ -312,8 +736,80 @@ double player_live_position(const Player *player, time_t now) { return channel_live_position(&player->channels->items[player->current_index], player->app_start_time, now); } +double player_get_sync_clock(Player *player, time_t now) { + double clock_seconds; + double queued_seconds = 0.0; + double bytes_per_second; + int audio_started; + + if (!player) { + return 0.0; + } + + if (!player->has_audio_stream || player->audio_device == 0) { + return player_live_position(player, now); + } + + bytes_per_second = player_audio_bytes_per_second(player); + if (bytes_per_second <= 0.0) { + return player_live_position(player, now); + } + + SDL_LockMutex(player->clock_mutex); + clock_seconds = player->latest_audio_end_pts; + audio_started = player->audio_started; + SDL_UnlockMutex(player->clock_mutex); + + if (!audio_started) { + return player_live_position(player, now); + } + + queued_seconds = (double) SDL_GetQueuedAudioSize(player->audio_device) / bytes_per_second; + clock_seconds -= queued_seconds; + if (clock_seconds < 0.0) { + clock_seconds = 0.0; + } + + return clock_seconds; +} + +void player_set_catchup_until(Player *player, Uint32 tick) { + if (!player) { + return; + } + + player->catchup_until = tick; +} + int player_is_in_blackout(const Player *player) { - return player && SDL_GetTicks() < player->tuning_blackout_until; + int in_blackout; + int ready; + + if (!player) { + return 0; + } + + SDL_LockMutex(player->clock_mutex); + ready = player->preroll_ready; + SDL_UnlockMutex(player->clock_mutex); + + in_blackout = SDL_GetTicks() < player->tuning_blackout_until || !ready; + return in_blackout; +} + +void player_resume_audio(Player *player) { + if (!player || !player->audio_mutex || !player->has_audio_stream) { + return; + } + + SDL_LockMutex(player->audio_mutex); + if (player->audio_device != 0 && SDL_GetQueuedAudioSize(player->audio_device) > 0) { + SDL_PauseAudioDevice(player->audio_device, 0); + SDL_LockMutex(player->clock_mutex); + player->audio_started = 1; + SDL_UnlockMutex(player->clock_mutex); + } + SDL_UnlockMutex(player->audio_mutex); } void player_get_error(Player *player, char *buffer, size_t buffer_size) { diff --git a/src/player.h b/src/player.h index b7fd911..78b8ece 100644 --- a/src/player.h +++ b/src/player.h @@ -15,6 +15,16 @@ typedef struct Player { int current_index; time_t app_start_time; Uint32 tuning_blackout_until; + SDL_AudioDeviceID audio_device; + SDL_AudioSpec audio_spec; + SDL_mutex *audio_mutex; + SDL_mutex *clock_mutex; + double latest_audio_end_pts; + int has_audio_stream; + int audio_started; + int preroll_ready; + Uint32 catchup_until; + char audio_driver[32]; SDL_mutex *error_mutex; char last_error[256]; } Player; @@ -23,8 +33,12 @@ int player_init(Player *player, const ChannelList *channels, time_t app_start_ti void player_destroy(Player *player); int player_tune(Player *player, int channel_index); int player_consume_latest_frame(Player *player, FrameData *out); +int player_consume_synced_frame(Player *player, FrameData *out, double clock_seconds, double lead_tolerance); double player_live_position(const Player *player, time_t now); +double player_get_sync_clock(Player *player, time_t now); +void player_set_catchup_until(Player *player, Uint32 tick); int player_is_in_blackout(const Player *player); +void player_resume_audio(Player *player); void player_get_error(Player *player, char *buffer, size_t buffer_size); #endif diff --git a/src/player.o b/src/player.o index 9ce72460399a342fab4e91e75c1bc880cd045fc0..c9f8d2a286ceba69b4cad285c103e6f5f87eed0b 100644 GIT binary patch literal 21792 zcmbtb4|r77m4754A=r7rs*7!{({|_vS7HKK1OCg93B0iZQHVxChas6EDM@BJe-x@) zqcZ_shiKgD+O2Jues*_pwY&6N>tdlA{z#zPDy7{@+g)W{TA5OnQlF(?w;FUwd&R)5ezw8-WRto%2VCi7mwGOvVBn#57iM|2?=t~Cc)f-B$>sA8zJs< zTniN*-GbUkj-I5UQ`kk#+}JX3JUe5sWy6A86QJ~2F6XOm4X)m2U(^p`%jD44&Tj_5 zDi@yZ0zKK_CgxMv6+(SrG`k$!sYwb#WcA~Kk=zE*P;y3cd0kw^iW?nQk?JAMNU?v0 zq-Q=S|7NJ%9>JS}HwN3-Aou+G1SnezcUQF*E_X&kfemV;e4w8SNX|$;utgQQJS56r z66I~8yj_=-B7@-CaH!!(c<_#pM1H<-~m(M0E-vx>@CG7ZQ!qBVWTrzOT z&ToZ?P9a(U-BF~5;oxq{Po7tr@1VQi$qi3(797lc%qg6Ez$x5NQ#fbfNamNhcPC|! z54>hOCp1C!WlW>JWg$3*JO0BdOn%8JG}RO~Oy!p?vv_O(AFS9aG&guApB%#{&*tW*J%aq z8JlP^huuvFC{sPODo+N;lXlTmwluuvfaM3A!QBihirnr1`sBt)*m8mA@Q|`EQ;I&K@3|Q*6eXJW584r$Z>No50p8d3V+@rJy1aA%A z7K~sAz&gY-s->>TFQJ?}(xiz`R46fX`~7AarLR(I5K_cC%iXxH5D%&@G}_KZ+qX54;bIBs8h z(B6Coizhs|$q#ZEpq^ww?odukzb51!Tk5_Ubg@kD3aFLi6qGl(`{1DiU!F$#VRm>N z^4U9KW=YrrLi&W{Kcq`A4hw?Wc(Z$7vKE-=%|s?&Ywn2 z@_7aIKgnl=_&hO=&(*MzFS2_1Gj& zI%{l3mb$+zUJ1(5eyr>Rilv2rqCJ8~#_F>3=Yb3>4CWjsn)CG9Y&;%IrHwdY_dc2< zBb7Aa`{t%N{2p0wcYub1EOdm4FJ^t?pfcMrRjjDD{Wnwob^+1X@FdOrYNF2{aXVVLJj~!vw(!sAwSKy^Y8qEDhS`3@&fNrpf|1gHN(1t?sK_zvbqZfiu~&gS0zhRtJW&I0Ifi zaqz9t(crUS25x4Xg6#Q5$X>v-()_lDF#Ap`+8c|P!d=Xy@aBb$S4~dH{bhJ39dk%` zJKq6EO8sMVhS|m0jM+PgnY-kWlCs?MwxADr_7rYz#fJCSkg)j0clB;j{3n!n;eGw> z@f*qmsB0!DknaiwxO_{FQu@QY+TZHsp9yu=7uY zn04KOLKr7r7v#3={MS*XNg#hXMhJ4{wRv_$s*@X0?$|vGp)bTvIh=Bz}CU52oD#g>RVkS(~(!XxO(;PW|^Tz z4~V%boExdq??vGSBiZK+3GH%&Ut#j>RA=x)#kO=Gt5cl^pP{|Zaeq4gMM^b}yNiiy zSMdxcTEEYbXE(vwc{9TlOR{U&L2yXJ&l?Vf+%psTI1e>$w==#VUHR1$|8^SQhFPCU ziWHCkj<1Aj8~$&2xjfcxGJa^hm&wXbG1Erp$9`i>bCwr}z>>XZI(SLm@4ZvDQvMWg zS~YU4AHHv_fEp`v)z~ioqMPwdkPl(7XZ+|x zH@tRS5p@r9s(gpq8qVM#VMYh?fdc?4NH$u!!JRez*givUKCmBn@M#NoNa!$(NrzlYr4n45(A zJ*QC5U1^gc`?>pc)fjx>=k~yl(36}biJY5p{B{P`@D-~*Zr2^Rzwm1|R{MTKhWjtB zFdIw<_8;zwnV@Z}9`qL8IcDsl0^J_I@Y?7o-o6kYQ15U|U3?LHrV$>37Z>41U`$i| zC@K(Q4me<@X7mKs6u(0EBzVnsKy)1nx$k_3MhF!KS0n!nPLzQ)Tqq2Xqb_$KP& zkeHwRUS(@5JSw*VmcpC&;AJ3H#w}L9N5*6T{y+-wd%+&!q-eo`IhD0xycVjED*T=FAXlH- zFwEadgo-p4$#5ylYL>MY0(K^u42miRy_iyY`M1otFvXXD0k=Ovj%vR5Os9zQS?c>x z!|-_jEq(jg4vqLKQ$k{7 z-R3O<-9NCL_xQB1P`mPZmXhS4G6lx*9Lq56yN*Hn(S2 z+OwO%yBWNj>x*yUC>nAG@6uQD;?)>qh{aih?Y>26lcZt|P$ezZpde~ABUGy|eir>o z_et`xE1bf^?6%l?FD-1Q6*4fqVb&Fks{YdN6z*?C-RXGS=^iDgb}AP-?rD4*QT{`M zN)O^mp9(VuZ!}*uYYGq38l<<7mmox8v$`JH`NLFum^Kc&CGSPeerhwlh0q;Y%?}06 z`J#Tj{CI{`^bqfB2|cm>t;bIrOVRgVQi8?NA{QRtVi1O};7_aeUkzjbd~h!v ziPH|Ry48Ma80_G0PHrqcIYro)!? zlA<-|m-rT}|p6JH4(T?^0SSpoBS+mks zFx%Ok@FT#FAiuef%}V>D40rc+XEeCb4`M}@vXa@Jbc{)?WOp)lM|Ub_MSIdwC`PHR zFS@oT=FcSj#GpIc)BU-aKiap^zarGCId`VIZ%5ovCFZoypG|l7t$QC0#;w+L6Z2(B zQ?YckH`$|Dj?qPXdJ-Mc3}op_#iE`5Xlh+rGxN;ezI*3K2ToqU^Oq}>o z$wVp>>*R(p{w2Bj+l#*=(U*yB$c(isyBgD)X@Q<<-ZY>y)7?_@FgD?!U%a?C8C&O% zr=ql~JRRn}A+1()45UrZdr0#;@HhrDwykmNYjn z^w)-BYrCU;{)YK=bL;BoG%T2{i`Kb%jwXHX25ZuWnn@o%dur7XR!<}FkLb3~Qoh(% zv#BEJo0+Rz;`3KrTkV?}+~=za9`IEyo*qI4Wna~``zVG)A6VO|4^lThmiTJ6Qr}IL zL7zW2xnd>LFY;A6`wo2HvH~F4LUc$E(BoQPO|GKi=IVXEs-@F|)xN4F)7yPj`-o-- zwIltE1YhK|IqzD zh%%)s@jIolS1E@?gJi$0G%uT=E@WQfJ3OiK1~9D!|B}=W{-S|-(jiT$MP+OUI#(&3 zr{%j(%=HqO;7~PlQbI=HE&k9x$b0}oKkutjeCe>EbhgT@TI65R*b2hN`CryIvzZ+( zOb$o>b5PTMLDS|cZh_x8({F+gU9W9PK7C5lttGk^vgNhpmu$~@GW=mp?=Qo@s`2_V{85cBFT)?x_~A1ApEd3;o62u%e7KArk#Qt^ zCoW6FT(7o{mEp8s((G^JeC&a35vjVg-1r3#i0tDsEIDvh5i zqxUgB#adE^pR4KX%lLdq<4t9_n%A?e{xUwFWV&hN)(MeZqVaM&&jntN5j~AJ5gzuy zZ}!0B9yt9O&P4oc;er$4v@cAA(-|`nP9czq@E>{Lzw*GVu| zTRrez5Bzoye82;L$OHd|2mY7`PM=gJvd<4ZaQc=s5&cOIeAEL!4?(zz_yj!gIuHCB z5BvrXyvqZ>*8?B&z`y2!Kkk7)<$)jez+dvfU-Q6gCQoe7k9gphd*I7F@OBS;od=%u zz+DggOCIVGsN$@N=;FC(ox{Q{SnP)88FWo%(KXoCKmG1wa)wuOh zh4VWAikmgQM&o?n2mXlRqw%$>n`KcrCYGw}(1l1kmWimyjB1JWMpNq}$y6+r=;?`2 zP+_AX?dXX`QxO$|QE%W872`mk?mkP!S0WwJzRqsM9%AXbdaJIrnI8>QYWNgC8~M3_ zo^!czu4}HXY+?D0X{y2SQl4#}ldEXeL5|ok(|IGMgF0;@y3n5f!*WQ#^qd z4$eflL5EsOAO~nlj!a4fUSuSIjG_-xMurOu{i2V?sN=RRO*$mRIDRLj9E?U+1(pm z7mJLE!{I1PS>dxZiIHSB-33amb~+YYk7+;z&I}Xp0YSw4k+61$=4>hz>&vJaSMf(` z;d&T>N~I9F$MsoVgJLZlDJ8<*ST7bR30Sg6bYmi$iFC%|(QHqKO$^haj&)6R2$fMJ zEt0~2Za`IqDAP)_NHmp-Zj4}o8l3yXWe^zUQ4Be;RH{VMh@`TZ5KOr93Im6DWD=HJ zm9JxD@}%Shl4tuUl1L58x@xk>Rg?qMhLlKV(sNlhrX}Xr!BOSHNoVj!peZY^m!sai zO61LOj7&_eSviX(f=~<#E3r{g=-!LJ#>>0-6m!KAqJcLGyU<2q_Z%AqK5d7}w$|fV(zD#$IY3NR4kBF@8 z!7c;$H7J~wl#KHJC>W~)iH`cnQU z#J>SQL;tS8=L-B>G7J*Y&%@8q*9x3ssRsW$fm1Ba;7<$uQv&~QfnP50KM1^0;PjqE ziR6*|Ljsrl`vorf(|>KFM0_OwR|GEY`74c+SQJY&dfCJPiL!C?BJc%hGxTABlaCoZ z>Va<*_!WZw0T299fm2-C@ZTYD`a3Rz9~Ai20-uC?D0{@}Fn*~nyVoD@W2tOmw0)e*(JSOnP0>4+_a(=&{ahl%`2>Qna?i2V^f{&c{ zUkP0DKP~v!f=`7G9n-kwY8X9)Uh1%8geKj*>cUO`XqHfG!>1ijSj zJRP_!m*+x_m)osH;Pgq!@V`Ouk^D0Pm-^l%_{e#ANZ{uQ{*MYilK)7S1XoI(gjoyTIkV zyx@VqEAUpqf2t1^B(lR&{ER#oc;MFyT?kDD{u<282=*QN1pYHYFa7Y02mJ?h(2neLgWz+Mz@=VU zflK)x6*$F=jGPArzEa??3cO9==hC5tMDpB-pWzb}_)P+j3%p(67t$ezMDu$Qeuhtj z#)<#U0&f=hEdu|vz(WG>5jcJ0HT>@s_-cUDp6!93D{$%Oa|AB!aJj}wemWA3 zUaJMYv~%18PYPVxZNK0z*UL`?ytCh(Y`|FsAH zwg(=d%Lfwa701tv8xr_Bfwy_!aSwc>2R`6|Kj4ACDe!I~PX%2LkVyW|;AiCjsKBND z=L=lg^GboQ7kriqT-xC_4?N+4KPK>c!T(8t_Xxa>E+t5$m(=%4flGa_6L=r`7(1*N zxa4!Yz%Lc_UlX{r+qVQR?Y2wc3Bl(Tjgy~$Qs8Fe{kemmOCxS#UC+b$$7ijTBsM&Qyun+1L|`WgP`>wqT7Bl*+{ zT=JPCaA{9h;F8a`1TM$jEAR#(&+`J8e2xoT%JT`jv>}ztzd++OkMg{}TF}e$dKGg~ zd?f#slT-uom-wB+uTp3;_S_}rOU9kHdC0#^$Rqh{aqy*<{{BzBqY|dW3?=*)0+IAJ zxrEalf|8$~%Ad&=Cau;5!syqp>sxhh@Kd_}WEpP$ez8g0$IzR<56G9{=DYs(GTeN3 zKURjD@8xfn;pV&ed>RnRjBCDU)883M+_v|;z@KbtT zt-AEa-b{2Y@=QwQU8a=67pXe><8`Z!{|3HpZ8~k$r4m#j@`gHmaF$hk4(2{gfD#(( z@Kqz)QzGj~XNX9#K!rYKcO-gy@omql>q}%}bxW4D&B33X<5P&@73xEk7EN{#IqBr_tZ^Ux^kfm&?D+ zueumJsoL27VCvVQFp>S6mMIEH6O#R>;%CM;eCZBF2Gp66DR6_ zRCioXjUt)-&E)r@jY#SR4HauYt^mKvN>9Vi@8MKb+s z%O=L?ZAc$MVIuo)(f04s1Csse{lfG&_NV6Zu>o00dg(ygNrxU1*^AE6^8V9+m5B%h f_9MqedZKJ$m+LDiNIxiEK}bx5CaY<{?a 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