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 f4227c4..d0b92bb 100644 Binary files a/src/app.o and b/src/app.o differ 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 db95206..3eb478d 100644 Binary files a/src/frame_queue.o and b/src/frame_queue.o differ 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 9ce7246..c9f8d2a 100644 Binary files a/src/player.o and b/src/player.o differ