diff --git a/libobs/CMakeLists.txt b/libobs/CMakeLists.txt index fa19fd99a..a9f4ac9ed 100644 --- a/libobs/CMakeLists.txt +++ b/libobs/CMakeLists.txt @@ -347,6 +347,12 @@ source_group("media-io\\Header Files" FILES ${libobs_mediaio_HEADERS}) source_group("util\\Source Files" FILES ${libobs_util_SOURCES}) source_group("util\\Header Files" FILES ${libobs_util_HEADERS}) +if(BUILD_CAPTIONS) + include_directories(${CMAKE_SOURCE_DIR}/deps/libcaption) + set(libobs_PLATFORM_DEPS + ${libobs_PLATFORM_DEPS} + caption) +endif() add_library(libobs SHARED ${libobs_SOURCES} ${libobs_HEADERS}) diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index 974687f82..470a10904 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -773,6 +773,13 @@ struct obs_weak_output { struct obs_output *output; }; +#define CAPTION_LINE_CHARS (32) +#define CAPTION_LINE_BYTES (4*CAPTION_LINE_CHARS) +struct caption_text { + char text[CAPTION_LINE_BYTES+1]; + struct caption_text *next; +}; + struct obs_output { struct obs_context_data context; struct obs_output_info info; @@ -827,6 +834,11 @@ struct obs_output { struct video_scale_info video_conversion; struct audio_convert_info audio_conversion; + pthread_mutex_t caption_mutex; + double caption_timestamp; + struct caption_text *caption_head; + struct caption_text *caption_tail; + bool valid; uint64_t active_delay_ns; diff --git a/libobs/obs-output.c b/libobs/obs-output.c index bdff07ddd..e2e897d76 100644 --- a/libobs/obs-output.c +++ b/libobs/obs-output.c @@ -20,6 +20,11 @@ #include "obs.h" #include "obs-internal.h" +#if BUILD_CAPTIONS +#include +#include +#endif + static inline bool active(const struct obs_output *output) { return os_atomic_load_bool(&output->active); @@ -99,11 +104,14 @@ obs_output_t *obs_output_create(const char *id, const char *name, output = bzalloc(sizeof(struct obs_output)); pthread_mutex_init_value(&output->interleaved_mutex); pthread_mutex_init_value(&output->delay_mutex); + pthread_mutex_init_value(&output->caption_mutex); if (pthread_mutex_init(&output->interleaved_mutex, NULL) != 0) goto fail; if (pthread_mutex_init(&output->delay_mutex, NULL) != 0) goto fail; + if (pthread_mutex_init(&output->caption_mutex, NULL) != 0) + goto fail; if (os_event_init(&output->stopping_event, OS_EVENT_TYPE_MANUAL) != 0) goto fail; if (!init_output_handlers(output, name, settings, hotkey_data)) @@ -196,6 +204,7 @@ void obs_output_destroy(obs_output_t *output) } os_event_destroy(output->stopping_event); + pthread_mutex_destroy(&output->caption_mutex); pthread_mutex_destroy(&output->interleaved_mutex); pthread_mutex_destroy(&output->delay_mutex); os_event_destroy(output->reconnect_stop_event); @@ -235,6 +244,7 @@ bool obs_output_actual_start(obs_output_t *output) if (os_atomic_load_long(&output->delay_restart_refs)) os_atomic_dec_long(&output->delay_restart_refs); + output->caption_timestamp = 0; return success; } @@ -356,6 +366,12 @@ void obs_output_actual_stop(obs_output_t *output, bool force, uint64_t ts) signal_stop(output); os_event_signal(output->stopping_event); } + + while (output->caption_head) { + output->caption_tail = output->caption_head->next; + bfree(output->caption_head); + output->caption_head = output->caption_tail; + } } void obs_output_stop(obs_output_t *output) @@ -942,6 +958,50 @@ static inline bool has_higher_opposing_ts(struct obs_output *output, return output->highest_video_ts > packet->dts_usec; } +#if BUILD_CAPTIONS +static const uint8_t nal_start[4] = {0, 0, 0, 1}; + +static bool add_caption(struct obs_output *output, struct encoder_packet *out) +{ + caption_frame_t cf; + sei_t sei; + uint8_t *data; + size_t size; + + DARRAY(uint8_t) out_data; + + out_data.array = out->data; + out_data.num = out->size; + out_data.capacity = out->size; + + if (out->priority > 1) + return false; + + sei_init(&sei); + + caption_frame_init(&cf); + caption_frame_from_text(&cf, &output->caption_head->text[0]); + + sei_from_caption_frame(&sei, &cf); + + data = malloc(sei_render_size(&sei)); + size = sei_render(&sei, data); + /* TODO SEI should come after AUD/SPS/PPS, but before any VCL */ + da_push_back_array(out_data, nal_start, 4); + da_push_back_array(out_data, data, size); + out->data = out_data.array; + out->size = out_data.num; + free(data); + + sei_free(&sei); + + struct caption_text *next = output->caption_head->next; + bfree(output->caption_head); + output->caption_head = next; + return true; +} +#endif + static inline void send_interleaved(struct obs_output *output) { struct encoder_packet out = output->interleaved_packets.array[0]; @@ -952,10 +1012,35 @@ static inline void send_interleaved(struct obs_output *output) if (!has_higher_opposing_ts(output, &out)) return; - if (out.type == OBS_ENCODER_VIDEO) + da_erase(output->interleaved_packets, 0); + + if (out.type == OBS_ENCODER_VIDEO) { output->total_frames++; - da_erase(output->interleaved_packets, 0); +#if BUILD_CAPTIONS + pthread_mutex_lock(&output->caption_mutex); + + double frame_timestamp = (out.pts * out.timebase_num) / + (double)out.timebase_den; + + /* TODO if output->caption_timestamp is more than 5 seconds + * old, send empty frame */ + if (output->caption_head && + output->caption_timestamp <= frame_timestamp) { + blog(LOG_INFO,"Sending caption: %f \"%s\"", + frame_timestamp, + &output->caption_head->text[0]); + + if (add_caption(output, &out)) { + output->caption_timestamp = + frame_timestamp + 2.0; + } + } + + pthread_mutex_unlock(&output->caption_mutex); +#endif + } + output->info.encoded_packet(output->context.data, &out); obs_encoder_packet_release(&out); } @@ -1954,3 +2039,61 @@ const char *obs_output_get_id(const obs_output_t *output) return obs_output_valid(output, "obs_output_get_id") ? output->info.id : NULL; } + +#if BUILD_CAPTIONS +static struct caption_text *caption_text_new(const char *text, size_t bytes, + struct caption_text *tail, struct caption_text **head) +{ + struct caption_text *next = bzalloc(sizeof(struct caption_text)); + snprintf(&next->text[0], CAPTION_LINE_BYTES + 1, "%.*s", bytes, text); + + if (!*head) { + *head = next; + } else { + tail->next = next; + } + + return next; +} + +void obs_output_output_caption_text1(obs_output_t *output, const char *text) +{ + if (!obs_output_valid(output, "obs_output_output_caption_text1")) + return; + if (!active(output)) + return; + + // split text into 32 charcter strings + int size = (int)strlen(text); + int r; + size_t char_count; + size_t line_length = 0; + size_t trimmed_length = 0; + + blog(LOG_DEBUG, "Caption text: %s", text); + + pthread_mutex_lock(&output->caption_mutex); + + for (r = 0 ; 0 < size && CAPTION_LINE_CHARS > r; ++r) { + line_length = utf8_line_length(text); + trimmed_length = utf8_trimmed_length(text, line_length); + char_count = utf8_char_count(text, trimmed_length); + + if (SCREEN_COLS < char_count) { + char_count = utf8_wrap_length(text, CAPTION_LINE_CHARS); + line_length = utf8_string_length(text, char_count + 1); + } + + output->caption_tail = caption_text_new( + text, + line_length, + output->caption_tail, + &output->caption_head); + + text += line_length; + size -= (int)line_length; + } + + pthread_mutex_unlock(&output->caption_mutex); +} +#endif diff --git a/libobs/obs.h b/libobs/obs.h index c48c92fd8..b5db5da87 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -1460,6 +1460,11 @@ EXPORT uint32_t obs_output_get_height(const obs_output_t *output); EXPORT const char *obs_output_get_id(const obs_output_t *output); +#if BUILD_CAPTIONS +EXPORT void obs_output_output_caption_text1(obs_output_t *output, + const char *text); +#endif + /* ------------------------------------------------------------------------- */ /* Functions used by outputs */ diff --git a/libobs/obsconfig.h.in b/libobs/obsconfig.h.in index 7a7833cd5..f86962d59 100644 --- a/libobs/obsconfig.h.in +++ b/libobs/obsconfig.h.in @@ -1,10 +1,19 @@ #pragma once +#ifndef ON +#define ON 1 +#endif + +#ifndef OFF +#define OFF 0 +#endif + #define OBS_VERSION "@OBS_VERSION@" #define OBS_DATA_PATH "@OBS_DATA_PATH@" #define OBS_INSTALL_PREFIX "@OBS_INSTALL_PREFIX@" #define OBS_PLUGIN_DESTINATION "@OBS_PLUGIN_DESTINATION@" #define OBS_RELATIVE_PREFIX "@OBS_RELATIVE_PREFIX@" #define OBS_UNIX_STRUCTURE @OBS_UNIX_STRUCTURE@ +#define BUILD_CAPTIONS @BUILD_CAPTIONS@ #define HAVE_DBUS @HAVE_DBUS@