2026-03-27 21:45:40 -04:00
|
|
|
#include "player.h"
|
|
|
|
|
|
|
|
|
|
#include <limits.h>
|
2026-03-27 22:59:37 -04:00
|
|
|
#include <math.h>
|
2026-03-27 21:45:40 -04:00
|
|
|
#include <stdint.h>
|
2026-03-27 22:59:37 -04:00
|
|
|
#include <libavutil/channel_layout.h>
|
|
|
|
|
#include <libavutil/imgutils.h>
|
|
|
|
|
#include <libavutil/opt.h>
|
|
|
|
|
#include <libavutil/samplefmt.h>
|
2026-03-27 21:45:40 -04:00
|
|
|
#include <libavcodec/avcodec.h>
|
|
|
|
|
#include <libavformat/avformat.h>
|
|
|
|
|
#include <libswscale/swscale.h>
|
2026-03-27 22:59:37 -04:00
|
|
|
#include <libswresample/swresample.h>
|
2026-03-27 21:45:40 -04:00
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 22:59:37 -04:00
|
|
|
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,
|
2026-03-28 00:25:06 -04:00
|
|
|
double pts_base_offset,
|
2026-03-27 22:59:37 -04:00
|
|
|
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
|
2026-03-28 00:25:06 -04:00
|
|
|
: pts_base_offset + (audio_frame->best_effort_timestamp * av_q2d(audio_time_base));
|
2026-03-27 22:59:37 -04:00
|
|
|
frame_duration = (double) sample_count / (double) out_rate;
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
queued_limit = (int) (player_audio_bytes_per_second(player) * 0.18);
|
2026-03-27 22:59:37 -04:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
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;
|
|
|
|
|
}
|
2026-03-27 22:59:37 -04:00
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
static int decode_context_open(DecodeContext *ctx, const ProgramEntry *program, Player *player) {
|
|
|
|
|
const AVCodec *video_codec = NULL;
|
|
|
|
|
const AVCodec *audio_codec = NULL;
|
2026-03-27 21:45:40 -04:00
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
decode_context_reset(ctx);
|
|
|
|
|
ctx->video_stream_index = -1;
|
|
|
|
|
ctx->audio_stream_index = -1;
|
2026-03-27 21:45:40 -04:00
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
if (avformat_open_input(&ctx->format_context, program->file_path, NULL, NULL) < 0) {
|
2026-03-27 21:45:40 -04:00
|
|
|
player_set_error(player, "Unable to open media file");
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
2026-03-28 00:25:06 -04:00
|
|
|
if (avformat_find_stream_info(ctx->format_context, NULL) < 0) {
|
2026-03-27 21:45:40 -04:00
|
|
|
player_set_error(player, "Unable to read stream metadata");
|
2026-03-28 00:25:06 -04:00
|
|
|
return -1;
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
2026-03-28 00:25:06 -04:00
|
|
|
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;
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-28 00:25:06 -04:00
|
|
|
if (ctx->video_stream_index < 0) {
|
2026-03-27 21:45:40 -04:00
|
|
|
player_set_error(player, "No video stream found");
|
2026-03-28 00:25:06 -04:00
|
|
|
return -1;
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
video_codec = avcodec_find_decoder(ctx->format_context->streams[ctx->video_stream_index]->codecpar->codec_id);
|
|
|
|
|
if (!video_codec) {
|
2026-03-27 21:45:40 -04:00
|
|
|
player_set_error(player, "Unsupported video codec");
|
2026-03-28 00:25:06 -04:00
|
|
|
return -1;
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
2026-03-28 00:25:06 -04:00
|
|
|
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) {
|
2026-03-27 21:45:40 -04:00
|
|
|
player_set_error(player, "Unable to initialize decoder");
|
2026-03-28 00:25:06 -04:00
|
|
|
return -1;
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
if (ctx->audio_stream_index >= 0) {
|
|
|
|
|
audio_codec = avcodec_find_decoder(ctx->format_context->streams[ctx->audio_stream_index]->codecpar->codec_id);
|
2026-03-27 22:59:37 -04:00
|
|
|
if (audio_codec) {
|
2026-03-28 00:25:06 -04:00
|
|
|
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) {
|
2026-03-27 22:59:37 -04:00
|
|
|
player_set_error(player, "Unable to initialize audio decoder");
|
2026-03-28 00:25:06 -04:00
|
|
|
return -1;
|
2026-03-27 22:59:37 -04:00
|
|
|
}
|
2026-03-28 00:25:06 -04:00
|
|
|
ctx->audio_time_base = ctx->format_context->streams[ctx->audio_stream_index]->time_base;
|
2026-03-27 22:59:37 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
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) {
|
2026-03-27 21:45:40 -04:00
|
|
|
player_set_error(player, "Unable to allocate FFmpeg frame buffers");
|
2026-03-28 00:25:06 -04:00
|
|
|
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) {
|
2026-03-27 21:45:40 -04:00
|
|
|
player_set_error(player, "Unable to initialize scaler");
|
2026-03-28 00:25:06 -04:00
|
|
|
return -1;
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
player->has_audio_stream = ctx->audio_codec_context != NULL;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 10:56:27 -04:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
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);
|
|
|
|
|
|
2026-03-28 10:56:27 -04:00
|
|
|
if (decode_context_sync_to_schedule(player,
|
|
|
|
|
channel,
|
|
|
|
|
&ctx,
|
|
|
|
|
&program,
|
|
|
|
|
¤t_program_index,
|
|
|
|
|
&seek_seconds,
|
|
|
|
|
1,
|
|
|
|
|
&queued_any_audio,
|
|
|
|
|
&queued_first_video) != 0) {
|
2026-03-28 00:25:06 -04:00
|
|
|
decode_context_reset(&ctx);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 10:56:27 -04:00
|
|
|
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;
|
2026-03-27 22:59:37 -04:00
|
|
|
}
|
2026-03-27 21:45:40 -04:00
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
if (av_read_frame(ctx.format_context, ctx.packet) < 0) {
|
2026-03-28 10:56:27 -04:00
|
|
|
if (decode_context_sync_to_schedule(player,
|
|
|
|
|
channel,
|
|
|
|
|
&ctx,
|
|
|
|
|
&program,
|
|
|
|
|
¤t_program_index,
|
|
|
|
|
&seek_seconds,
|
|
|
|
|
1,
|
|
|
|
|
&queued_any_audio,
|
|
|
|
|
&queued_first_video) != 0) {
|
2026-03-28 00:25:06 -04:00
|
|
|
goto cleanup;
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
2026-03-28 00:25:06 -04:00
|
|
|
continue;
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
if (ctx.packet->stream_index == ctx.audio_stream_index && ctx.audio_codec_context) {
|
|
|
|
|
if (avcodec_send_packet(ctx.audio_codec_context, ctx.packet) >= 0) {
|
2026-03-27 22:59:37 -04:00
|
|
|
while (!should_stop(player)) {
|
2026-03-28 00:25:06 -04:00
|
|
|
int receive_audio = avcodec_receive_frame(ctx.audio_codec_context, ctx.audio_frame);
|
2026-03-27 22:59:37 -04:00
|
|
|
if (receive_audio == AVERROR(EAGAIN) || receive_audio == AVERROR_EOF) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (receive_audio < 0) {
|
|
|
|
|
player_set_error(player, "Audio decode failed");
|
2026-03-28 00:25:06 -04:00
|
|
|
av_packet_unref(ctx.packet);
|
2026-03-27 22:59:37 -04:00
|
|
|
goto cleanup;
|
|
|
|
|
}
|
2026-03-28 00:25:06 -04:00
|
|
|
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);
|
2026-03-27 22:59:37 -04:00
|
|
|
goto cleanup;
|
|
|
|
|
}
|
|
|
|
|
MAYBE_MARK_PREROLL_READY();
|
2026-03-28 00:25:06 -04:00
|
|
|
av_frame_unref(ctx.audio_frame);
|
2026-03-27 22:59:37 -04:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-28 00:25:06 -04:00
|
|
|
av_packet_unref(ctx.packet);
|
2026-03-27 22:59:37 -04:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
if (ctx.packet->stream_index != ctx.video_stream_index) {
|
|
|
|
|
av_packet_unref(ctx.packet);
|
2026-03-27 21:45:40 -04:00
|
|
|
continue;
|
|
|
|
|
}
|
2026-03-28 00:25:06 -04:00
|
|
|
if (avcodec_send_packet(ctx.video_codec_context, ctx.packet) < 0) {
|
|
|
|
|
av_packet_unref(ctx.packet);
|
2026-03-27 21:45:40 -04:00
|
|
|
continue;
|
|
|
|
|
}
|
2026-03-28 00:25:06 -04:00
|
|
|
av_packet_unref(ctx.packet);
|
2026-03-27 21:45:40 -04:00
|
|
|
|
|
|
|
|
while (!should_stop(player)) {
|
|
|
|
|
double frame_seconds;
|
2026-03-28 00:25:06 -04:00
|
|
|
int receive = avcodec_receive_frame(ctx.video_codec_context, ctx.decoded_frame);
|
2026-03-27 21:45:40 -04:00
|
|
|
if (receive == AVERROR(EAGAIN) || receive == AVERROR_EOF) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (receive < 0) {
|
|
|
|
|
player_set_error(player, "Video decode failed");
|
|
|
|
|
goto cleanup;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
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));
|
2026-03-27 21:45:40 -04:00
|
|
|
|
|
|
|
|
{
|
|
|
|
|
FrameData frame = {0};
|
|
|
|
|
uint8_t *dest_data[4] = {0};
|
|
|
|
|
int dest_linesize[4] = {0};
|
2026-03-27 22:59:37 -04:00
|
|
|
int image_size;
|
2026-03-27 21:45:40 -04:00
|
|
|
|
2026-03-28 00:25:06 -04:00
|
|
|
frame.width = ctx.video_codec_context->width;
|
|
|
|
|
frame.height = ctx.video_codec_context->height;
|
2026-03-27 21:45:40 -04:00
|
|
|
frame.pts_seconds = frame_seconds;
|
2026-03-28 00:25:06 -04:00
|
|
|
image_size = av_image_alloc(dest_data, dest_linesize, frame.width, frame.height, AV_PIX_FMT_YUV420P, 1);
|
2026-03-27 22:59:37 -04:00
|
|
|
if (image_size < 0) {
|
2026-03-27 21:45:40 -04:00
|
|
|
player_set_error(player, "Unable to allocate frame buffer");
|
|
|
|
|
goto cleanup;
|
|
|
|
|
}
|
2026-03-27 22:59:37 -04:00
|
|
|
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];
|
2026-03-28 00:25:06 -04:00
|
|
|
sws_scale(ctx.sws_context,
|
|
|
|
|
(const uint8_t *const *) ctx.decoded_frame->data,
|
|
|
|
|
ctx.decoded_frame->linesize,
|
2026-03-27 21:45:40 -04:00
|
|
|
0,
|
2026-03-28 00:25:06 -04:00
|
|
|
ctx.video_codec_context->height,
|
2026-03-27 21:45:40 -04:00
|
|
|
dest_data,
|
|
|
|
|
dest_linesize);
|
|
|
|
|
frame_queue_push(&player->frame_queue, &frame);
|
2026-03-28 00:25:06 -04:00
|
|
|
queued_first_video = 1;
|
2026-03-27 22:59:37 -04:00
|
|
|
MAYBE_MARK_PREROLL_READY();
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rc = 0;
|
|
|
|
|
|
|
|
|
|
cleanup:
|
2026-03-28 00:25:06 -04:00
|
|
|
decode_context_reset(&ctx);
|
2026-03-27 22:59:37 -04:00
|
|
|
player->has_audio_stream = 0;
|
2026-03-27 21:45:40 -04:00
|
|
|
return rc;
|
2026-03-27 22:59:37 -04:00
|
|
|
|
|
|
|
|
#undef MAYBE_MARK_PREROLL_READY
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
int player_init(Player *player, const ChannelList *channels, Uint64 app_start_ticks) {
|
2026-03-27 21:45:40 -04:00
|
|
|
if (!player || !channels) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memset(player, 0, sizeof(*player));
|
|
|
|
|
player->channels = channels;
|
|
|
|
|
player->current_index = -1;
|
2026-03-27 23:19:42 -04:00
|
|
|
player->app_start_ticks = app_start_ticks;
|
2026-03-27 22:59:37 -04:00
|
|
|
player->audio_mutex = SDL_CreateMutex();
|
|
|
|
|
player->clock_mutex = SDL_CreateMutex();
|
2026-03-27 21:45:40 -04:00
|
|
|
player->error_mutex = SDL_CreateMutex();
|
2026-03-27 22:59:37 -04:00
|
|
|
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);
|
|
|
|
|
}
|
2026-03-27 21:45:40 -04:00
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (frame_queue_init(&player->frame_queue) != 0) {
|
2026-03-27 22:59:37 -04:00
|
|
|
SDL_DestroyMutex(player->audio_mutex);
|
|
|
|
|
SDL_DestroyMutex(player->clock_mutex);
|
2026-03-27 21:45:40 -04:00
|
|
|
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);
|
2026-03-27 22:59:37 -04:00
|
|
|
player_close_audio(player);
|
2026-03-27 21:45:40 -04:00
|
|
|
frame_queue_destroy(&player->frame_queue);
|
2026-03-27 22:59:37 -04:00
|
|
|
if (player->audio_mutex) {
|
|
|
|
|
SDL_DestroyMutex(player->audio_mutex);
|
|
|
|
|
}
|
|
|
|
|
if (player->clock_mutex) {
|
|
|
|
|
SDL_DestroyMutex(player->clock_mutex);
|
|
|
|
|
}
|
2026-03-27 21:45:40 -04:00
|
|
|
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);
|
2026-03-27 22:59:37 -04:00
|
|
|
player_clear_audio(player);
|
|
|
|
|
player->has_audio_stream = 0;
|
2026-03-27 21:45:40 -04:00
|
|
|
SDL_AtomicSet(&player->stop_requested, 0);
|
|
|
|
|
player->current_index = channel_index;
|
|
|
|
|
player->tuning_blackout_until = SDL_GetTicks() + 200;
|
2026-03-27 22:59:37 -04:00
|
|
|
player->catchup_until = player->tuning_blackout_until + 350;
|
2026-03-27 21:45:40 -04:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 22:59:37 -04:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
late_tolerance = SDL_GetTicks() < player->catchup_until ? 0.220 : 0.130;
|
2026-03-27 22:59:37 -04:00
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
double player_live_position(const Player *player, Uint64 now_ticks) {
|
2026-03-27 21:45:40 -04:00
|
|
|
if (!player || !player->channels || player->current_index < 0 || player->current_index >= player->channels->count) {
|
|
|
|
|
return 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
return channel_live_position_precise(&player->channels->items[player->current_index], player->app_start_ticks, now_ticks);
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
double player_get_sync_clock(Player *player, Uint64 now_ticks) {
|
2026-03-27 22:59:37 -04:00
|
|
|
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) {
|
2026-03-27 23:19:42 -04:00
|
|
|
return player_live_position(player, now_ticks);
|
2026-03-27 22:59:37 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bytes_per_second = player_audio_bytes_per_second(player);
|
|
|
|
|
if (bytes_per_second <= 0.0) {
|
2026-03-27 23:19:42 -04:00
|
|
|
return player_live_position(player, now_ticks);
|
2026-03-27 22:59:37 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-03-27 23:19:42 -04:00
|
|
|
return player_live_position(player, now_ticks);
|
2026-03-27 22:59:37 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 21:45:40 -04:00
|
|
|
int player_is_in_blackout(const Player *player) {
|
2026-03-27 22:59:37 -04:00
|
|
|
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);
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|