#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, double pts_base_offset, 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 : pts_base_offset + (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.18); 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; } typedef struct DecodeContext { AVFormatContext *format_context; AVCodecContext *video_codec_context; AVCodecContext *audio_codec_context; struct SwsContext *sws_context; SwrContext *swr_context; AVPacket *packet; AVFrame *decoded_frame; AVFrame *audio_frame; AVRational video_time_base; AVRational audio_time_base; int video_stream_index; int audio_stream_index; } DecodeContext; static void decode_context_reset(DecodeContext *ctx) { if (!ctx) { return; } if (ctx->packet) av_packet_free(&ctx->packet); if (ctx->decoded_frame) av_frame_free(&ctx->decoded_frame); if (ctx->audio_frame) av_frame_free(&ctx->audio_frame); if (ctx->video_codec_context) avcodec_free_context(&ctx->video_codec_context); if (ctx->audio_codec_context) avcodec_free_context(&ctx->audio_codec_context); if (ctx->format_context) avformat_close_input(&ctx->format_context); if (ctx->sws_context) sws_freeContext(ctx->sws_context); if (ctx->swr_context) swr_free(&ctx->swr_context); memset(ctx, 0, sizeof(*ctx)); ctx->video_stream_index = -1; ctx->audio_stream_index = -1; } static int decode_context_open(DecodeContext *ctx, const ProgramEntry *program, Player *player) { const AVCodec *video_codec = NULL; const AVCodec *audio_codec = NULL; decode_context_reset(ctx); ctx->video_stream_index = -1; ctx->audio_stream_index = -1; if (avformat_open_input(&ctx->format_context, program->file_path, NULL, NULL) < 0) { player_set_error(player, "Unable to open media file"); return -1; } if (avformat_find_stream_info(ctx->format_context, NULL) < 0) { player_set_error(player, "Unable to read stream metadata"); return -1; } for (unsigned int i = 0; i < ctx->format_context->nb_streams; ++i) { if (ctx->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { ctx->video_stream_index = (int) i; } else if (ctx->format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && ctx->audio_stream_index < 0) { ctx->audio_stream_index = (int) i; } } if (ctx->video_stream_index < 0) { player_set_error(player, "No video stream found"); return -1; } video_codec = avcodec_find_decoder(ctx->format_context->streams[ctx->video_stream_index]->codecpar->codec_id); if (!video_codec) { player_set_error(player, "Unsupported video codec"); return -1; } ctx->video_codec_context = avcodec_alloc_context3(video_codec); if (!ctx->video_codec_context || avcodec_parameters_to_context(ctx->video_codec_context, ctx->format_context->streams[ctx->video_stream_index]->codecpar) < 0 || avcodec_open2(ctx->video_codec_context, video_codec, NULL) < 0) { player_set_error(player, "Unable to initialize decoder"); return -1; } if (ctx->audio_stream_index >= 0) { audio_codec = avcodec_find_decoder(ctx->format_context->streams[ctx->audio_stream_index]->codecpar->codec_id); if (audio_codec) { ctx->audio_codec_context = avcodec_alloc_context3(audio_codec); if (!ctx->audio_codec_context || avcodec_parameters_to_context(ctx->audio_codec_context, ctx->format_context->streams[ctx->audio_stream_index]->codecpar) < 0 || avcodec_open2(ctx->audio_codec_context, audio_codec, NULL) < 0) { player_set_error(player, "Unable to initialize audio decoder"); return -1; } ctx->audio_time_base = ctx->format_context->streams[ctx->audio_stream_index]->time_base; } } ctx->packet = av_packet_alloc(); ctx->decoded_frame = av_frame_alloc(); ctx->audio_frame = av_frame_alloc(); if (!ctx->packet || !ctx->decoded_frame || !ctx->audio_frame) { player_set_error(player, "Unable to allocate FFmpeg frame buffers"); return -1; } ctx->video_time_base = ctx->format_context->streams[ctx->video_stream_index]->time_base; ctx->sws_context = sws_getContext(ctx->video_codec_context->width, ctx->video_codec_context->height, ctx->video_codec_context->pix_fmt, ctx->video_codec_context->width, ctx->video_codec_context->height, AV_PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL); if (!ctx->sws_context) { player_set_error(player, "Unable to initialize scaler"); return -1; } player->has_audio_stream = ctx->audio_codec_context != NULL; return 0; } static int decode_context_seek_to_seconds(DecodeContext *ctx, double seek_seconds) { int64_t seek_target; if (!ctx || seek_seconds <= 0.0 || !ctx->format_context || ctx->video_stream_index < 0) { return 0; } seek_target = (int64_t) (seek_seconds / av_q2d(ctx->video_time_base)); if (avformat_seek_file(ctx->format_context, ctx->video_stream_index, INT64_MIN, seek_target, INT64_MAX, 0) < 0) { return -1; } avcodec_flush_buffers(ctx->video_codec_context); if (ctx->audio_codec_context) { avcodec_flush_buffers(ctx->audio_codec_context); } return 0; } static int decode_context_sync_to_schedule(Player *player, const Channel *channel, DecodeContext *ctx, const ProgramEntry **program, int *current_program_index, double *seek_seconds, int force_reopen, int *queued_any_audio, int *queued_first_video) { const ProgramEntry *scheduled_program; int scheduled_program_index = -1; double scheduled_seek_seconds = 0.0; if (!player || !channel || !ctx || !program || !current_program_index || !seek_seconds) { return -1; } scheduled_program = channel_resolve_program(channel, player->app_start_ticks, SDL_GetTicks64(), &scheduled_seek_seconds, &scheduled_program_index); if (!scheduled_program) { return -1; } if (!force_reopen && *program == scheduled_program && *current_program_index == scheduled_program_index) { *seek_seconds = scheduled_seek_seconds; return 0; } if (decode_context_open(ctx, scheduled_program, player) != 0) { return -1; } if (decode_context_seek_to_seconds(ctx, scheduled_seek_seconds) != 0) { player_set_error(player, "Unable to seek to scheduled program position"); return -1; } frame_queue_clear(&player->frame_queue); player_clear_audio(player); *program = scheduled_program; *current_program_index = scheduled_program_index; *seek_seconds = scheduled_seek_seconds; if (queued_any_audio) { *queued_any_audio = 0; } if (queued_first_video) { *queued_first_video = 0; } return 0; } static int decode_thread_main(void *userdata) { DecoderThreadArgs *args = (DecoderThreadArgs *) userdata; Player *player = args->player; const Channel *channel = args->channel; DecodeContext ctx; const ProgramEntry *program; int current_program_index = 0; double seek_seconds = 0.0; int queued_any_audio = 0; int queued_first_video = 0; int rc = -1; #define MAYBE_MARK_PREROLL_READY() do { if (queued_first_video && (queued_any_audio || !player->has_audio_stream)) { player_mark_preroll_ready(player); } } while (0) memset(&ctx, 0, sizeof(ctx)); ctx.video_stream_index = -1; ctx.audio_stream_index = -1; free(args); if (decode_context_sync_to_schedule(player, channel, &ctx, &program, ¤t_program_index, &seek_seconds, 1, &queued_any_audio, &queued_first_video) != 0) { decode_context_reset(&ctx); return -1; } while (!should_stop(player)) { if (decode_context_sync_to_schedule(player, channel, &ctx, &program, ¤t_program_index, &seek_seconds, 0, &queued_any_audio, &queued_first_video) != 0) { goto cleanup; } if (av_read_frame(ctx.format_context, ctx.packet) < 0) { if (decode_context_sync_to_schedule(player, channel, &ctx, &program, ¤t_program_index, &seek_seconds, 1, &queued_any_audio, &queued_first_video) != 0) { goto cleanup; } continue; } if (ctx.packet->stream_index == ctx.audio_stream_index && ctx.audio_codec_context) { if (avcodec_send_packet(ctx.audio_codec_context, ctx.packet) >= 0) { while (!should_stop(player)) { int receive_audio = avcodec_receive_frame(ctx.audio_codec_context, ctx.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(ctx.packet); goto cleanup; } if (queue_audio_frame(player, &ctx.swr_context, ctx.audio_codec_context, ctx.audio_time_base, ctx.audio_frame, program->start_offset_seconds, &queued_any_audio) != 0) { av_packet_unref(ctx.packet); goto cleanup; } MAYBE_MARK_PREROLL_READY(); av_frame_unref(ctx.audio_frame); } } av_packet_unref(ctx.packet); continue; } if (ctx.packet->stream_index != ctx.video_stream_index) { av_packet_unref(ctx.packet); continue; } if (avcodec_send_packet(ctx.video_codec_context, ctx.packet) < 0) { av_packet_unref(ctx.packet); continue; } av_packet_unref(ctx.packet); while (!should_stop(player)) { double frame_seconds; int receive = avcodec_receive_frame(ctx.video_codec_context, ctx.decoded_frame); if (receive == AVERROR(EAGAIN) || receive == AVERROR_EOF) { break; } if (receive < 0) { player_set_error(player, "Video decode failed"); goto cleanup; } frame_seconds = ctx.decoded_frame->best_effort_timestamp == AV_NOPTS_VALUE ? program->start_offset_seconds + seek_seconds : program->start_offset_seconds + (ctx.decoded_frame->best_effort_timestamp * av_q2d(ctx.video_time_base)); { FrameData frame = {0}; uint8_t *dest_data[4] = {0}; int dest_linesize[4] = {0}; int image_size; frame.width = ctx.video_codec_context->width; frame.height = ctx.video_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(ctx.sws_context, (const uint8_t *const *) ctx.decoded_frame->data, ctx.decoded_frame->linesize, 0, ctx.video_codec_context->height, dest_data, dest_linesize); frame_queue_push(&player->frame_queue, &frame); queued_first_video = 1; MAYBE_MARK_PREROLL_READY(); } } } rc = 0; cleanup: decode_context_reset(&ctx); 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, Uint64 app_start_ticks) { if (!player || !channels) { return -1; } memset(player, 0, sizeof(*player)); player->channels = channels; player->current_index = -1; player->app_start_ticks = app_start_ticks; 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.220 : 0.130; 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, Uint64 now_ticks) { if (!player || !player->channels || player->current_index < 0 || player->current_index >= player->channels->count) { return 0.0; } return channel_live_position_precise(&player->channels->items[player->current_index], player->app_start_ticks, now_ticks); } double player_get_sync_clock(Player *player, Uint64 now_ticks) { 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_ticks); } bytes_per_second = player_audio_bytes_per_second(player); if (bytes_per_second <= 0.0) { return player_live_position(player, now_ticks); } 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_ticks); } 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); }