Milestone, fix black screen and initial frame issues
This commit is contained in:
parent
f4fa723863
commit
0a250b05f3
11 changed files with 708 additions and 47 deletions
6
Makefile
6
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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
126
src/app.c
126
src/app.c
|
|
@ -1,9 +1,57 @@
|
|||
#define _POSIX_C_SOURCE 200809L
|
||||
|
||||
#include "app.h"
|
||||
|
||||
#include <SDL2/SDL_ttf.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
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 : "<unset>",
|
||||
wayland_display && wayland_display[0] != '\0' ? wayland_display : "<unset>",
|
||||
runtime_dir && runtime_dir[0] != '\0' ? runtime_dir : "<unset>",
|
||||
pulse_server && pulse_server[0] != '\0' ? pulse_server : "<unset>",
|
||||
video_driver && video_driver[0] != '\0' ? video_driver : "<pending>");
|
||||
}
|
||||
|
||||
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)) {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
BIN
src/app.o
BIN
src/app.o
Binary file not shown.
|
|
@ -1,5 +1,6 @@
|
|||
#include "frame_queue.h"
|
||||
|
||||
#include <libavutil/mem.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@
|
|||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#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
|
||||
|
|
|
|||
Binary file not shown.
548
src/player.c
548
src/player.c
|
|
@ -1,10 +1,16 @@
|
|||
#include "player.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
#include <libavutil/channel_layout.h>
|
||||
#include <libavutil/imgutils.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libavutil/samplefmt.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libswscale/swscale.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
14
src/player.h
14
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
|
||||
|
|
|
|||
BIN
src/player.o
BIN
src/player.o
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue