diff --git a/cmake/Modules/FindLibrist.cmake b/cmake/Modules/FindLibrist.cmake new file mode 100644 index 000000000..e46c20b1e --- /dev/null +++ b/cmake/Modules/FindLibrist.cmake @@ -0,0 +1,74 @@ +# Once done these will be defined: +# +# LIBRIST_FOUND LIBRIST_INCLUDE_DIRS LIBRIST_LIBRARIES +# +# For use in OBS: +# +# LIBRIST_INCLUDE_DIR + +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(_LIBRIST QUIET librist) +endif() + +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_lib_suffix 64) +else() + set(_lib_suffix 32) +endif() + +find_path( + LIBRIST_INCLUDE_DIR + NAMES librist.h librist/librist.h + HINTS ENV LIBRIST_PATH ${LIBRIST_PATH} ${CMAKE_SOURCE_DIR}/${LIBRIST_PATH} + ${_LIBRIST_INCLUDE_DIRS} ${DepsPath} + PATHS /usr/include /usr/local/include /opt/local/include /sw/include + PATH_SUFFIXES include) + +find_library( + LIBRIST_LIB + NAMES ${_LIBRIST_LIBRARIES} librist rist + HINTS ENV LIBRIST_PATH ${LIBRIST_PATH} ${CMAKE_SOURCE_DIR}/${LIBRIST_PATH} + ${_LIBRIST_LIBRARY_DIRS} ${DepsPath} + PATHS /usr/lib /usr/local/lib /opt/local/lib /sw/lib + PATH_SUFFIXES + lib${_lib_suffix} + lib + libs${_lib_suffix} + libs + bin${_lib_suffix} + bin + ../lib${_lib_suffix} + ../lib + ../libs${_lib_suffix} + ../libs + ../bin${_lib_suffix} + ../bin) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Librist DEFAULT_MSG LIBRIST_LIB + LIBRIST_INCLUDE_DIR) +mark_as_advanced(LIBRIST_INCLUDE_DIR LIBRIST_LIB) + +if(LIBRIST_FOUND) + set(LIBRIST_INCLUDE_DIRS ${LIBRIST_INCLUDE_DIR}) + set(LIBRIST_LIBRARIES ${LIBRIST_LIB}) + + if(NOT TARGET Librist::Librist) + if(IS_ABSOLUTE "${LIBRIST_LIBRARIES}") + add_library(Librist::Librist UNKNOWN IMPORTED) + set_target_properties(Librist::Librist PROPERTIES IMPORTED_LOCATION + "${LIBRIST_LIBRARIES}") + else() + add_library(Librist::Librist INTERFACE IMPORTED) + set_target_properties(Librist::Librist PROPERTIES IMPORTED_LIBNAME + "${LIBRIST_LIBRARIES}") + endif() + + set_target_properties( + Librist::Librist PROPERTIES INTERFACE_INCLUDE_DIRECTORIES + "${LIBRIST_INCLUDE_DIRS}") + endif() +else() + message(STATUS "librist library not found") +endif() diff --git a/cmake/Modules/FindLibsrt.cmake b/cmake/Modules/FindLibsrt.cmake new file mode 100644 index 000000000..a812df87f --- /dev/null +++ b/cmake/Modules/FindLibsrt.cmake @@ -0,0 +1,74 @@ +# Once done these will be defined: +# +# LIBSRT_FOUND LIBSRT_INCLUDE_DIRS LIBSRT_LIBRARIES +# +# For use in OBS: +# +# LIBSRT_INCLUDE_DIR + +find_package(PkgConfig QUIET) +if(PKG_CONFIG_FOUND) + pkg_check_modules(_LIBSRT QUIET libsrt) +endif() + +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(_lib_suffix 64) +else() + set(_lib_suffix 32) +endif() + +find_path( + LIBSRT_INCLUDE_DIR + NAMES srt.h srt/srt.h + HINTS ENV LIBSRT_PATH ${LIBSRT_PATH} ${CMAKE_SOURCE_DIR}/${LIBSRT_PATH} + ${_LIBSRT_INCLUDE_DIRS} ${DepsPath} + PATHS /usr/include /usr/local/include /opt/local/include /sw/include + PATH_SUFFIXES include) + +find_library( + LIBSRT_LIB + NAMES ${_LIBSRT_LIBRARIES} srt libsrt + HINTS ENV LIBSRT_PATH ${LIBSRT_PATH} ${CMAKE_SOURCE_DIR}/${LIBSRT_PATH} + ${_LIBSRT_LIBRARY_DIRS} ${DepsPath} + PATHS /usr/lib /usr/local/lib /opt/local/lib /sw/lib + PATH_SUFFIXES + lib${_lib_suffix} + lib + libs${_lib_suffix} + libs + bin${_lib_suffix} + bin + ../lib${_lib_suffix} + ../lib + ../libs${_lib_suffix} + ../libs + ../bin${_lib_suffix} + ../bin) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Libsrt DEFAULT_MSG LIBSRT_LIB + LIBSRT_INCLUDE_DIR) +mark_as_advanced(LIBSRT_INCLUDE_DIR LIBSRT_LIB) + +if(LIBSRT_FOUND) + set(LIBSRT_INCLUDE_DIRS ${LIBSRT_INCLUDE_DIR}) + set(LIBSRT_LIBRARIES ${LIBSRT_LIB}) + + if(NOT TARGET Libsrt::Libsrt) + if(IS_ABSOLUTE "${LIBSRT_LIBRARIES}") + add_library(Libsrt::Libsrt UNKNOWN IMPORTED) + set_target_properties(Libsrt::Libsrt PROPERTIES IMPORTED_LOCATION + "${LIBSRT_LIBRARIES}") + else() + add_library(Libsrt::Libsrt INTERFACE IMPORTED) + set_target_properties(Libsrt::Libsrt PROPERTIES IMPORTED_LIBNAME + "${LIBSRT_LIBRARIES}") + endif() + + set_target_properties( + Libsrt::Libsrt PROPERTIES INTERFACE_INCLUDE_DIRECTORIES + "${LIBSRT_INCLUDE_DIRS}") + endif() +else() + message(STATUS "libsrt library not found") +endif() diff --git a/plugins/obs-ffmpeg/CMakeLists.txt b/plugins/obs-ffmpeg/CMakeLists.txt index f3b091b59..2e970562b 100644 --- a/plugins/obs-ffmpeg/CMakeLists.txt +++ b/plugins/obs-ffmpeg/CMakeLists.txt @@ -14,6 +14,9 @@ find_package( add_library(obs-ffmpeg MODULE) add_library(OBS::ffmpeg ALIAS obs-ffmpeg) + +find_package(Librist QUIET) +find_package(Libsrt QUIET) add_subdirectory(ffmpeg-mux) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/obs-ffmpeg-config.h.in @@ -27,13 +30,17 @@ target_sources( obs-ffmpeg-av1.c obs-ffmpeg-nvenc.c obs-ffmpeg-output.c + obs-ffmpeg-mpegts.c obs-ffmpeg-mux.c obs-ffmpeg-mux.h obs-ffmpeg-hls-mux.c obs-ffmpeg-source.c obs-ffmpeg-compat.h obs-ffmpeg-formats.h - ${CMAKE_BINARY_DIR}/config/obs-ffmpeg-config.h) + ${CMAKE_BINARY_DIR}/config/obs-ffmpeg-config.h + obs-ffmpeg-srt.h + obs-ffmpeg-rist.h + obs-ffmpeg-url.h) target_include_directories(obs-ffmpeg PRIVATE ${CMAKE_BINARY_DIR}/config) @@ -48,7 +55,9 @@ target_link_libraries( FFmpeg::avdevice FFmpeg::avutil FFmpeg::swscale - FFmpeg::swresample) + FFmpeg::swresample + Librist::Librist + Libsrt::Libsrt) if(ENABLE_FFMPEG_LOGGING) target_sources(obs-ffmpeg PRIVATE obs-ffmpeg-logging.c) @@ -68,6 +77,7 @@ if(OS_WINDOWS) if(MSVC) target_link_libraries(obs-ffmpeg PRIVATE OBS::w32-pthreads) endif() + target_link_libraries(obs-ffmpeg PRIVATE ws2_32.lib) set(MODULE_DESCRIPTION "OBS FFmpeg module") configure_file(${CMAKE_SOURCE_DIR}/cmake/bundle/windows/obs-module.rc.in diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c b/plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c new file mode 100644 index 000000000..b9aebf6b8 --- /dev/null +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mpegts.c @@ -0,0 +1,1235 @@ +/****************************************************************************** + Copyright (C) 2014 by Hugh Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include +#include +#include +#include +#include +#include + +#include "obs-ffmpeg-output.h" +#include "obs-ffmpeg-formats.h" +#include "obs-ffmpeg-compat.h" +#include "obs-ffmpeg-rist.h" +#include "obs-ffmpeg-srt.h" +#include +#include + +/* ------------------------------------------------------------------------- */ +#define do_log(level, format, ...) \ + blog(level, "[obs-ffmpeg mpegts muxer: '%s'] " format, \ + obs_output_get_name(stream->output), ##__VA_ARGS__) + +#define warn(format, ...) do_log(LOG_WARNING, format, ##__VA_ARGS__) +#define info(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__) +#define error(format, ...) do_log(LOG_ERROR, format, ##__VA_ARGS__) + +static void ffmpeg_mpegts_set_last_error(struct ffmpeg_data *data, + const char *error) +{ + if (data->last_error) + bfree(data->last_error); + + data->last_error = bstrdup(error); +} + +void ffmpeg_mpegts_log_error(int log_level, struct ffmpeg_data *data, + const char *format, ...) +{ + va_list args; + char out[4096]; + + va_start(args, format); + vsnprintf(out, sizeof(out), format, args); + va_end(args); + + ffmpeg_mpegts_set_last_error(data, out); + + blog(log_level, "%s", out); +} + +static bool is_rist(struct ffmpeg_output *stream) +{ + return !strncmp(stream->ff_data.config.url, RIST_PROTO, + sizeof(RIST_PROTO) - 1); +} + +static bool is_srt(struct ffmpeg_output *stream) +{ + return !strncmp(stream->ff_data.config.url, SRT_PROTO, + sizeof(SRT_PROTO) - 1); +} + +static bool proto_is_allowed(struct ffmpeg_output *stream) +{ + return !strncmp(stream->ff_data.config.url, UDP_PROTO, + sizeof(UDP_PROTO) - 1) || + !strncmp(stream->ff_data.config.url, TCP_PROTO, + sizeof(TCP_PROTO) - 1) || + !strncmp(stream->ff_data.config.url, HTTP_PROTO, + sizeof(HTTP_PROTO) - 1); +} + +static bool new_stream(struct ffmpeg_data *data, AVStream **stream, + const char *name) +{ + + *stream = avformat_new_stream(data->output, NULL); + if (!*stream) { + ffmpeg_mpegts_log_error( + LOG_WARNING, data, + "Couldn't create stream for encoder '%s'", name); + return false; + } + + (*stream)->id = data->output->nb_streams - 1; + return true; +} + +static bool get_audio_headers(struct ffmpeg_output *stream, + struct ffmpeg_data *data, int idx) +{ + AVCodecParameters *par = data->audio_infos[idx].stream->codecpar; + obs_encoder_t *aencoder = + obs_output_get_audio_encoder(stream->output, idx); + struct encoder_packet packet = { + .type = OBS_ENCODER_AUDIO, .timebase_den = 1, .track_idx = idx}; + + if (obs_encoder_get_extra_data(aencoder, &packet.data, &packet.size)) { + par->extradata = av_memdup(packet.data, packet.size); + par->extradata_size = (int)packet.size; + avcodec_parameters_to_context(data->audio_infos[idx].ctx, par); + return 1; + } + return 0; +} + +static bool get_video_headers(struct ffmpeg_output *stream, + struct ffmpeg_data *data) +{ + AVCodecParameters *par = data->video->codecpar; + obs_encoder_t *vencoder = obs_output_get_video_encoder(stream->output); + struct encoder_packet packet = {.type = OBS_ENCODER_VIDEO, + .timebase_den = 1}; + + if (obs_encoder_get_extra_data(vencoder, &packet.data, &packet.size)) { + par->extradata = av_memdup(packet.data, packet.size); + par->extradata_size = (int)packet.size; + avcodec_parameters_to_context(data->video_ctx, + data->video->codecpar); + return 1; + } + return 0; +} + +static bool create_video_stream(struct ffmpeg_output *stream, + struct ffmpeg_data *data) +{ + AVCodecContext *context; + void *extradata = NULL; + struct obs_video_info ovi; + + if (!obs_get_video_info(&ovi)) { + ffmpeg_mpegts_log_error(LOG_WARNING, data, "No active video"); + return false; + } + const char *name = data->config.video_encoder; + const AVCodecDescriptor *codec = avcodec_descriptor_get_by_name(name); + if (!codec) { + error("Couldn't find codec '%s'\n", name); + return false; + } + if (!new_stream(data, &data->video, name)) + return false; + if ((data->config.color_trc == AVCOL_TRC_SMPTE2084) || + (data->config.color_trc == AVCOL_TRC_ARIB_STD_B67)) { + AVMasteringDisplayMetadata *const mastering = + av_mastering_display_metadata_alloc(); + mastering->display_primaries[0][0] = av_make_q(17, 25); + mastering->display_primaries[0][1] = av_make_q(8, 25); + mastering->display_primaries[1][0] = av_make_q(53, 200); + mastering->display_primaries[1][1] = av_make_q(69, 100); + mastering->display_primaries[2][0] = av_make_q(3, 20); + mastering->display_primaries[2][1] = av_make_q(3, 50); + mastering->white_point[0] = av_make_q(3127, 10000); + mastering->white_point[1] = av_make_q(329, 1000); + mastering->min_luminance = av_make_q(0, 1); + mastering->max_luminance = av_make_q( + (int)obs_get_video_hdr_nominal_peak_level(), 1); + mastering->has_primaries = 1; + mastering->has_luminance = 1; + av_stream_add_side_data(data->video, + AV_PKT_DATA_MASTERING_DISPLAY_METADATA, + (uint8_t *)mastering, + sizeof(*mastering)); + } + context = avcodec_alloc_context3(NULL); + context->codec_type = codec->type; + context->codec_id = codec->id; + context->bit_rate = (int64_t)data->config.video_bitrate * 1000; + context->width = data->config.scale_width; + context->height = data->config.scale_height; + context->coded_width = data->config.scale_width; + context->coded_height = data->config.scale_height; + context->time_base = (AVRational){ovi.fps_den, ovi.fps_num}; + context->gop_size = data->config.gop_size; + context->pix_fmt = data->config.format; + context->color_range = data->config.color_range; + context->color_primaries = data->config.color_primaries; + context->color_trc = data->config.color_trc; + context->colorspace = data->config.colorspace; + context->thread_count = 0; + + data->video->time_base = context->time_base; +#if LIBAVFORMAT_VERSION_MAJOR < 59 + data->video->codec->time_base = context->time_base; +#endif + data->video->avg_frame_rate = av_inv_q(context->time_base); + + data->video_ctx = context; + data->config.width = data->config.scale_width; + data->config.height = data->config.scale_height; + + avcodec_parameters_from_context(data->video->codecpar, context); + + return true; +} + +static bool create_audio_stream(struct ffmpeg_output *stream, + struct ffmpeg_data *data, int idx) +{ + AVCodecContext *context; + AVStream *avstream; + void *extradata = NULL; + struct obs_audio_info aoi; + const char *name = data->config.audio_encoder; + + const AVCodecDescriptor *codec = avcodec_descriptor_get_by_name(name); + if (!codec) { + warn("Couldn't find codec '%s'\n", name); + return false; + } + + if (!obs_get_audio_info(&aoi)) { + ffmpeg_mpegts_log_error(LOG_WARNING, data, "No active audio"); + return false; + } + + if (!new_stream(data, &avstream, data->config.audio_encoder)) + return false; + + context = avcodec_alloc_context3(NULL); + context->codec_type = codec->type; + context->codec_id = codec->id; + context->bit_rate = (int64_t)data->config.audio_bitrate * 1000; + context->time_base = (AVRational){1, aoi.samples_per_sec}; + context->channels = get_audio_channels(aoi.speakers); + context->sample_rate = aoi.samples_per_sec; + context->channel_layout = + av_get_default_channel_layout(context->channels); + + //avutil default channel layout for 5 channels is 5.0 ; fix for 4.1 + if (aoi.speakers == SPEAKERS_4POINT1) + context->channel_layout = av_get_channel_layout("4.1"); + + context->sample_fmt = AV_SAMPLE_FMT_S16; + context->frame_size = data->config.frame_size; + + avstream->time_base = context->time_base; + + data->audio_samplerate = aoi.samples_per_sec; + data->audio_format = convert_ffmpeg_sample_format(context->sample_fmt); + data->audio_planes = get_audio_planes(data->audio_format, aoi.speakers); + data->audio_size = get_audio_size(data->audio_format, aoi.speakers, 1); + + data->audio_infos[idx].stream = avstream; + data->audio_infos[idx].ctx = context; + avcodec_parameters_from_context(data->audio_infos[idx].stream->codecpar, + context); + return true; +} + +static inline bool init_streams(struct ffmpeg_output *stream, + struct ffmpeg_data *data) +{ + if (!create_video_stream(stream, data)) + return false; + + if (data->num_audio_streams) { + data->audio_infos = calloc(data->num_audio_streams, + sizeof(*data->audio_infos)); + for (int i = 0; i < data->num_audio_streams; i++) { + if (!create_audio_stream(stream, data, i)) + return false; + } + } + + return true; +} + +int ff_network_init(void) +{ +#if HAVE_WINSOCK2_H + WSADATA wsaData; + + if (WSAStartup(MAKEWORD(1, 1), &wsaData)) + return 0; +#endif + return 1; +} + +static inline int connect_mpegts_url(struct ffmpeg_output *stream, bool is_rist) +{ + int err = 0; + const char *url = stream->ff_data.config.url; + if (!ff_network_init()) { + ffmpeg_mpegts_log_error(LOG_ERROR, &stream->ff_data, + "Can not initialize network."); + return AVERROR(EIO); + } + + URLContext *uc = av_mallocz(sizeof(URLContext) + strlen(url) + 1); + if (!uc) { + ffmpeg_mpegts_log_error(LOG_ERROR, &stream->ff_data, + "Can not allocate memory."); + goto fail; + } + uc->url = (char *)url; + uc->max_packet_size = is_rist ? RIST_MAX_PAYLOAD_SIZE + : SRT_LIVE_DEFAULT_PAYLOAD_SIZE; + uc->priv_data = is_rist ? av_mallocz(sizeof(RISTContext)) + : av_mallocz(sizeof(SRTContext)); + if (!uc->priv_data) { + ffmpeg_mpegts_log_error(LOG_ERROR, &stream->ff_data, + "Can not allocate memory."); + goto fail; + } + stream->h = uc; + if (is_rist) + err = librist_open(uc, uc->url); + else + err = libsrt_open(uc, uc->url); + if (err < 0) + goto fail; + return 0; +fail: + if (uc) + av_freep(&uc->priv_data); + av_freep(&uc); +#if HAVE_WINSOCK2_H + WSACleanup(); +#endif + return err; +} + +static inline int allocate_custom_aviocontext(struct ffmpeg_output *stream, + bool is_rist) +{ + /* allocate buffers */ + uint8_t *buffer = NULL; + int buffer_size; + URLContext *h = stream->h; + AVIOContext *s = NULL; + + buffer_size = UDP_DEFAULT_PAYLOAD_SIZE; + + buffer = av_malloc(buffer_size); + if (!buffer) + return AVERROR(ENOMEM); + /* allocate custom avio_context */ + if (is_rist) + s = avio_alloc_context( + buffer, buffer_size, AVIO_FLAG_WRITE, h, NULL, + (int (*)(void *, uint8_t *, int))librist_write, NULL); + else + s = avio_alloc_context( + buffer, buffer_size, AVIO_FLAG_WRITE, h, NULL, + (int (*)(void *, uint8_t *, int))libsrt_write, NULL); + if (!s) + goto fail; + s->max_packet_size = h->max_packet_size; + s->opaque = h; + stream->s = s; + stream->ff_data.output->pb = s; + + return 0; +fail: + av_freep(&buffer); + return AVERROR(ENOMEM); +} + +static inline int open_output_file(struct ffmpeg_output *stream, + struct ffmpeg_data *data) +{ + int ret; + bool rist = is_rist(stream); + bool srt = is_srt(stream); + bool allowed_proto = proto_is_allowed(stream); + AVDictionary *dict = NULL; + + /* Retrieve protocol settings for udp, tcp, rtp ... (not srt or rist). + * These options will be passed to protocol by avio_open2 through dict. + * The invalid options will be left in dict. */ + if (!rist && !srt) { + if ((ret = av_dict_parse_string(&dict, + data->config.protocol_settings, + "=", " ", 0))) { + ffmpeg_mpegts_log_error( + LOG_WARNING, data, + "Failed to parse protocol settings: %s\n%s", + av_err2str(ret), + data->config.protocol_settings); + + av_dict_free(&dict); + return OBS_OUTPUT_INVALID_STREAM; + } + + if (av_dict_count(dict) > 0) { + struct dstr str = {0}; + + AVDictionaryEntry *entry = NULL; + while ((entry = av_dict_get(dict, "", entry, + AV_DICT_IGNORE_SUFFIX))) + dstr_catf(&str, "\n\t%s=%s", entry->key, + entry->value); + + info("Using protocol settings: %s", str.array); + dstr_free(&str); + } + } + + /* Ensure h264 bitstream auto conversion from avcc to annex B */ + data->output->flags |= AVFMT_FLAG_AUTO_BSF; + + /* Open URL for rist, srt or other protocols compatible with mpegts + * muxer supported by avformat (udp, tcp, rtp ...). + */ + if (rist) { + ret = connect_mpegts_url(stream, true); + } else if (srt) { + ret = connect_mpegts_url(stream, false); + } else if (allowed_proto) { + ret = avio_open2(&data->output->pb, data->config.url, + AVIO_FLAG_WRITE, NULL, &dict); + } else { + info("[ffmpeg mpegts muxer:] Invalid protocol: %s", + data->config.url); + return OBS_OUTPUT_BAD_PATH; + } + + if (ret < 0) { + if ((rist || srt) && (ret == OBS_OUTPUT_CONNECT_FAILED || + ret == OBS_OUTPUT_INVALID_STREAM)) { + error("failed to open the url or invalid stream"); + } else { + ffmpeg_mpegts_log_error(LOG_WARNING, data, + "Couldn't open '%s', %s", + data->config.url, + av_err2str(ret)); + av_dict_free(&dict); + } + return ret; + } + + /* Log invalid protocol settings for all protocols except srt or rist. + * Or for srt & rist, allocate custom avio_ctx which will host the + * protocols write callbacks. + */ + if (!rist && !srt) { + if (av_dict_count(dict) > 0) { + struct dstr str = {0}; + + AVDictionaryEntry *entry = NULL; + while ((entry = av_dict_get(dict, "", entry, + AV_DICT_IGNORE_SUFFIX))) + dstr_catf(&str, "\n\t%s=%s", entry->key, + entry->value); + + info("[ffmpeg mpegts muxer:]Invalid protocol settings: %s", + str.array); + dstr_free(&str); + } + av_dict_free(&dict); + } else { + ret = allocate_custom_aviocontext(stream, rist); + if (ret < 0) { + info("Couldn't allocate custom avio_context for rist or srt'%s', %s\n", + data->config.url, av_err2str(ret)); + return OBS_OUTPUT_INVALID_STREAM; + } + } + + return 0; +} + +static void close_video(struct ffmpeg_data *data) +{ + avcodec_free_context(&data->video_ctx); +} + +static void close_audio(struct ffmpeg_data *data) +{ + for (int idx = 0; idx < data->num_audio_streams; idx++) { + for (size_t i = 0; i < MAX_AV_PLANES; i++) + circlebuf_free(&data->excess_frames[idx][i]); + + if (data->samples[idx][0]) + av_freep(&data->samples[idx][0]); + if (data->audio_infos[idx].ctx) { + avcodec_free_context(&data->audio_infos[idx].ctx); + } + if (data->aframe[idx]) + av_frame_free(&data->aframe[idx]); + } +} + +static void close_mpegts_url(struct ffmpeg_output *stream, bool is_rist) +{ + int err = 0; + AVIOContext *s = stream->s; + if (!s) + return; + URLContext *h = s->opaque; + if (!h) + return; /* can happen when opening the url fails */ + + /* close rist or srt URLs ; free URLContext */ + if (is_rist) { + err = librist_close(h); + } else { + err = libsrt_close(h); + } + av_freep(&h->priv_data); + av_freep(h); + + /* close custom avio_context for srt or rist */ + avio_flush(stream->s); + stream->s->opaque = NULL; + av_freep(&stream->s->buffer); + avio_context_free(&stream->s); + + if (err) + info("[ffmpeg mpegts muxer:] Error closing URL %s", + stream->ff_data.config.url); +} + +void ffmpeg_mpegts_data_free(struct ffmpeg_output *stream, + struct ffmpeg_data *data) +{ + if (data->initialized) + av_write_trailer(data->output); + + if (data->video) + close_video(data); + if (data->audio_infos) { + close_audio(data); + free(data->audio_infos); + } + + if (data->output) { + if (is_rist(stream) || is_srt(stream)) { + close_mpegts_url(stream, is_rist(stream)); + } else { + avio_close(data->output->pb); + } + avformat_free_context(data->output); + data->video = NULL; + data->audio_infos = NULL; + data->output = NULL; + data->num_audio_streams = 0; + } + + if (data->last_error) + bfree(data->last_error); + + memset(data, 0, sizeof(struct ffmpeg_data)); +} + +static inline const char *safe_str(const char *s) +{ + if (s == NULL) + return "(NULL)"; + else + return s; +} + +bool ffmpeg_mpegts_data_init(struct ffmpeg_output *stream, + struct ffmpeg_data *data, + struct ffmpeg_cfg *config) +{ + memset(data, 0, sizeof(struct ffmpeg_data)); + data->config = *config; + data->num_audio_streams = config->audio_mix_count; + data->audio_tracks = config->audio_tracks; + + if (!config->url || !*config->url) + return false; + +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 9, 100) + av_register_all(); +#endif + avformat_network_init(); + +#if LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(59, 0, 100) + AVOutputFormat *output_format; +#else + const AVOutputFormat *output_format; +#endif + + output_format = av_guess_format("mpegts", NULL, "video/M2PT"); + + if (output_format == NULL) { + ffmpeg_mpegts_log_error(LOG_WARNING, data, + "Couldn't set output format to mpegts"); + goto fail; + } else { + info("info: Output format name and long_name: %s, %s\n", + output_format->name ? output_format->name : "unknown", + output_format->long_name ? output_format->long_name + : "unknown"); + } + + avformat_alloc_output_context2(&data->output, output_format, NULL, + data->config.url); + + if (!data->output) { + ffmpeg_mpegts_log_error(LOG_WARNING, data, + "Couldn't create avformat context"); + goto fail; + } + + return true; + +fail: + warn("ffmpeg_data_init failed"); + return false; +} + +/* ------------------------------------------------------------------------- */ + +static inline bool stopping(struct ffmpeg_output *output) +{ + return os_atomic_load_bool(&output->stopping); +} + +static const char *ffmpeg_mpegts_getname(void *unused) +{ + UNUSED_PARAMETER(unused); + return obs_module_text("FFmpegMpegts"); +} + +static void ffmpeg_mpegts_log_callback(void *param, int level, + const char *format, va_list args) +{ + if (level <= AV_LOG_INFO) + blogva(LOG_DEBUG, format, args); + + UNUSED_PARAMETER(param); +} + +static void *ffmpeg_mpegts_create(obs_data_t *settings, obs_output_t *output) +{ + struct ffmpeg_output *data = bzalloc(sizeof(struct ffmpeg_output)); + pthread_mutex_init_value(&data->write_mutex); + data->output = output; + + if (pthread_mutex_init(&data->write_mutex, NULL) != 0) + goto fail; + if (os_event_init(&data->stop_event, OS_EVENT_TYPE_AUTO) != 0) + goto fail; + if (os_sem_init(&data->write_sem, 0) != 0) + goto fail; + + av_log_set_callback(ffmpeg_mpegts_log_callback); + + UNUSED_PARAMETER(settings); + return data; + +fail: + pthread_mutex_destroy(&data->write_mutex); + os_event_destroy(data->stop_event); + bfree(data); + return NULL; +} + +static void ffmpeg_mpegts_full_stop(void *data); +static void ffmpeg_mpegts_deactivate(struct ffmpeg_output *output); + +static void ffmpeg_mpegts_destroy(void *data) +{ + struct ffmpeg_output *output = data; + + if (output) { + if (output->connecting) + pthread_join(output->start_thread, NULL); + + ffmpeg_mpegts_full_stop(output); + + pthread_mutex_destroy(&output->write_mutex); + os_sem_destroy(output->write_sem); + os_event_destroy(output->stop_event); + bfree(data); + } +} + +static uint64_t get_packet_sys_dts(struct ffmpeg_output *output, + AVPacket *packet) +{ + struct ffmpeg_data *data = &output->ff_data; + uint64_t pause_offset = obs_output_get_pause_offset(output->output); + uint64_t start_ts; + + AVRational time_base; + + if (data->video && data->video->index == packet->stream_index) { + time_base = data->video->time_base; + start_ts = output->video_start_ts; + } else { + time_base = data->audio_infos[0].stream->time_base; + start_ts = output->audio_start_ts; + } + + return start_ts + pause_offset + + (uint64_t)av_rescale_q(packet->dts, time_base, + (AVRational){1, 1000000000}); +} + +static int mpegts_process_packet(struct ffmpeg_output *output) +{ + AVPacket *packet = NULL; + bool new_packet = false; + int ret; + + pthread_mutex_lock(&output->write_mutex); + if (output->packets.num) { + packet = output->packets.array[0]; + da_erase(output->packets, 0); + new_packet = true; + } + pthread_mutex_unlock(&output->write_mutex); + + if (!new_packet) + return 0; + + //blog(LOG_DEBUG, + // "size = %d, flags = %lX, stream = %d, " + // "packets queued: %lu", + // packet->size, packet->flags, packet->stream_index, + // output->packets.num); + + if (stopping(output)) { + uint64_t sys_ts = get_packet_sys_dts(output, packet); + if (sys_ts >= output->stop_ts) + return 0; + } + output->total_bytes += packet->size; + ret = av_interleaved_write_frame(output->ff_data.output, packet); + if (ret < 0) { + av_packet_free(&packet); + ffmpeg_mpegts_log_error( + LOG_WARNING, &output->ff_data, + "process_packet: Error writing packet: %s", + av_err2str(ret)); + + /* Treat "Invalid data found when processing input" and + * "Invalid argument" as non-fatal */ + if (ret == AVERROR_INVALIDDATA || ret == -EINVAL) { + return 0; + } + return ret; + } + + return 0; +} + +static void *write_thread(void *data) +{ + struct ffmpeg_output *output = data; + + while (os_sem_wait(output->write_sem) == 0) { + /* check to see if shutting down */ + if (os_event_try(output->stop_event) == 0) + break; + + int ret = mpegts_process_packet(output); + if (ret != 0) { + int code = OBS_OUTPUT_DISCONNECTED; + + pthread_detach(output->write_thread); + output->write_thread_active = false; + + if (ret == -ENOSPC) + code = OBS_OUTPUT_NO_SPACE; + + obs_output_signal_stop(output->output, code); + ffmpeg_mpegts_deactivate(output); + break; + } + } + + os_atomic_set_bool(&output->active, false); + return NULL; +} + +static bool get_extradata(struct ffmpeg_output *stream) +{ + struct ffmpeg_data *ff_data = &stream->ff_data; + + /* get extradata for av headers from encoders */ + if (!get_video_headers(stream, ff_data)) + return false; + for (int i = 0; i < ff_data->num_audio_streams; i++) { + if (!get_audio_headers(stream, ff_data, i)) + return false; + } + + return true; +} + +/* set ffmpeg_config & init write_thread & capture */ +static bool set_config(struct ffmpeg_output *stream) +{ + struct ffmpeg_cfg config; + bool success; + int ret; + int code; + + /* 1. Get URL from service & set format + mime-type. */ + obs_service_t *service; + service = obs_output_get_service(stream->output); + if (!service) + return false; + config.url = obs_service_get_url(service); + + config.format_name = "mpegts"; + config.format_mime_type = "video/M2PT"; + + /* 2. video settings */ + // 2.a) set video format from obs to FFmpeg + video_t *video = obs_output_video(stream->output); + config.format = + obs_to_ffmpeg_video_format(video_output_get_format(video)); + + if (config.format == AV_PIX_FMT_NONE) { + blog(LOG_DEBUG, "invalid pixel format used for mpegts output"); + return false; + } + + // 2.b) set colorspace, color_range & transfer characteristic (from voi) + const struct video_output_info *voi = video_output_get_info(video); + config.color_range = voi->range == VIDEO_RANGE_FULL ? AVCOL_RANGE_JPEG + : AVCOL_RANGE_MPEG; + config.colorspace = format_is_yuv(voi->format) ? AVCOL_SPC_BT709 + : AVCOL_SPC_RGB; + switch (voi->colorspace) { + case VIDEO_CS_601: + config.color_primaries = AVCOL_PRI_SMPTE170M; + config.color_trc = AVCOL_TRC_SMPTE170M; + config.colorspace = AVCOL_SPC_SMPTE170M; + break; + case VIDEO_CS_DEFAULT: + case VIDEO_CS_709: + config.color_primaries = AVCOL_PRI_BT709; + config.color_trc = AVCOL_TRC_BT709; + config.colorspace = AVCOL_SPC_BT709; + break; + case VIDEO_CS_SRGB: + config.color_primaries = AVCOL_PRI_BT709; + config.color_trc = AVCOL_TRC_IEC61966_2_1; + config.colorspace = AVCOL_SPC_BT709; + break; + case VIDEO_CS_2100_PQ: + config.color_primaries = AVCOL_PRI_BT2020; + config.color_trc = AVCOL_TRC_SMPTE2084; + config.colorspace = AVCOL_SPC_BT2020_NCL; + break; + case VIDEO_CS_2100_HLG: + config.color_primaries = AVCOL_PRI_BT2020; + config.color_trc = AVCOL_TRC_ARIB_STD_B67; + config.colorspace = AVCOL_SPC_BT2020_NCL; + } + + // 2.c) set width & height + config.width = (int)obs_output_get_width(stream->output); + config.height = (int)obs_output_get_height(stream->output); + config.scale_width = config.width; + config.scale_height = config.height; + + // 2.d) set video codec & id from video encoder + obs_encoder_t *vencoder = obs_output_get_video_encoder(stream->output); + config.video_encoder = obs_encoder_get_codec(vencoder); + if (strcmp(config.video_encoder, "h264") == 0) + config.video_encoder_id = AV_CODEC_ID_H264; + else + config.video_encoder_id = AV_CODEC_ID_AV1; + + // 2.e) set video bitrate & gop through video encoder settings + obs_data_t *settings = obs_encoder_get_settings(vencoder); + config.video_bitrate = (int)obs_data_get_int(settings, "bitrate"); + int keyint_sec = (int)obs_data_get_int(settings, "keyint_sec"); + config.gop_size = keyint_sec ? keyint_sec * voi->fps_num / voi->fps_den + : 250; + obs_data_release(settings); + + /* 3. Audio settings */ + // 3.a) set audio encoder and id to aac + obs_encoder_t *aencoder = + obs_output_get_audio_encoder(stream->output, 0); + config.audio_encoder = "aac"; + config.audio_encoder_id = AV_CODEC_ID_AAC; + + // 3.b) get audio bitrate from the audio encoder. + settings = obs_encoder_get_settings(aencoder); + config.audio_bitrate = (int)obs_data_get_int(settings, "bitrate"); + obs_data_release(settings); + + // 3.c set audio frame size + config.frame_size = (int)obs_encoder_get_frame_size(aencoder); + + // 3.d) set the number of tracks + // The UI for multiple tracks is not written for streaming outputs. + // When it is, modify write_packet & uncomment : + // config.audio_tracks = (int)obs_output_get_mixers(stream->output); + // config.audio_mix_count = get_audio_mix_count(config.audio_tracks); + config.audio_tracks = 1; + config.audio_mix_count = 1; + + /* 4. Muxer & protocol settings */ + // This requires some UI to be written for the output. + // at the service level unless one can load the output in the settings/stream screen. + settings = obs_output_get_settings(stream->output); + obs_data_set_default_string(settings, "muxer_settings", ""); + config.muxer_settings = obs_data_get_string(settings, "muxer_settings"); + obs_data_release(settings); + config.protocol_settings = ""; + + /* 5. unused ffmpeg codec settings */ + config.video_settings = ""; + config.audio_settings = ""; + + success = ffmpeg_mpegts_data_init(stream, &stream->ff_data, &config); + if (!success) { + if (stream->ff_data.last_error) { + obs_output_set_last_error(stream->output, + stream->ff_data.last_error); + } + ffmpeg_mpegts_data_free(stream, &stream->ff_data); + code = OBS_OUTPUT_INVALID_STREAM; + goto fail; + } + struct ffmpeg_data *ff_data = &stream->ff_data; + if (!stream->got_headers) { + if (!init_streams(stream, ff_data)) { + error("mpegts avstream failed to be created"); + code = OBS_OUTPUT_INVALID_STREAM; + goto fail; + } + code = open_output_file(stream, ff_data); + if (code != 0) { + error("failed to open the url"); + goto fail; + } + av_dump_format(ff_data->output, 0, NULL, 1); + } + if (!obs_output_can_begin_data_capture(stream->output, 0)) + return false; + if (!obs_output_initialize_encoders(stream->output, 0)) + return false; + + ret = pthread_create(&stream->write_thread, NULL, write_thread, stream); + if (ret != 0) { + ffmpeg_mpegts_log_error( + LOG_WARNING, &stream->ff_data, + "ffmpeg_output_start: failed to create write " + "thread."); + code = OBS_OUTPUT_ERROR; + goto fail; + } + os_atomic_set_bool(&stream->active, true); + stream->write_thread_active = true; + stream->total_bytes = 0; + obs_output_begin_data_capture(stream->output, 0); + + return true; +fail: + obs_output_signal_stop(stream->output, code); + ffmpeg_mpegts_full_stop(stream); + return false; +} + +static void *start_thread(void *data) +{ + struct ffmpeg_output *output = data; + set_config(output); + output->connecting = false; + return NULL; +} + +static bool ffmpeg_mpegts_start(void *data) +{ + struct ffmpeg_output *output = data; + int ret; + + if (output->connecting) + return false; + + os_atomic_set_bool(&output->stopping, false); + output->audio_start_ts = 0; + output->video_start_ts = 0; + output->total_bytes = 0; + output->got_headers = false; + + ret = pthread_create(&output->start_thread, NULL, start_thread, output); + return (output->connecting = (ret == 0)); +} + +static void ffmpeg_mpegts_full_stop(void *data) +{ + struct ffmpeg_output *output = data; + + if (output->active) { + obs_output_end_data_capture(output->output); + ffmpeg_mpegts_deactivate(output); + } +} + +static void ffmpeg_mpegts_stop(void *data, uint64_t ts) +{ + struct ffmpeg_output *output = data; + + if (output->active) { + if (ts > 0) { + output->stop_ts = ts; + os_atomic_set_bool(&output->stopping, true); + } + + ffmpeg_mpegts_full_stop(output); + } else { + obs_output_signal_stop(output->output, OBS_OUTPUT_SUCCESS); + } +} + +static void ffmpeg_mpegts_deactivate(struct ffmpeg_output *output) +{ + if (output->write_thread_active) { + os_event_signal(output->stop_event); + os_sem_post(output->write_sem); + pthread_join(output->write_thread, NULL); + output->write_thread_active = false; + } + + pthread_mutex_lock(&output->write_mutex); + + for (size_t i = 0; i < output->packets.num; i++) + av_packet_free(output->packets.array + i); + da_free(output->packets); + + pthread_mutex_unlock(&output->write_mutex); + + ffmpeg_mpegts_data_free(output, &output->ff_data); +} + +static uint64_t ffmpeg_mpegts_total_bytes(void *data) +{ + struct ffmpeg_output *output = data; + return output->total_bytes; +} + +static inline int64_t rescale_ts2(AVStream *stream, AVRational codec_time_base, + int64_t val) +{ + return av_rescale_q_rnd(val / codec_time_base.num, codec_time_base, + stream->time_base, + AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX); +} + +/* Convert obs encoder_packet to FFmpeg AVPacket and write to circular buffer + * where it will be processed in the write_thread by process_packet. + */ +void mpegts_write_packet(struct ffmpeg_output *stream, + struct encoder_packet *encpacket) +{ + if (stopping(stream) || !stream->ff_data.video || + !stream->ff_data.video_ctx || !stream->ff_data.audio_infos) + return; + if (!stream->ff_data.audio_infos[encpacket->track_idx].stream) + return; + bool is_video = encpacket->type == OBS_ENCODER_VIDEO; + AVStream *avstream = + is_video ? stream->ff_data.video + : stream->ff_data.audio_infos[encpacket->track_idx] + .stream; + AVPacket *packet = NULL; + + const AVRational codec_time_base = + is_video ? stream->ff_data.video_ctx->time_base + : stream->ff_data.audio_infos[encpacket->track_idx] + .ctx->time_base; + + packet = av_packet_alloc(); + + packet->data = av_memdup(encpacket->data, (int)encpacket->size); + if (packet->data == NULL) { + error("couldn't allocate packet data"); + goto fail; + } + packet->size = (int)encpacket->size; + packet->stream_index = avstream->id; + packet->pts = rescale_ts2(avstream, codec_time_base, encpacket->pts); + packet->dts = rescale_ts2(avstream, codec_time_base, encpacket->dts); + + if (encpacket->keyframe) + packet->flags = AV_PKT_FLAG_KEY; + + pthread_mutex_lock(&stream->write_mutex); + da_push_back(stream->packets, &packet); + pthread_mutex_unlock(&stream->write_mutex); + os_sem_post(stream->write_sem); + return; +fail: + av_packet_free(&packet); +} +static bool write_header(struct ffmpeg_output *stream, struct ffmpeg_data *data) +{ + AVDictionary *dict = NULL; + int ret; + /* get mpegts muxer settings (can be used with rist, srt, rtp, etc ... */ + if ((ret = av_dict_parse_string(&dict, data->config.muxer_settings, "=", + " ", 0))) { + ffmpeg_mpegts_log_error( + LOG_WARNING, data, + "Failed to parse muxer settings: %s\n%s", + av_err2str(ret), data->config.muxer_settings); + + av_dict_free(&dict); + return false; + } + + if (av_dict_count(dict) > 0) { + struct dstr str = {0}; + + AVDictionaryEntry *entry = NULL; + while ((entry = av_dict_get(dict, "", entry, + AV_DICT_IGNORE_SUFFIX))) + dstr_catf(&str, "\n\t%s=%s", entry->key, entry->value); + + info("Using muxer settings: %s", str.array); + dstr_free(&str); + } + + /* Allocate the stream private data and write the stream header. */ + ret = avformat_write_header(data->output, &dict); + if (ret < 0) { + ffmpeg_mpegts_log_error( + LOG_WARNING, data, + "Error setting stream header for '%s': %s", + data->config.url, av_err2str(ret)); + return false; + } + + /* Log invalid muxer settings. */ + if (av_dict_count(dict) > 0) { + struct dstr str = {0}; + + AVDictionaryEntry *entry = NULL; + while ((entry = av_dict_get(dict, "", entry, + AV_DICT_IGNORE_SUFFIX))) + dstr_catf(&str, "\n\t%s=%s", entry->key, entry->value); + + info("[ffmpeg mpegts muxer:] Invalid mpegts muxer settings: %s", + str.array); + dstr_free(&str); + } + av_dict_free(&dict); + + return true; +} + +static bool ffmpeg_mpegts_data(void *data, struct encoder_packet *packet) +{ + struct ffmpeg_output *stream = data; + struct ffmpeg_data *ff_data = &stream->ff_data; + int code; + if (!stream->got_headers) { + if (get_extradata(stream)) { + stream->got_headers = true; + } else { + warn("failed to retrieve headers"); + code = OBS_OUTPUT_INVALID_STREAM; + goto fail; + } + if (!write_header(stream, ff_data)) { + error("failed to write headers"); + code = OBS_OUTPUT_INVALID_STREAM; + goto fail; + } + av_dump_format(ff_data->output, 0, NULL, 1); + ff_data->initialized = true; + } + + if (!stream->active) + return 0; + + /* encoder failure */ + if (!packet) { + obs_output_signal_stop(stream->output, OBS_OUTPUT_ENCODE_ERROR); + ffmpeg_mpegts_deactivate(stream); + return 0; + } + + if (stopping(stream)) { + if (packet->sys_dts_usec >= (int64_t)stream->stop_ts) { + ffmpeg_mpegts_deactivate(stream); + return 0; + } + } + + mpegts_write_packet(stream, packet); + return 1; +fail: + obs_output_signal_stop(stream->output, code); + ffmpeg_mpegts_full_stop(stream); + return false; +} + +static obs_properties_t *ffmpeg_mpegts_properties(void *unused) +{ + UNUSED_PARAMETER(unused); + + obs_properties_t *props = obs_properties_create(); + + obs_properties_add_text(props, "path", obs_module_text("FilePath"), + OBS_TEXT_DEFAULT); + return props; +} + +struct obs_output_info ffmpeg_mpegts_muxer = { + .id = "ffmpeg_mpegts_muxer", + .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK | + OBS_OUTPUT_SERVICE, + .encoded_video_codecs = "h264;hevc;av1", + .encoded_audio_codecs = "aac", + .get_name = ffmpeg_mpegts_getname, + .create = ffmpeg_mpegts_create, + .destroy = ffmpeg_mpegts_destroy, + .start = ffmpeg_mpegts_start, + .stop = ffmpeg_mpegts_stop, + .encoded_packet = ffmpeg_mpegts_data, + .get_total_bytes = ffmpeg_mpegts_total_bytes, + .get_properties = ffmpeg_mpegts_properties, +}; diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c index 5724a075c..2018a8974 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-mux.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-mux.c @@ -36,12 +36,6 @@ static const char *ffmpeg_mux_getname(void *type) return obs_module_text("FFmpegMuxer"); } -static const char *ffmpeg_mpegts_mux_getname(void *type) -{ - UNUSED_PARAMETER(type); - return obs_module_text("FFmpegMpegtsMuxer"); -} - static inline void replay_buffer_clear(struct ffmpeg_muxer *stream) { while (stream->packets.size > 0) { @@ -898,30 +892,6 @@ static int connect_time(struct ffmpeg_muxer *stream) return 0; } -static int ffmpeg_mpegts_mux_connect_time(void *data) -{ - struct ffmpeg_muxer *stream = data; - /* TODO */ - return connect_time(stream); -} - -struct obs_output_info ffmpeg_mpegts_muxer = { - .id = "ffmpeg_mpegts_muxer", - .flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK | - OBS_OUTPUT_SERVICE, - .encoded_video_codecs = "h264;av1", - .encoded_audio_codecs = "aac", - .get_name = ffmpeg_mpegts_mux_getname, - .create = ffmpeg_mux_create, - .destroy = ffmpeg_mux_destroy, - .start = ffmpeg_mux_start, - .stop = ffmpeg_mux_stop, - .encoded_packet = ffmpeg_mux_data, - .get_total_bytes = ffmpeg_mux_total_bytes, - .get_properties = ffmpeg_mux_properties, - .get_connect_time_ms = ffmpeg_mpegts_mux_connect_time, -}; - /* ------------------------------------------------------------------------ */ static const char *replay_buffer_getname(void *type) diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-output.c b/plugins/obs-ffmpeg/obs-ffmpeg-output.c index 7a4a026e6..e7bd7a18c 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-output.c +++ b/plugins/obs-ffmpeg/obs-ffmpeg-output.c @@ -28,30 +28,6 @@ #include #include -struct ffmpeg_output { - obs_output_t *output; - volatile bool active; - struct ffmpeg_data ff_data; - - bool connecting; - pthread_t start_thread; - - uint64_t total_bytes; - - uint64_t audio_start_ts; - uint64_t video_start_ts; - uint64_t stop_ts; - volatile bool stopping; - - bool write_thread_active; - pthread_mutex_t write_mutex; - pthread_t write_thread; - os_sem_t *write_sem; - os_event_t *stop_event; - - DARRAY(AVPacket *) packets; -}; - /* ------------------------------------------------------------------------- */ static void ffmpeg_output_set_last_error(struct ffmpeg_data *data, diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-output.h b/plugins/obs-ffmpeg/obs-ffmpeg-output.h index 73f5f7852..87a972a28 100644 --- a/plugins/obs-ffmpeg/obs-ffmpeg-output.h +++ b/plugins/obs-ffmpeg/obs-ffmpeg-output.h @@ -5,12 +5,14 @@ #include #include #include +#include "obs-ffmpeg-url.h" struct ffmpeg_cfg { const char *url; const char *format_name; const char *format_mime_type; const char *muxer_settings; + const char *protocol_settings; // not used yet for SRT nor RIST int gop_size; int video_bitrate; int audio_bitrate; @@ -32,6 +34,7 @@ struct ffmpeg_cfg { int scale_height; int width; int height; + int frame_size; // audio frame size }; struct ffmpeg_audio_info { @@ -74,5 +77,38 @@ struct ffmpeg_data { char *last_error; }; +struct ffmpeg_output { + obs_output_t *output; + volatile bool active; + struct ffmpeg_data ff_data; + + bool connecting; + pthread_t start_thread; + + uint64_t total_bytes; + + uint64_t audio_start_ts; + uint64_t video_start_ts; + uint64_t stop_ts; + volatile bool stopping; + + bool write_thread_active; + pthread_mutex_t write_mutex; + pthread_t write_thread; + os_sem_t *write_sem; + os_event_t *stop_event; + + DARRAY(AVPacket *) packets; + /* used for SRT & RIST */ + URLContext *h; + AVIOContext *s; + bool got_headers; +}; bool ffmpeg_data_init(struct ffmpeg_data *data, struct ffmpeg_cfg *config); void ffmpeg_data_free(struct ffmpeg_data *data); + +#define SRT_PROTO "srt" +#define UDP_PROTO "udp" +#define TCP_PROTO "tcp" +#define HTTP_PROTO "http" +#define RIST_PROTO "rist" diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-rist.h b/plugins/obs-ffmpeg/obs-ffmpeg-rist.h new file mode 100644 index 000000000..f3844246b --- /dev/null +++ b/plugins/obs-ffmpeg/obs-ffmpeg-rist.h @@ -0,0 +1,269 @@ +/* + * The following code is a port of FFmpeg/avformat/librist.c for obs-studio. + * Port by pkv@obsproject.com. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#pragma once +#include +#include "obs-ffmpeg-url.h" +#include +#include + +// RIST_MAX_PACKET_SIZE - 28 minimum protocol overhead +#define RIST_MAX_PAYLOAD_SIZE (10000 - 28) + +#define FF_LIBRIST_MAKE_VERSION(major, minor, patch) \ + ((patch) + ((minor)*0x100) + ((major)*0x10000)) +#define FF_LIBRIST_VERSION \ + FF_LIBRIST_MAKE_VERSION(LIBRIST_API_VERSION_MAJOR, \ + LIBRIST_API_VERSION_MINOR, \ + LIBRIST_API_VERSION_PATCH) +#define FF_LIBRIST_VERSION_41 FF_LIBRIST_MAKE_VERSION(4, 1, 0) +#define FF_LIBRIST_VERSION_42 FF_LIBRIST_MAKE_VERSION(4, 2, 0) + +#define FF_LIBRIST_FIFO_DEFAULT_SHIFT 13 + +typedef struct RISTContext { + int profile; + int buffer_size; + int packet_size; + int log_level; + int encryption; + int fifo_shift; + bool overrun_nonfatal; + char *secret; + + struct rist_logging_settings logging_settings; + struct rist_peer_config peer_config; + + struct rist_peer *peer; + struct rist_ctx *ctx; + int statsinterval; + struct rist_stats_sender_peer *stats_list; +} RISTContext; + +static int risterr2ret(int err) +{ + switch (err) { + case RIST_ERR_MALLOC: + return AVERROR(ENOMEM); + default: + return AVERROR_EXTERNAL; + } +} + +static int log_cb(void *arg, enum rist_log_level log_level, const char *msg) +{ + int level; + + switch (log_level) { + case RIST_LOG_ERROR: + level = AV_LOG_ERROR; + break; + case RIST_LOG_WARN: + level = AV_LOG_WARNING; + break; + case RIST_LOG_NOTICE: + level = AV_LOG_INFO; + break; + case RIST_LOG_INFO: + level = AV_LOG_VERBOSE; + break; + case RIST_LOG_DEBUG: + level = AV_LOG_DEBUG; + break; + case RIST_LOG_DISABLE: + level = AV_LOG_QUIET; + break; + default: + level = AV_LOG_WARNING; + } + + av_log(arg, level, "%s", msg); + + return 0; +} + +static int librist_close(URLContext *h) +{ + RISTContext *s = h->priv_data; + int ret = 0; + + s->peer = NULL; + + if (s->ctx) + ret = rist_destroy(s->ctx); + if (ret < 0) { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / librist] : failed to close properly %s\n", + h->url); + return -1; + } + s->ctx = NULL; + + return 0; +} + +static int cb_stats(void *arg, const struct rist_stats *stats_container) +{ + RISTContext *s = (RISTContext *)arg; + rist_log(&s->logging_settings, RIST_LOG_INFO, "%s\n", + stats_container->stats_json); + if (stats_container->stats_type == RIST_STATS_SENDER_PEER) { + blog(LOG_DEBUG, + "[obs-ffmpeg mpegts muxer / librist] RIST STATS\n\n" + "bandwidth [%.3f Mbps]\npackets sent [%llu]\npkts received [%llu]\n" + "pkts retransmitted [%llu]\nquality (pkt sent over sent+retransmitted+skipped) [%.2f] \n" + "rtt [%" PRIu32 " ms]\n\n", + (double)(stats_container->stats.sender_peer.bandwidth) / + 1000000.0, + stats_container->stats.sender_peer.sent, + stats_container->stats.sender_peer.received, + stats_container->stats.sender_peer.retransmitted, + stats_container->stats.sender_peer.quality, + stats_container->stats.sender_peer.rtt); + } + rist_stats_free(stats_container); + return 0; +} + +static int librist_open(URLContext *h, const char *uri) +{ + RISTContext *s = h->priv_data; + struct rist_logging_settings *logging_settings = &s->logging_settings; + struct rist_peer_config *peer_config = &s->peer_config; + int ret; + s->buffer_size = 3000; + s->profile = RIST_PROFILE_MAIN; + s->packet_size = 1316; + s->log_level = RIST_LOG_INFO; + s->encryption = 0; + s->secret = NULL; + s->overrun_nonfatal = 0; + s->fifo_shift = FF_LIBRIST_FIFO_DEFAULT_SHIFT; + s->logging_settings = + (struct rist_logging_settings)LOGGING_SETTINGS_INITIALIZER; + s->statsinterval = 60000; // log stats every 60 seconds + + ret = rist_logging_set(&logging_settings, s->log_level, log_cb, h, NULL, + NULL); + if (ret < 0) { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / librist] : Failed to initialize logging settings."); + return OBS_OUTPUT_CONNECT_FAILED; + } + + blog(LOG_INFO, + "[obs-ffmpeg mpegts muxer / librist] : \n librist version %s & API = %s .", + librist_version(), librist_api_version()); + + h->max_packet_size = s->packet_size; + ret = rist_sender_create(&s->ctx, s->profile, 0, logging_settings); + + if (ret < 0) { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / librist] : failed to create a sender \n"); + goto err; + } + + ret = rist_peer_config_defaults_set(peer_config); + if (ret < 0) { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / librist] : failed to set peer config defaults.\n"); + goto err; + } + +#if FF_LIBRIST_VERSION < FF_LIBRIST_VERSION_41 + ret = rist_parse_address( + uri, (const struct rist_peer_config **)&peer_config); +#else + ret = rist_parse_address2(uri, &peer_config); +#endif + if (ret < 0) { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / librist] : failed to parse the url %s\n", + uri); + librist_close(h); + return OBS_OUTPUT_INVALID_STREAM; + } + + if (((s->encryption == 128 || s->encryption == 256) && !s->secret) || + ((peer_config->key_size == 128 || peer_config->key_size == 256) && + !peer_config->secret[0])) { + blog(LOG_ERROR, + "secret is mandatory if encryption is enabled\n"); + librist_close(h); + return OBS_OUTPUT_INVALID_STREAM; + } + + if (s->secret && peer_config->secret[0] == 0) + av_strlcpy(peer_config->secret, s->secret, + RIST_MAX_STRING_SHORT); + + if (s->secret && (s->encryption == 128 || s->encryption == 256)) + peer_config->key_size = s->encryption; + + if (s->buffer_size) { + peer_config->recovery_length_min = s->buffer_size; + peer_config->recovery_length_max = s->buffer_size; + } + + ret = rist_peer_create(s->ctx, &s->peer, &s->peer_config); + if (ret < 0) { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / librist] : failed to create a peer. \n"); + goto err; + } + + ret = rist_start(s->ctx); + if (ret < 0) { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / librist] : rist failed to start \n"); + goto err; + } + if (rist_stats_callback_set(s->ctx, s->statsinterval, cb_stats, + (void *)s) == -1) { + rist_log(&s->logging_settings, RIST_LOG_ERROR, + "Could not enable stats callback\n"); + ; + } + return 0; + +err: + librist_close(h); + + return OBS_OUTPUT_CONNECT_FAILED; +} + +static int librist_write(URLContext *h, const uint8_t *buf, int size) +{ + RISTContext *s = h->priv_data; + struct rist_data_block data_block = {0}; + int ret; + + data_block.ts_ntp = 0; + data_block.payload = buf; + data_block.payload_len = size; + + ret = rist_sender_data_write(s->ctx, &data_block); + if (ret < 0) { + blog(LOG_WARNING, + "[obs-ffmpeg mpegts muxer / librist] : failed to send data of size %i bytes", + size); + return risterr2ret(ret); + } + + return ret; +} diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-srt.h b/plugins/obs-ffmpeg/obs-ffmpeg-srt.h new file mode 100644 index 000000000..e85b3cacf --- /dev/null +++ b/plugins/obs-ffmpeg/obs-ffmpeg-srt.h @@ -0,0 +1,834 @@ +/* + * The following code is a port of FFmpeg/avformat/libsrt.c for obs-studio. + * Port by pkv@obsproject.com. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#pragma once +#include +#include "obs-ffmpeg-url.h" +#include +#include + +#define POLLING_TIME 100 /// Time in milliseconds between interrupt check + +/* This is for MPEG-TS (7 TS packets) */ +#ifndef SRT_LIVE_DEFAULT_PAYLOAD_SIZE +#define SRT_LIVE_DEFAULT_PAYLOAD_SIZE 1316 +#endif + +enum SRTMode { + SRT_MODE_CALLER = 0, + SRT_MODE_LISTENER = 1, + SRT_MODE_RENDEZVOUS = 2 +}; + +typedef struct SRTContext { + SRTSOCKET fd; + int eid; + int64_t rw_timeout; + int64_t listen_timeout; + int recv_buffer_size; + int send_buffer_size; + + int64_t maxbw; + int pbkeylen; + char *passphrase; +#if SRT_VERSION_VALUE >= 0x010302 + int enforced_encryption; + int kmrefreshrate; + int kmpreannounce; + int64_t snddropdelay; +#endif + int mss; + int ffs; + int ipttl; + int iptos; + int64_t inputbw; + int oheadbw; + int64_t latency; + int tlpktdrop; + int nakreport; + int64_t connect_timeout; + int payload_size; + int64_t rcvlatency; + int64_t peerlatency; + enum SRTMode mode; + int sndbuf; + int rcvbuf; + int lossmaxttl; + int minversion; + char *streamid; + char *smoother; + int messageapi; + SRT_TRANSTYPE transtype; + int linger; + int tsbpd; + double time; // time in s in order to post logs at definite intervals +} SRTContext; + +static int libsrt_neterrno(URLContext *h) +{ + int os_errno; + int err = srt_getlasterror(&os_errno); + blog(LOG_ERROR, "[obs-ffmpeg mpegts muxer / libsrt] : %s\n", + srt_getlasterror_str()); + if (err == SRT_EASYNCRCV || err == SRT_EASYNCSND) + return AVERROR(EAGAIN); + return os_errno ? AVERROR(os_errno) : AVERROR_UNKNOWN; +} + +static int libsrt_getsockopt(URLContext *h, SRTSOCKET fd, SRT_SOCKOPT optname, + const char *optnamestr, void *optval, int *optlen) +{ + if (srt_getsockopt(fd, 0, optname, optval, optlen) < 0) { + blog(LOG_INFO, + "[obs-ffmpeg mpegts muxer / libsrt] : failed to get option %s on socket: %s\n", + optnamestr, srt_getlasterror_str()); + return AVERROR(EIO); + } + return 0; +} + +static int libsrt_socket_nonblock(SRTSOCKET socket, int enable) +{ + int ret, blocking = enable ? 0 : 1; + /* Setting SRTO_{SND,RCV}SYN options to 1 enable blocking mode, setting them to 0 enable non-blocking mode. */ + ret = srt_setsockopt(socket, 0, SRTO_SNDSYN, &blocking, + sizeof(blocking)); + if (ret < 0) + return ret; + return srt_setsockopt(socket, 0, SRTO_RCVSYN, &blocking, + sizeof(blocking)); +} + +static int libsrt_epoll_create(URLContext *h, SRTSOCKET fd, int write) +{ + int modes = SRT_EPOLL_ERR | (write ? SRT_EPOLL_OUT : SRT_EPOLL_IN); + int eid = srt_epoll_create(); + if (eid < 0) + return libsrt_neterrno(h); + if (srt_epoll_add_usock(eid, fd, &modes) < 0) { + srt_epoll_release(eid); + return libsrt_neterrno(h); + } + return eid; +} + +static int libsrt_network_wait_fd(URLContext *h, int eid, int write) +{ + int ret, len = 1, errlen = 1; + SRTSOCKET ready[1]; + SRTSOCKET error[1]; + + if (write) { + ret = srt_epoll_wait(eid, error, &errlen, ready, &len, + POLLING_TIME, 0, 0, 0, 0); + } else { + ret = srt_epoll_wait(eid, ready, &len, error, &errlen, + POLLING_TIME, 0, 0, 0, 0); + } + if (ret < 0) { + if (srt_getlasterror(NULL) == SRT_ETIMEOUT) + ret = AVERROR(EAGAIN); + else + ret = libsrt_neterrno(h); + } else { + ret = errlen ? AVERROR(EIO) : 0; + } + return ret; +} + +int check_interrupt(AVIOInterruptCB *cb) +{ + if (cb && cb->callback) + return cb->callback(cb->opaque); + return 0; +} + +static int libsrt_network_wait_fd_timeout(URLContext *h, int eid, int write, + int64_t timeout, + AVIOInterruptCB *int_cb) +{ + int ret; + int64_t wait_start = 0; + + while (1) { + if (check_interrupt(int_cb)) + return AVERROR_EXIT; + ret = libsrt_network_wait_fd(h, eid, write); + if (ret != AVERROR(EAGAIN)) + return ret; + if (timeout > 0) { + if (!wait_start) + wait_start = av_gettime_relative(); + else if (av_gettime_relative() - wait_start > timeout) + return AVERROR(ETIMEDOUT); + } + } +} + +static int libsrt_listen(int eid, SRTSOCKET fd, const struct sockaddr *addr, + socklen_t addrlen, URLContext *h, int64_t timeout) +{ + int ret; + int reuse = 1; + /* Max streamid length plus an extra space for the terminating null character */ + char streamid[513]; + int streamid_len = sizeof(streamid); + if (srt_setsockopt(fd, SOL_SOCKET, SRTO_REUSEADDR, &reuse, + sizeof(reuse))) { + blog(LOG_WARNING, + "[obs-ffmpeg mpegts muxer / libsrt] : setsockopt(SRTO_REUSEADDR) failed\n"); + } + if (srt_bind(fd, addr, addrlen)) + return libsrt_neterrno(h); + + if (srt_listen(fd, 1)) + return libsrt_neterrno(h); + + ret = libsrt_network_wait_fd_timeout(h, eid, 1, timeout, + &h->interrupt_callback); + if (ret < 0) + return ret; + + ret = srt_accept(fd, NULL, NULL); + if (ret < 0) + return libsrt_neterrno(h); + if (libsrt_socket_nonblock(ret, 1) < 0) + blog(LOG_DEBUG, + "[obs-ffmpeg mpegts muxer / libsrt] : libsrt_socket_nonblock failed\n"); + if (!libsrt_getsockopt(h, ret, SRTO_STREAMID, "SRTO_STREAMID", streamid, + &streamid_len)) + /* Note: returned streamid_len doesn't count the terminating null character */ + blog(LOG_INFO, + "[obs-ffmpeg mpegts muxer / libsrt] : accept streamid [%s], length %d\n", + streamid, streamid_len); + + return ret; +} + +static int libsrt_listen_connect(int eid, SRTSOCKET fd, + const struct sockaddr *addr, socklen_t addrlen, + int64_t timeout, URLContext *h, + int will_try_next) +{ + int ret; + if (srt_connect(fd, addr, addrlen) < 0) + return libsrt_neterrno(h); + + ret = libsrt_network_wait_fd_timeout(h, eid, 1, timeout, + &h->interrupt_callback); + if (ret < 0) { + if (will_try_next) { + blog(LOG_WARNING, + "[obs-ffmpeg mpegts muxer / libsrt] : Connection to %s failed (%s), trying next address\n", + h->url, av_err2str(ret)); + } else { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / libsrt] : Connection to %s failed: %s\n", + h->url, av_err2str(ret)); + } + } + return ret; +} + +static int libsrt_setsockopt(URLContext *h, SRTSOCKET fd, SRT_SOCKOPT optname, + const char *optnamestr, const void *optval, + int optlen) +{ + if (srt_setsockopt(fd, 0, optname, optval, optlen) < 0) { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / libsrt] : failed to set option %s on socket: %s\n", + optnamestr, srt_getlasterror_str()); + return AVERROR(EIO); + } + return 0; +} + +/* - The "POST" options can be altered any time on a connected socket. + They MAY have also some meaning when set prior to connecting; such + option is SRTO_RCVSYN, which makes connect/accept call asynchronous. + Because of that this option is treated special way in this app. */ +static int libsrt_set_options_post(URLContext *h, SRTSOCKET fd) +{ + SRTContext *s = (SRTContext *)h->priv_data; + + if ((s->inputbw >= 0 && + libsrt_setsockopt(h, fd, SRTO_INPUTBW, "SRTO_INPUTBW", &s->inputbw, + sizeof(s->inputbw)) < 0) || + (s->oheadbw >= 0 && + libsrt_setsockopt(h, fd, SRTO_OHEADBW, "SRTO_OHEADBW", &s->oheadbw, + sizeof(s->oheadbw)) < 0)) { + return AVERROR(EIO); + } + return 0; +} + +/* - The "PRE" options must be set prior to connecting and can't be altered + on a connected socket, however if set on a listening socket, they are + derived by accept-ed socket. */ +static int libsrt_set_options_pre(URLContext *h, SRTSOCKET fd) +{ + SRTContext *s = (SRTContext *)h->priv_data; + int yes = 1; + int latency = (int)(s->latency / 1000); + int rcvlatency = (int)(s->rcvlatency / 1000); + int peerlatency = (int)(s->peerlatency / 1000); +#if SRT_VERSION_VALUE >= 0x010302 + int snddropdelay = s->snddropdelay > 0 ? (int)(s->snddropdelay / 1000) + : (int)(s->snddropdelay); +#endif + int connect_timeout = (int)(s->connect_timeout); + + if ((s->mode == SRT_MODE_RENDEZVOUS && + libsrt_setsockopt(h, fd, SRTO_RENDEZVOUS, "SRTO_RENDEZVOUS", &yes, + sizeof(yes)) < 0) || + (s->transtype != SRTT_INVALID && + libsrt_setsockopt(h, fd, SRTO_TRANSTYPE, "SRTO_TRANSTYPE", + &s->transtype, sizeof(s->transtype)) < 0) || + (s->maxbw >= 0 && + libsrt_setsockopt(h, fd, SRTO_MAXBW, "SRTO_MAXBW", &s->maxbw, + sizeof(s->maxbw)) < 0) || + (s->pbkeylen >= 0 && + libsrt_setsockopt(h, fd, SRTO_PBKEYLEN, "SRTO_PBKEYLEN", + &s->pbkeylen, sizeof(s->pbkeylen)) < 0) || + (s->passphrase && + libsrt_setsockopt(h, fd, SRTO_PASSPHRASE, "SRTO_PASSPHRASE", + s->passphrase, + (int)strlen(s->passphrase)) < 0) || +#if SRT_VERSION_VALUE >= 0x010302 +#if SRT_VERSION_VALUE >= 0x010401 + (s->enforced_encryption >= 0 && + libsrt_setsockopt(h, fd, SRTO_ENFORCEDENCRYPTION, + "SRTO_ENFORCEDENCRYPTION", + &s->enforced_encryption, + sizeof(s->enforced_encryption)) < 0) || +#else + /* SRTO_STRICTENC == SRTO_ENFORCEDENCRYPTION (53), but for compatibility, we used SRTO_STRICTENC */ + (s->enforced_encryption >= 0 && + libsrt_setsockopt(h, fd, SRTO_STRICTENC, "SRTO_STRICTENC", + &s->enforced_encryption, + sizeof(s->enforced_encryption)) < 0) || +#endif + (s->kmrefreshrate >= 0 && + libsrt_setsockopt(h, fd, SRTO_KMREFRESHRATE, "SRTO_KMREFRESHRATE", + &s->kmrefreshrate, + sizeof(s->kmrefreshrate)) < 0) || + (s->kmpreannounce >= 0 && + libsrt_setsockopt(h, fd, SRTO_KMPREANNOUNCE, "SRTO_KMPREANNOUNCE", + &s->kmpreannounce, + sizeof(s->kmpreannounce)) < 0) || + (s->snddropdelay >= -1 && + libsrt_setsockopt(h, fd, SRTO_SNDDROPDELAY, "SRTO_SNDDROPDELAY", + &snddropdelay, sizeof(snddropdelay)) < 0) || +#endif + (s->mss >= 0 && libsrt_setsockopt(h, fd, SRTO_MSS, "SRTO_MSS", + &s->mss, sizeof(s->mss)) < 0) || + (s->ffs >= 0 && libsrt_setsockopt(h, fd, SRTO_FC, "SRTO_FC", + &s->ffs, sizeof(s->ffs)) < 0) || + (s->ipttl >= 0 && + libsrt_setsockopt(h, fd, SRTO_IPTTL, "SRTO_IPTTL", &s->ipttl, + sizeof(s->ipttl)) < 0) || + (s->iptos >= 0 && + libsrt_setsockopt(h, fd, SRTO_IPTOS, "SRTO_IPTOS", &s->iptos, + sizeof(s->iptos)) < 0) || + (s->latency >= 0 && + libsrt_setsockopt(h, fd, SRTO_LATENCY, "SRTO_LATENCY", &latency, + sizeof(latency)) < 0) || + (s->rcvlatency >= 0 && + libsrt_setsockopt(h, fd, SRTO_RCVLATENCY, "SRTO_RCVLATENCY", + &rcvlatency, sizeof(rcvlatency)) < 0) || + (s->peerlatency >= 0 && + libsrt_setsockopt(h, fd, SRTO_PEERLATENCY, "SRTO_PEERLATENCY", + &peerlatency, sizeof(peerlatency)) < 0) || + (s->tlpktdrop >= 0 && + libsrt_setsockopt(h, fd, SRTO_TLPKTDROP, "SRTO_TLPKTDROP", + &s->tlpktdrop, sizeof(s->tlpktdrop)) < 0) || + (s->nakreport >= 0 && + libsrt_setsockopt(h, fd, SRTO_NAKREPORT, "SRTO_NAKREPORT", + &s->nakreport, sizeof(s->nakreport)) < 0) || + (connect_timeout >= 0 && + libsrt_setsockopt(h, fd, SRTO_CONNTIMEO, "SRTO_CONNTIMEO", + &connect_timeout, + sizeof(connect_timeout)) < 0) || + (s->sndbuf >= 0 && + libsrt_setsockopt(h, fd, SRTO_SNDBUF, "SRTO_SNDBUF", &s->sndbuf, + sizeof(s->sndbuf)) < 0) || + (s->rcvbuf >= 0 && + libsrt_setsockopt(h, fd, SRTO_RCVBUF, "SRTO_RCVBUF", &s->rcvbuf, + sizeof(s->rcvbuf)) < 0) || + (s->lossmaxttl >= 0 && + libsrt_setsockopt(h, fd, SRTO_LOSSMAXTTL, "SRTO_LOSSMAXTTL", + &s->lossmaxttl, sizeof(s->lossmaxttl)) < 0) || + (s->minversion >= 0 && + libsrt_setsockopt(h, fd, SRTO_MINVERSION, "SRTO_MINVERSION", + &s->minversion, sizeof(s->minversion)) < 0) || + (s->streamid && + libsrt_setsockopt(h, fd, SRTO_STREAMID, "SRTO_STREAMID", + s->streamid, (int)strlen(s->streamid)) < 0) || +#if SRT_VERSION_VALUE >= 0x010401 + (s->smoother && + libsrt_setsockopt(h, fd, SRTO_CONGESTION, "SRTO_CONGESTION", + s->smoother, (int)strlen(s->smoother)) < 0) || +#else + (s->smoother && + libsrt_setsockopt(h, fd, SRTO_SMOOTHER, "SRTO_SMOOTHER", + s->smoother, (int)strlen(s->smoother)) < 0) || +#endif + (s->messageapi >= 0 && + libsrt_setsockopt(h, fd, SRTO_MESSAGEAPI, "SRTO_MESSAGEAPI", + &s->messageapi, sizeof(s->messageapi)) < 0) || + (s->payload_size >= 0 && + libsrt_setsockopt(h, fd, SRTO_PAYLOADSIZE, "SRTO_PAYLOADSIZE", + &s->payload_size, + sizeof(s->payload_size)) < 0) || + (/*(h->flags & AVIO_FLAG_WRITE) &&*/ + libsrt_setsockopt(h, fd, SRTO_SENDER, "SRTO_SENDER", &yes, + sizeof(yes)) < 0) || + (s->tsbpd >= 0 && + libsrt_setsockopt(h, fd, SRTO_TSBPDMODE, "SRTO_TSBPDMODE", + &s->tsbpd, sizeof(s->tsbpd)) < 0)) { + return AVERROR(EIO); + } + + if (s->linger >= 0) { + struct linger lin; + lin.l_linger = s->linger; + lin.l_onoff = lin.l_linger > 0 ? 1 : 0; + if (libsrt_setsockopt(h, fd, SRTO_LINGER, "SRTO_LINGER", &lin, + sizeof(lin)) < 0) + return AVERROR(EIO); + } + return 0; +} + +static int libsrt_setup(URLContext *h, const char *uri) +{ + struct addrinfo hints = {0}, *ai, *cur_ai; + int port; + SRTSOCKET fd; + SRTContext *s = (SRTContext *)h->priv_data; + const char *p; + char buf[1024]; + int ret; + char hostname[1024], proto[1024], path[1024]; + char portstr[10]; + int64_t open_timeout = 0; + int eid, write_eid; + + av_url_split(proto, sizeof(proto), NULL, 0, hostname, sizeof(hostname), + &port, path, sizeof(path), uri); + if (strcmp(proto, "srt")) // should not happen ! + return AVERROR(EINVAL); + if (port <= 0 || port >= 65536) { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / libsrt] : Port missing in uri\n"); + return OBS_OUTPUT_CONNECT_FAILED; + } + p = strchr(uri, '?'); + if (p) { + if (av_find_info_tag(buf, sizeof(buf), "timeout", p)) { + s->rw_timeout = strtoll(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "listen_timeout", p)) { + s->listen_timeout = strtoll(buf, NULL, 10); + } + } + if (s->rw_timeout >= 0) { + open_timeout = h->rw_timeout = s->rw_timeout; + } + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + snprintf(portstr, sizeof(portstr), "%d", port); + if (s->mode == SRT_MODE_LISTENER) + hints.ai_flags |= AI_PASSIVE; + ret = getaddrinfo(hostname[0] ? hostname : NULL, portstr, &hints, &ai); + if (ret) { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / libsrt] : Failed to resolve hostname %s: %s\n", + hostname, gai_strerror(ret)); + return OBS_OUTPUT_CONNECT_FAILED; + } + + cur_ai = ai; + +restart: + + fd = srt_create_socket(); + if (fd < 0) { + ret = libsrt_neterrno(h); + goto fail; + } + + if ((ret = libsrt_set_options_pre(h, fd)) < 0) { + goto fail; + } + + /* Set the socket's send or receive buffer sizes, if specified. + If unspecified or setting fails, system default is used. */ + if (s->recv_buffer_size > 0) { + srt_setsockopt(fd, SOL_SOCKET, SRTO_UDP_RCVBUF, + &s->recv_buffer_size, + sizeof(s->recv_buffer_size)); + } + if (s->send_buffer_size > 0) { + srt_setsockopt(fd, SOL_SOCKET, SRTO_UDP_SNDBUF, + &s->send_buffer_size, + sizeof(s->send_buffer_size)); + } + if (libsrt_socket_nonblock(fd, 1) < 0) + blog(LOG_DEBUG, + "[obs-ffmpeg mpegts muxer / libsrt] : libsrt_socket_nonblock failed\n"); + + ret = write_eid = libsrt_epoll_create(h, fd, 1); + if (ret < 0) + goto fail1; + if (s->mode == SRT_MODE_LISTENER) { + // multi-client + ret = libsrt_listen(write_eid, fd, cur_ai->ai_addr, + (socklen_t)cur_ai->ai_addrlen, h, + s->listen_timeout); + srt_epoll_release(write_eid); + if (ret < 0) + goto fail1; + srt_close(fd); + fd = ret; + } else { + if (s->mode == SRT_MODE_RENDEZVOUS) { + if (srt_bind(fd, cur_ai->ai_addr, + (int)(cur_ai->ai_addrlen))) { + ret = libsrt_neterrno(h); + srt_epoll_release(write_eid); + goto fail1; + } + } + + ret = libsrt_listen_connect(write_eid, fd, cur_ai->ai_addr, + (socklen_t)(cur_ai->ai_addrlen), + open_timeout, h, !!cur_ai->ai_next); + srt_epoll_release(write_eid); + if (ret < 0) { + if (ret == AVERROR_EXIT) + goto fail1; + else + goto fail; + } + } + if ((ret = libsrt_set_options_post(h, fd)) < 0) { + goto fail; + } + + int packet_size = 0; + int optlen = sizeof(packet_size); + ret = libsrt_getsockopt(h, fd, SRTO_PAYLOADSIZE, "SRTO_PAYLOADSIZE", + &packet_size, &optlen); + if (ret < 0) + goto fail1; + if (packet_size > 0) + h->max_packet_size = packet_size; + + ret = eid = libsrt_epoll_create(h, fd, 1 /*flags & AVIO_FLAG_WRITE*/); + if (eid < 0) + goto fail1; + + s->fd = fd; + s->eid = eid; + + freeaddrinfo(ai); + return 0; + +fail: + if (cur_ai->ai_next) { + /* Retry with the next sockaddr */ + cur_ai = cur_ai->ai_next; + if (fd >= 0) + srt_close(fd); + ret = 0; + goto restart; + } +fail1: + if (fd >= 0) + srt_close(fd); + freeaddrinfo(ai); + return ret; +} + +static void libsrt_set_defaults(SRTContext *s) +{ + s->rw_timeout = -1; + s->listen_timeout = -1; + s->send_buffer_size = -1; + s->recv_buffer_size = -1; + s->payload_size = SRT_LIVE_DEFAULT_PAYLOAD_SIZE; + s->maxbw = -1; + s->pbkeylen = -1; + s->passphrase = NULL; + s->mss = -1; + s->ffs = -1; + s->ipttl = -1; + s->iptos = -1; + s->inputbw = -1; + s->oheadbw = -1; + s->latency = -1; + s->rcvlatency = -1; + s->peerlatency = -1; + s->tlpktdrop = -1; + s->nakreport = -1; + s->connect_timeout = -1; + s->mode = SRT_MODE_CALLER; + s->sndbuf = -1; + s->rcvbuf = -1; + s->lossmaxttl = -1; + s->minversion = -1; + s->streamid = NULL; + s->smoother = NULL; + s->messageapi = -1; + s->transtype = SRTT_LIVE; + s->linger = -1; + s->tsbpd = -1; +} + +static int libsrt_open(URLContext *h, const char *uri) +{ + SRTContext *s = (SRTContext *)h->priv_data; + const char *p; + char buf[1024]; + int ret = 0; + + if (srt_startup() < 0) { + blog(LOG_ERROR, + "[obs-ffmpeg mpegts muxer / libsrt] : libsrt failed to load.\n"); + return OBS_OUTPUT_CONNECT_FAILED; + } else { + blog(LOG_INFO, + "[obs-ffmpeg mpegts muxer / libsrt] : libsrt v.%s loaded.\n", + SRT_VERSION_STRING); + } + libsrt_set_defaults(s); + + /* SRT options (srt/srt.h) */ + p = strchr(uri, '?'); + if (p) { + if (av_find_info_tag(buf, sizeof(buf), "maxbw", p)) { + s->maxbw = strtoll(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "pbkeylen", p)) { + s->pbkeylen = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "passphrase", p)) { + av_freep(&s->passphrase); + s->passphrase = av_strndup(buf, strlen(buf)); + } +#if SRT_VERSION_VALUE >= 0x010302 + if (av_find_info_tag(buf, sizeof(buf), "enforced_encryption", + p)) { + s->enforced_encryption = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "kmrefreshrate", p)) { + s->kmrefreshrate = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "kmpreannounce", p)) { + s->kmpreannounce = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "snddropdelay", p)) { + s->snddropdelay = strtoll(buf, NULL, 10); + } +#endif + if (av_find_info_tag(buf, sizeof(buf), "mss", p)) { + s->mss = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "ffs", p)) { + s->ffs = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "ipttl", p)) { + s->ipttl = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "iptos", p)) { + s->iptos = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "inputbw", p)) { + s->inputbw = strtoll(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "oheadbw", p)) { + s->oheadbw = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "latency", p)) { + s->latency = strtoll(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "tsbpddelay", p)) { + s->latency = strtoll(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "rcvlatency", p)) { + s->rcvlatency = strtoll(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "peerlatency", p)) { + s->peerlatency = strtoll(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "tlpktdrop", p)) { + s->tlpktdrop = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "nakreport", p)) { + s->nakreport = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "connect_timeout", p)) { + s->connect_timeout = strtoll(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "payload_size", p) || + av_find_info_tag(buf, sizeof(buf), "pkt_size", p)) { + s->payload_size = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "mode", p)) { + if (!strcmp(buf, "caller")) { + s->mode = SRT_MODE_CALLER; + } else if (!strcmp(buf, "listener")) { + s->mode = SRT_MODE_LISTENER; + } else if (!strcmp(buf, "rendezvous")) { + s->mode = SRT_MODE_RENDEZVOUS; + } else { + ret = AVERROR(EINVAL); + goto err; + } + } + if (av_find_info_tag(buf, sizeof(buf), "sndbuf", p)) { + s->sndbuf = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "rcvbuf", p)) { + s->rcvbuf = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "lossmaxttl", p)) { + s->lossmaxttl = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "minversion", p)) { + s->minversion = strtol(buf, NULL, 0); + } + if (av_find_info_tag(buf, sizeof(buf), "streamid", p)) { + av_freep(&s->streamid); + s->streamid = av_strdup(buf); + if (!s->streamid) { + ret = AVERROR(ENOMEM); + goto err; + } + } + if (av_find_info_tag(buf, sizeof(buf), "smoother", p)) { + av_freep(&s->smoother); + s->smoother = av_strdup(buf); + if (!s->smoother) { + ret = AVERROR(ENOMEM); + goto err; + } + } + if (av_find_info_tag(buf, sizeof(buf), "messageapi", p)) { + s->messageapi = strtol(buf, NULL, 10); + } + if (av_find_info_tag(buf, sizeof(buf), "transtype", p)) { + if (!strcmp(buf, "live")) { + s->transtype = SRTT_LIVE; + } else if (!strcmp(buf, "file")) { + s->transtype = SRTT_FILE; + } else { + ret = AVERROR(EINVAL); + goto err; + } + } + if (av_find_info_tag(buf, sizeof(buf), "linger", p)) { + s->linger = strtol(buf, NULL, 10); + } + } + ret = libsrt_setup(h, uri); + if (ret < 0) + goto err; + + struct timeb timebuffer; + ftime(&timebuffer); + s->time = (double)timebuffer.time + 0.001 * (double)timebuffer.millitm; + + return 0; + +err: + av_freep(&s->smoother); + av_freep(&s->streamid); + srt_cleanup(); + return ret; +} + +static int libsrt_write(URLContext *h, const uint8_t *buf, int size) +{ + SRTContext *s = (SRTContext *)h->priv_data; + int ret; + SRT_TRACEBSTATS perf; + + ret = libsrt_network_wait_fd_timeout(h, s->eid, 1, h->rw_timeout, + &h->interrupt_callback); + if (ret) + return ret; + + ret = srt_send(s->fd, (char *)buf, size); + if (ret < 0) { + ret = libsrt_neterrno(h); + } else { + /* log every 60 seconds the rtt and link bandwidth + * rtt: round-trip time + * link bandwidth: bandwidth from ingest to egress + */ + struct timeb timebuffer; + ftime(&timebuffer); + double time = (double)timebuffer.time + + 0.001 * (double)timebuffer.millitm; + if (time > (s->time + 60.0)) { + srt_bistats(s->fd, &perf, 0, 1); + blog(LOG_INFO, + "[obs-ffmpeg mpegts muxer / libsrt] : rtt [%.2f ms], link bandwidth [%.1f Mbps]\n", + perf.msRTT, perf.mbpsBandwidth); + s->time = time; + } + } + + return ret; +} + +static int libsrt_close(URLContext *h) +{ + SRTContext *s = (SRTContext *)h->priv_data; + + /* Log stream stats. */ + SRT_TRACEBSTATS perf; + srt_bstats(s->fd, &perf, 1); + blog(LOG_INFO, + "[obs-ffmpeg mpegts muxer / libsrt] : Stream stats\n\n" + "time elapsed [%.1f sec]\nmean speed [%.1f Mbp]\n" + "total bytes sent [%.1f MB]\nbytes retransmitted [%.1f %%]\n" + "bytes dropped [%.1f %%]\n\n", + (double)perf.msTimeStamp / 1000.0, perf.mbpsSendRate, + (double)perf.byteSentTotal / 1000000.0, + perf.byteSentTotal + ? perf.byteRetransTotal / perf.byteSentTotal * 100.0 + : 0, + perf.byteSentTotal + ? perf.byteSndDropTotal / perf.byteSentTotal * 100.0 + : 0); + + srt_epoll_release(s->eid); + int err = srt_close(s->fd); + if (err < 0) { + blog(LOG_ERROR, "[obs-ffmpeg mpegts muxer / libsrt] : %s\n", + srt_getlasterror_str()); + return -1; + } + + srt_cleanup(); + blog(LOG_INFO, "[obs-ffmpeg mpegts muxer / libsrt] : closing srt"); + + return 0; +} diff --git a/plugins/obs-ffmpeg/obs-ffmpeg-url.h b/plugins/obs-ffmpeg/obs-ffmpeg-url.h new file mode 100644 index 000000000..213c21a53 --- /dev/null +++ b/plugins/obs-ffmpeg/obs-ffmpeg-url.h @@ -0,0 +1,143 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define HTTP_PROTO "http" +#define RIST_PROTO "rist" +#define SRT_PROTO "srt" +#define TCP_PROTO "tcp" +#define UDP_PROTO "udp" + +/* lightened version of a struct used by avformat */ +typedef struct URLContext { + void *priv_data; /* SRTContext or RISTContext */ + char *url; /* URL */ + int max_packet_size; + AVIOInterruptCB interrupt_callback; + int64_t rw_timeout; /* max time to wait for write completion in mcs */ +} URLContext; + +#define UDP_DEFAULT_PAYLOAD_SIZE 1316 + +/* We need to override libsrt/win/syslog_defs.h due to conflicts w/ some libobs + * definitions. + */ +#ifndef _WIN32 +#define _SYS_SYSLOG_H +#endif + +#ifdef _WIN32 +#ifndef INC_SRT_WINDOWS_SYSLOG_DEFS_H +#define INC_SRT_WINDOWS_SYSLOG_DEFS_H + +#define LOG_EMERG 0 +#define LOG_ALERT 1 +#define LOG_CRIT 2 +#define LOG_ERR 3 +//#define LOG_WARNING 4 // this creates issues w/ libobs LOG_WARNING = 200 +#define LOG_NOTICE 5 +//#define LOG_INFO 6 // issue w/ libobs +//#define LOG_DEBUG 7 // issue w/ libobs +#define LOG_PRIMASK 0x07 + +#define LOG_PRI(p) ((p)&LOG_PRIMASK) +#define LOG_MAKEPRI(fac, pri) (((fac) << 3) | (pri)) + +#define LOG_KERN (0 << 3) +#define LOG_USER (1 << 3) +#define LOG_MAIL (2 << 3) +#define LOG_DAEMON (3 << 3) +#define LOG_AUTH (4 << 3) +#define LOG_SYSLOG (5 << 3) +#define LOG_LPR (6 << 3) +#define LOG_NEWS (7 << 3) +#define LOG_UUCP (8 << 3) +#define LOG_CRON (9 << 3) +#define LOG_AUTHPRIV (10 << 3) +#define LOG_FTP (11 << 3) + +/* Codes through 15 are reserved for system use */ +#define LOG_LOCAL0 (16 << 3) +#define LOG_LOCAL1 (17 << 3) +#define LOG_LOCAL2 (18 << 3) +#define LOG_LOCAL3 (19 << 3) +#define LOG_LOCAL4 (20 << 3) +#define LOG_LOCAL5 (21 << 3) +#define LOG_LOCAL6 (22 << 3) +#define LOG_LOCAL7 (23 << 3) + +#define LOG_NFACILITIES 24 +#define LOG_FACMASK 0x03f8 +#define LOG_FAC(p) (((p)&LOG_FACMASK) >> 3) +#endif +#endif + +/* We need to override libsrt/logging_api.h due to conflicts with some libobs + * definitions. + */ + +#define INC_SRT_LOGGING_API_H + +// These are required for access functions: +// - adding FA (requires set) +// - setting a log stream (requires iostream) +#ifdef __cplusplus +#include +#include +#endif + +#ifndef _WIN32 +#include +#endif + +// Syslog is included so that it provides log level names. +// Haivision log standard requires the same names plus extra one: +#ifndef LOG_DEBUG_TRACE +#define LOG_DEBUG_TRACE 8 +#endif + +// Flags +#define SRT_LOGF_DISABLE_TIME 1 +#define SRT_LOGF_DISABLE_THREADNAME 2 +#define SRT_LOGF_DISABLE_SEVERITY 4 +#define SRT_LOGF_DISABLE_EOL 8 + +// Handler type +typedef void SRT_LOG_HANDLER_FN(void *opaque, int level, const char *file, + int line, const char *area, + const char *message); + +#ifdef __cplusplus +namespace srt_logging { + +struct LogFA { +private: + int value; + +public: + operator int() const { return value; } + + LogFA(int v) : value(v) {} +}; + +const LogFA LOGFA_GENERAL = 0; + +namespace LogLevel { +enum type { + fatal = LOG_CRIT, + error = LOG_ERR, + warning = 4, //issue w/ libobs so LOG_WARNING is removed + note = LOG_NOTICE, + debug = 7 //issue w/ libobs so LOG_DEBUG is removed +}; +} +class Logger; +} +#endif