#include "player.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include typedef struct DecoderThreadArgs { Player *player; const Channel *channel; } DecoderThreadArgs; static void player_set_error(Player *player, const char *message) { if (!player || !player->error_mutex) { return; } SDL_LockMutex(player->error_mutex); snprintf(player->last_error, sizeof(player->last_error), "%s", message ? message : "Unknown playback error"); SDL_UnlockMutex(player->error_mutex); } 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; double seek_seconds = 0.0; AVRational time_base; 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); if (avformat_open_input(&format_context, channel->file_path, NULL, NULL) < 0) { player_set_error(player, "Unable to open media file"); return -1; } if (avformat_find_stream_info(format_context, NULL) < 0) { player_set_error(player, "Unable to read stream metadata"); goto cleanup; } 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; } else if (format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && audio_stream_index < 0) { audio_stream_index = (int) i; } } if (video_stream_index < 0) { player_set_error(player, "No video stream found"); goto cleanup; } codec = avcodec_find_decoder(format_context->streams[video_stream_index]->codecpar->codec_id); if (!codec) { player_set_error(player, "Unsupported video codec"); goto cleanup; } codec_context = avcodec_alloc_context3(codec); if (!codec_context) { player_set_error(player, "Unable to allocate codec context"); goto cleanup; } if (avcodec_parameters_to_context(codec_context, format_context->streams[video_stream_index]->codecpar) < 0 || avcodec_open2(codec_context, codec, NULL) < 0) { player_set_error(player, "Unable to initialize decoder"); 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 || !audio_frame || !packet) { player_set_error(player, "Unable to allocate FFmpeg frame buffers"); goto cleanup; } 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_YUV420P, SWS_BILINEAR, NULL, NULL, NULL); if (!sws_context) { player_set_error(player, "Unable to initialize scaler"); goto cleanup; } seek_seconds = channel_live_position(channel, player->app_start_time, time(NULL)); if (channel->duration_seconds > 0.0) { 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; 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; } if (avcodec_send_packet(codec_context, packet) < 0) { av_packet_unref(packet); continue; } av_packet_unref(packet); while (!should_stop(player)) { double frame_seconds; int receive = avcodec_receive_frame(codec_context, decoded_frame); if (receive == AVERROR(EAGAIN) || receive == AVERROR_EOF) { break; } if (receive < 0) { player_set_error(player, "Video decode failed"); goto cleanup; } frame_seconds = decoded_frame->best_effort_timestamp == AV_NOPTS_VALUE ? seek_seconds : decoded_frame->best_effort_timestamp * av_q2d(time_base); { 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.pts_seconds = frame_seconds; 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; } 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, 0, codec_context->height, dest_data, dest_linesize); frame_queue_push(&player->frame_queue, &frame); if (!queued_first_video) { queued_first_video = 1; } MAYBE_MARK_PREROLL_READY(); } } } rc = 0; cleanup: if (packet) { av_packet_free(&packet); } 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) { if (!player || !player->thread) { return; } SDL_AtomicSet(&player->stop_requested, 1); SDL_WaitThread(player->thread, NULL); player->thread = NULL; } int player_init(Player *player, const ChannelList *channels, time_t app_start_time) { if (!player || !channels) { return -1; } memset(player, 0, sizeof(*player)); 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->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; } return 0; } void player_destroy(Player *player) { if (!player) { return; } 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); } memset(player, 0, sizeof(*player)); } int player_tune(Player *player, int channel_index) { DecoderThreadArgs *args; if (!player || !player->channels || channel_index < 0 || channel_index >= player->channels->count) { return -1; } 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)); if (!args) { player_set_error(player, "Unable to allocate thread args"); return -1; } args->player = player; args->channel = &player->channels->items[channel_index]; player->thread = SDL_CreateThread(decode_thread_main, "decoder", args); if (!player->thread) { free(args); player_set_error(player, "Unable to start decoder thread"); return -1; } return 0; } int player_consume_latest_frame(Player *player, FrameData *out) { if (!player || !out) { return 0; } 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; } 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) { 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) { if (!player || !buffer || buffer_size == 0) { return; } SDL_LockMutex(player->error_mutex); snprintf(buffer, buffer_size, "%s", player->last_error); SDL_UnlockMutex(player->error_mutex); }