commit
19cb3b9463
|
@ -174,6 +174,7 @@ Basic.AutoConfig.StreamPage.DisconnectAccount="Disconnect Account"
|
|||
Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Title="Disconnect Account?"
|
||||
Basic.AutoConfig.StreamPage.DisconnectAccount.Confirm.Text="This change will apply immediately. Are you sure you want to disconnect your account?"
|
||||
Basic.AutoConfig.StreamPage.GetStreamKey="Get Stream Key"
|
||||
Basic.AutoConfig.StreamPage.MoreInfo="More Info"
|
||||
Basic.AutoConfig.StreamPage.UseStreamKey="Use Stream Key"
|
||||
Basic.AutoConfig.StreamPage.Service="Service"
|
||||
Basic.AutoConfig.StreamPage.Service.ShowAll="Show All..."
|
||||
|
|
|
@ -74,7 +74,32 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="service"/>
|
||||
<widget class="QWidget" name="serviceWidget" native="true">
|
||||
<layout class="QHBoxLayout" name="serviceWidgetLayout">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QComboBox" name="service"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="UrlPushButton" name="moreInfoButton">
|
||||
<property name="text">
|
||||
<string>Basic.AutoConfig.StreamPage.MoreInfo</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<spacer name="verticalSpacer">
|
||||
|
|
|
@ -824,10 +824,41 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QComboBox" name="service">
|
||||
<property name="maxVisibleItems">
|
||||
<number>20</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="serviceWidget" native="true">
|
||||
<layout class="QHBoxLayout" name="serviceWidgetLayout" stretch="0,0">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QComboBox" name="service">
|
||||
<property name="maxVisibleItems">
|
||||
<number>20</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="UrlPushButton" name="moreInfoButton">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Basic.AutoConfig.StreamPage.MoreInfo</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
|
|
|
@ -285,6 +285,8 @@ AutoConfigStreamPage::AutoConfigStreamPage(QWidget *parent)
|
|||
|
||||
connect(ui->service, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(UpdateKeyLink()));
|
||||
connect(ui->service, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(UpdateMoreInfoLink()));
|
||||
|
||||
connect(ui->key, SIGNAL(textChanged(const QString &)), this,
|
||||
SLOT(UpdateCompleted()));
|
||||
|
@ -575,6 +577,35 @@ void AutoConfigStreamPage::ServiceChanged()
|
|||
UpdateCompleted();
|
||||
}
|
||||
|
||||
void AutoConfigStreamPage::UpdateMoreInfoLink()
|
||||
{
|
||||
if (IsCustomService()) {
|
||||
ui->moreInfoButton->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
QString serviceName = ui->service->currentText();
|
||||
obs_properties_t *props = obs_get_service_properties("rtmp_common");
|
||||
obs_property_t *services = obs_properties_get(props, "service");
|
||||
|
||||
OBSData settings = obs_data_create();
|
||||
obs_data_release(settings);
|
||||
|
||||
obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName));
|
||||
obs_property_modified(services, settings);
|
||||
|
||||
const char *more_info_link =
|
||||
obs_data_get_string(settings, "more_info_link");
|
||||
|
||||
if (!more_info_link || (*more_info_link == '\0')) {
|
||||
ui->moreInfoButton->hide();
|
||||
} else {
|
||||
ui->moreInfoButton->setTargetUrl(QUrl(more_info_link));
|
||||
ui->moreInfoButton->show();
|
||||
}
|
||||
obs_properties_destroy(props);
|
||||
}
|
||||
|
||||
void AutoConfigStreamPage::UpdateKeyLink()
|
||||
{
|
||||
QString serviceName = ui->service->currentText();
|
||||
|
@ -585,7 +616,7 @@ void AutoConfigStreamPage::UpdateKeyLink()
|
|||
if (serviceName == "Twitch") {
|
||||
streamKeyLink =
|
||||
"https://www.twitch.tv/broadcast/dashboard/streamkey";
|
||||
} else if (serviceName == "YouTube / YouTube Gaming") {
|
||||
} else if (serviceName.startsWith("YouTube")) {
|
||||
streamKeyLink = "https://www.youtube.com/live_dashboard";
|
||||
isYoutube = true;
|
||||
} else if (serviceName.startsWith("Restream.io")) {
|
||||
|
@ -814,6 +845,7 @@ AutoConfig::AutoConfig(QWidget *parent) : QWizard(parent)
|
|||
|
||||
streamPage->UpdateServerList();
|
||||
streamPage->UpdateKeyLink();
|
||||
streamPage->UpdateMoreInfoLink();
|
||||
streamPage->lastService.clear();
|
||||
|
||||
if (!customServer) {
|
||||
|
|
|
@ -195,6 +195,7 @@ public slots:
|
|||
void on_useStreamKey_clicked();
|
||||
void ServiceChanged();
|
||||
void UpdateKeyLink();
|
||||
void UpdateMoreInfoLink();
|
||||
void UpdateServerList();
|
||||
void UpdateCompleted();
|
||||
};
|
||||
|
|
|
@ -75,6 +75,8 @@ void OBSBasicSettings::InitStreamPage()
|
|||
SLOT(UpdateKeyLink()));
|
||||
connect(ui->customServer, SIGNAL(editingFinished(const QString &)),
|
||||
this, SLOT(UpdateKeyLink()));
|
||||
connect(ui->service, SIGNAL(currentIndexChanged(int)), this,
|
||||
SLOT(UpdateMoreInfoLink()));
|
||||
}
|
||||
|
||||
void OBSBasicSettings::LoadStream1Settings()
|
||||
|
@ -138,6 +140,7 @@ void OBSBasicSettings::LoadStream1Settings()
|
|||
obs_data_release(settings);
|
||||
|
||||
UpdateKeyLink();
|
||||
UpdateMoreInfoLink();
|
||||
|
||||
bool streamActive = obs_frontend_streaming_active();
|
||||
ui->streamPage->setEnabled(!streamActive);
|
||||
|
@ -212,6 +215,35 @@ void OBSBasicSettings::SaveStream1Settings()
|
|||
main->auth->LoadUI();
|
||||
}
|
||||
|
||||
void OBSBasicSettings::UpdateMoreInfoLink()
|
||||
{
|
||||
if (IsCustomService()) {
|
||||
ui->moreInfoButton->hide();
|
||||
return;
|
||||
}
|
||||
|
||||
QString serviceName = ui->service->currentText();
|
||||
obs_properties_t *props = obs_get_service_properties("rtmp_common");
|
||||
obs_property_t *services = obs_properties_get(props, "service");
|
||||
|
||||
OBSData settings = obs_data_create();
|
||||
obs_data_release(settings);
|
||||
|
||||
obs_data_set_string(settings, "service", QT_TO_UTF8(serviceName));
|
||||
obs_property_modified(services, settings);
|
||||
|
||||
const char *more_info_link =
|
||||
obs_data_get_string(settings, "more_info_link");
|
||||
|
||||
if (!more_info_link || (*more_info_link == '\0')) {
|
||||
ui->moreInfoButton->hide();
|
||||
} else {
|
||||
ui->moreInfoButton->setTargetUrl(QUrl(more_info_link));
|
||||
ui->moreInfoButton->show();
|
||||
}
|
||||
obs_properties_destroy(props);
|
||||
}
|
||||
|
||||
void OBSBasicSettings::UpdateKeyLink()
|
||||
{
|
||||
QString serviceName = ui->service->currentText();
|
||||
|
@ -220,7 +252,7 @@ void OBSBasicSettings::UpdateKeyLink()
|
|||
if (serviceName == "Twitch") {
|
||||
streamKeyLink =
|
||||
"https://www.twitch.tv/broadcast/dashboard/streamkey";
|
||||
} else if (serviceName == "YouTube / YouTube Gaming") {
|
||||
} else if (serviceName.startsWith("YouTube")) {
|
||||
streamKeyLink = "https://www.youtube.com/live_dashboard";
|
||||
} else if (serviceName.startsWith("Restream.io")) {
|
||||
streamKeyLink =
|
||||
|
|
|
@ -239,6 +239,7 @@ private:
|
|||
private slots:
|
||||
void UpdateServerList();
|
||||
void UpdateKeyLink();
|
||||
void UpdateMoreInfoLink();
|
||||
void on_show_clicked();
|
||||
void on_authPwShow_clicked();
|
||||
void on_connectAccount_clicked();
|
||||
|
|
|
@ -20,8 +20,9 @@ set(obs-ffmpeg_config_HEADERS
|
|||
"${CMAKE_CURRENT_BINARY_DIR}/obs-ffmpeg-config.h")
|
||||
|
||||
set(obs-ffmpeg_HEADERS
|
||||
obs-ffmpeg-compat.h
|
||||
obs-ffmpeg-formats.h
|
||||
obs-ffmpeg-compat.h)
|
||||
obs-ffmpeg-mux.h)
|
||||
|
||||
set(obs-ffmpeg_SOURCES
|
||||
obs-ffmpeg.c
|
||||
|
@ -29,6 +30,7 @@ set(obs-ffmpeg_SOURCES
|
|||
obs-ffmpeg-nvenc.c
|
||||
obs-ffmpeg-output.c
|
||||
obs-ffmpeg-mux.c
|
||||
obs-ffmpeg-hls-mux.c
|
||||
obs-ffmpeg-source.c)
|
||||
|
||||
if(UNIX AND NOT APPLE)
|
||||
|
|
|
@ -15,6 +15,7 @@ add_executable(obs-ffmpeg-mux
|
|||
${obs-ffmpeg-mux_HEADERS})
|
||||
|
||||
target_link_libraries(obs-ffmpeg-mux
|
||||
libobs
|
||||
${FFMPEG_LIBRARIES})
|
||||
|
||||
set_target_properties(obs-ffmpeg-mux PROPERTIES FOLDER "plugins/obs-ffmpeg")
|
||||
|
|
|
@ -22,13 +22,17 @@
|
|||
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "ffmpeg-mux.h"
|
||||
|
||||
#include <util/dstr.h>
|
||||
#include <libavformat/avformat.h>
|
||||
|
||||
#define ANSI_COLOR_RED "\x1b[0;91m"
|
||||
#define ANSI_COLOR_MAGENTA "\x1b[0;95m"
|
||||
#define ANSI_COLOR_RESET "\x1b[0m"
|
||||
|
||||
#if LIBAVCODEC_VERSION_MAJOR >= 58
|
||||
#define CODEC_FLAG_GLOBAL_H AV_CODEC_FLAG_GLOBAL_HEADER
|
||||
#else
|
||||
|
@ -37,6 +41,8 @@
|
|||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
static char *global_stream_key = "";
|
||||
|
||||
struct resize_buf {
|
||||
uint8_t *buf;
|
||||
size_t size;
|
||||
|
@ -70,6 +76,8 @@ static inline void resize_buf_free(struct resize_buf *rb)
|
|||
|
||||
struct main_params {
|
||||
char *file;
|
||||
/* printable_file is file with any stream key information removed */
|
||||
struct dstr printable_file;
|
||||
int has_video;
|
||||
int tracks;
|
||||
char *vcodec;
|
||||
|
@ -172,6 +180,8 @@ static void ffmpeg_mux_free(struct ffmpeg_mux *ffm)
|
|||
free(ffm->audio);
|
||||
}
|
||||
|
||||
dstr_free(&ffm->params.printable_file);
|
||||
|
||||
memset(ffm, 0, sizeof(*ffm));
|
||||
}
|
||||
|
||||
|
@ -218,6 +228,38 @@ static bool get_audio_params(struct audio_params *audio, int *argc,
|
|||
return true;
|
||||
}
|
||||
|
||||
static void ffmpeg_log_callback(void *param, int level, const char *format,
|
||||
va_list args)
|
||||
{
|
||||
char out_buffer[4096];
|
||||
struct dstr out = {0};
|
||||
|
||||
vsnprintf(out_buffer, sizeof(out_buffer), format, args);
|
||||
dstr_copy(&out, out_buffer);
|
||||
dstr_replace(&out, global_stream_key, "{stream_key}");
|
||||
|
||||
switch (level) {
|
||||
case AV_LOG_INFO:
|
||||
fprintf(stdout, "info: [ffmpeg_muxer] %s", out.array);
|
||||
fflush(stdout);
|
||||
break;
|
||||
|
||||
case AV_LOG_WARNING:
|
||||
fprintf(stdout, "%swarning: [ffmpeg_muxer] %s%s",
|
||||
ANSI_COLOR_MAGENTA, out.array, ANSI_COLOR_RESET);
|
||||
fflush(stdout);
|
||||
break;
|
||||
|
||||
case AV_LOG_ERROR:
|
||||
fprintf(stderr, "%serror: [ffmpeg_muxer] %s%s", ANSI_COLOR_RED,
|
||||
out.array, ANSI_COLOR_RESET);
|
||||
fflush(stderr);
|
||||
}
|
||||
|
||||
dstr_free(&out);
|
||||
UNUSED_PARAMETER(param);
|
||||
}
|
||||
|
||||
static bool init_params(int *argc, char ***argv, struct main_params *params,
|
||||
struct audio_params **p_audio)
|
||||
{
|
||||
|
@ -287,6 +329,18 @@ static bool init_params(int *argc, char ***argv, struct main_params *params,
|
|||
|
||||
*p_audio = audio;
|
||||
|
||||
dstr_copy(¶ms->printable_file, params->file);
|
||||
|
||||
get_opt_str(argc, argv, &global_stream_key, "stream key");
|
||||
if (strcmp(global_stream_key, "") != 0) {
|
||||
dstr_replace(¶ms->printable_file, global_stream_key,
|
||||
"{stream_key}");
|
||||
}
|
||||
|
||||
#ifdef DEBUG_FFMPEG
|
||||
av_log_set_callback(ffmpeg_log_callback);
|
||||
#endif
|
||||
|
||||
get_opt_str(argc, argv, ¶ms->muxer_settings, "muxer settings");
|
||||
|
||||
return true;
|
||||
|
@ -353,6 +407,10 @@ static void create_video_stream(struct ffmpeg_mux *ffm)
|
|||
(AVRational){ffm->params.fps_den, ffm->params.fps_num};
|
||||
|
||||
ffm->video_stream->time_base = context->time_base;
|
||||
#if LIBAVFORMAT_VERSION_MAJOR < 59
|
||||
// codec->time_base may still be used if LIBAVFORMAT_VERSION_MAJOR < 59
|
||||
ffm->video_stream->codec->time_base = context->time_base;
|
||||
#endif
|
||||
ffm->video_stream->avg_frame_rate = av_inv_q(context->time_base);
|
||||
|
||||
if (ffm->output->oformat->flags & AVFMT_GLOBALHEADER)
|
||||
|
@ -521,7 +579,8 @@ static inline int open_output_file(struct ffmpeg_mux *ffm)
|
|||
AVIO_FLAG_WRITE);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Couldn't open '%s', %s\n",
|
||||
ffm->params.file, av_err2str(ret));
|
||||
ffm->params.printable_file.array,
|
||||
av_err2str(ret));
|
||||
return FFM_ERROR;
|
||||
}
|
||||
}
|
||||
|
@ -548,8 +607,8 @@ static inline int open_output_file(struct ffmpeg_mux *ffm)
|
|||
|
||||
ret = avformat_write_header(ffm->output, &dict);
|
||||
if (ret < 0) {
|
||||
fprintf(stderr, "Error opening '%s': %s\n", ffm->params.file,
|
||||
av_err2str(ret));
|
||||
fprintf(stderr, "Error opening '%s': %s",
|
||||
ffm->params.printable_file.array, av_err2str(ret));
|
||||
|
||||
av_dict_free(&dict);
|
||||
|
||||
|
@ -564,29 +623,38 @@ static inline int open_output_file(struct ffmpeg_mux *ffm)
|
|||
#define SRT_PROTO "srt"
|
||||
#define UDP_PROTO "udp"
|
||||
#define TCP_PROTO "tcp"
|
||||
#define HTTP_PROTO "http"
|
||||
|
||||
static int ffmpeg_mux_init_context(struct ffmpeg_mux *ffm)
|
||||
{
|
||||
AVOutputFormat *output_format;
|
||||
int ret;
|
||||
bool isNetwork = false;
|
||||
bool is_network = false;
|
||||
bool is_http = false;
|
||||
is_http = (strncmp(ffm->params.file, HTTP_PROTO,
|
||||
sizeof(HTTP_PROTO) - 1) == 0);
|
||||
|
||||
if (strncmp(ffm->params.file, SRT_PROTO, sizeof(SRT_PROTO) - 1) == 0 ||
|
||||
strncmp(ffm->params.file, UDP_PROTO, sizeof(UDP_PROTO) - 1) == 0 ||
|
||||
strncmp(ffm->params.file, TCP_PROTO, sizeof(TCP_PROTO) - 1) == 0)
|
||||
isNetwork = true;
|
||||
|
||||
if (isNetwork) {
|
||||
strncmp(ffm->params.file, TCP_PROTO, sizeof(TCP_PROTO) - 1) == 0 ||
|
||||
is_http) {
|
||||
is_network = true;
|
||||
avformat_network_init();
|
||||
output_format = av_guess_format("mpegts", NULL, "video/M2PT");
|
||||
} else {
|
||||
output_format = av_guess_format(NULL, ffm->params.file, NULL);
|
||||
}
|
||||
|
||||
if (is_network && !is_http)
|
||||
output_format = av_guess_format("mpegts", NULL, "video/M2PT");
|
||||
else
|
||||
output_format = av_guess_format(NULL, ffm->params.file, NULL);
|
||||
|
||||
if (output_format == NULL) {
|
||||
fprintf(stderr, "Couldn't find an appropriate muxer for '%s'\n",
|
||||
ffm->params.file);
|
||||
ffm->params.printable_file.array);
|
||||
return FFM_ERROR;
|
||||
}
|
||||
printf("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");
|
||||
|
||||
ret = avformat_alloc_output_context2(&ffm->output, output_format, NULL,
|
||||
ffm->params.file);
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum ffm_packet_type {
|
||||
|
|
|
@ -0,0 +1,332 @@
|
|||
#include "obs-ffmpeg-mux.h"
|
||||
|
||||
#define do_log(level, format, ...) \
|
||||
blog(level, "[ffmpeg hls 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__)
|
||||
|
||||
const char *ffmpeg_hls_mux_getname(void *type)
|
||||
{
|
||||
UNUSED_PARAMETER(type);
|
||||
return obs_module_text("FFmpegHlsMuxer");
|
||||
}
|
||||
|
||||
int hls_stream_dropped_frames(void *data)
|
||||
{
|
||||
struct ffmpeg_muxer *stream = data;
|
||||
return stream->dropped_frames;
|
||||
}
|
||||
|
||||
void ffmpeg_hls_mux_destroy(void *data)
|
||||
{
|
||||
struct ffmpeg_muxer *stream = data;
|
||||
|
||||
if (stream) {
|
||||
deactivate(stream, 0);
|
||||
|
||||
pthread_mutex_destroy(&stream->write_mutex);
|
||||
os_sem_destroy(stream->write_sem);
|
||||
os_event_destroy(stream->stop_event);
|
||||
|
||||
da_free(stream->mux_packets);
|
||||
circlebuf_free(&stream->packets);
|
||||
|
||||
os_process_pipe_destroy(stream->pipe);
|
||||
dstr_free(&stream->path);
|
||||
dstr_free(&stream->printable_path);
|
||||
dstr_free(&stream->stream_key);
|
||||
dstr_free(&stream->muxer_settings);
|
||||
bfree(data);
|
||||
}
|
||||
}
|
||||
|
||||
void *ffmpeg_hls_mux_create(obs_data_t *settings, obs_output_t *output)
|
||||
{
|
||||
struct ffmpeg_muxer *stream = bzalloc(sizeof(*stream));
|
||||
pthread_mutex_init_value(&stream->write_mutex);
|
||||
stream->output = output;
|
||||
|
||||
/* init mutex, semaphore and event */
|
||||
if (pthread_mutex_init(&stream->write_mutex, NULL) != 0)
|
||||
goto fail;
|
||||
if (os_event_init(&stream->stop_event, OS_EVENT_TYPE_AUTO) != 0)
|
||||
goto fail;
|
||||
if (os_sem_init(&stream->write_sem, 0) != 0)
|
||||
goto fail;
|
||||
|
||||
UNUSED_PARAMETER(settings);
|
||||
return stream;
|
||||
|
||||
fail:
|
||||
ffmpeg_hls_mux_destroy(stream);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool process_packet(struct ffmpeg_muxer *stream)
|
||||
{
|
||||
struct encoder_packet packet;
|
||||
bool has_packet = false;
|
||||
bool ret = true;
|
||||
|
||||
pthread_mutex_lock(&stream->write_mutex);
|
||||
|
||||
if (stream->packets.size) {
|
||||
circlebuf_pop_front(&stream->packets, &packet, sizeof(packet));
|
||||
has_packet = true;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&stream->write_mutex);
|
||||
|
||||
if (has_packet) {
|
||||
ret = write_packet(stream, &packet);
|
||||
obs_encoder_packet_release(&packet);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void *write_thread(void *data)
|
||||
{
|
||||
struct ffmpeg_muxer *stream = data;
|
||||
|
||||
while (os_sem_wait(stream->write_sem) == 0) {
|
||||
if (os_event_try(stream->stop_event) == 0)
|
||||
return NULL;
|
||||
|
||||
if (!process_packet(stream))
|
||||
break;
|
||||
}
|
||||
|
||||
obs_output_signal_stop(stream->output, OBS_OUTPUT_ERROR);
|
||||
deactivate(stream, 0);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool ffmpeg_hls_mux_start(void *data)
|
||||
{
|
||||
struct ffmpeg_muxer *stream = data;
|
||||
obs_service_t *service;
|
||||
const char *path_str;
|
||||
const char *stream_key;
|
||||
struct dstr path = {0};
|
||||
obs_encoder_t *vencoder;
|
||||
obs_data_t *settings;
|
||||
int keyint_sec;
|
||||
|
||||
if (!obs_output_can_begin_data_capture(stream->output, 0))
|
||||
return false;
|
||||
if (!obs_output_initialize_encoders(stream->output, 0))
|
||||
return false;
|
||||
|
||||
service = obs_output_get_service(stream->output);
|
||||
if (!service)
|
||||
return false;
|
||||
path_str = obs_service_get_url(service);
|
||||
stream_key = obs_service_get_key(service);
|
||||
dstr_copy(&stream->stream_key, stream_key);
|
||||
dstr_copy(&path, path_str);
|
||||
dstr_replace(&path, "{stream_key}", stream_key);
|
||||
dstr_copy(&stream->muxer_settings,
|
||||
"method=PUT http_persistent=1 ignore_io_errors=1 ");
|
||||
dstr_catf(&stream->muxer_settings, "http_user_agent=libobs/%s",
|
||||
OBS_VERSION);
|
||||
|
||||
vencoder = obs_output_get_video_encoder(stream->output);
|
||||
settings = obs_encoder_get_settings(vencoder);
|
||||
keyint_sec = obs_data_get_int(settings, "keyint_sec");
|
||||
if (keyint_sec) {
|
||||
dstr_catf(&stream->muxer_settings, " hls_time=%d", keyint_sec);
|
||||
stream->keyint_sec = keyint_sec;
|
||||
}
|
||||
|
||||
obs_data_release(settings);
|
||||
|
||||
start_pipe(stream, path.array);
|
||||
dstr_free(&path);
|
||||
|
||||
if (!stream->pipe) {
|
||||
obs_output_set_last_error(
|
||||
stream->output, obs_module_text("HelperProcessFailed"));
|
||||
warn("Failed to create process pipe");
|
||||
return false;
|
||||
}
|
||||
stream->mux_thread_joinable = pthread_create(&stream->mux_thread, NULL,
|
||||
write_thread, stream) == 0;
|
||||
if (!stream->mux_thread_joinable)
|
||||
return false;
|
||||
|
||||
/* write headers and start capture */
|
||||
os_atomic_set_bool(&stream->active, true);
|
||||
os_atomic_set_bool(&stream->capturing, true);
|
||||
stream->is_hls = true;
|
||||
stream->total_bytes = 0;
|
||||
stream->dropped_frames = 0;
|
||||
stream->min_priority = 0;
|
||||
|
||||
obs_output_begin_data_capture(stream->output, 0);
|
||||
|
||||
dstr_copy(&stream->printable_path, path_str);
|
||||
info("Writing to path '%s'...", stream->printable_path.array);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool write_packet_to_buf(struct ffmpeg_muxer *stream,
|
||||
struct encoder_packet *packet)
|
||||
{
|
||||
circlebuf_push_back(&stream->packets, packet,
|
||||
sizeof(struct encoder_packet));
|
||||
return true;
|
||||
}
|
||||
|
||||
static void drop_frames(struct ffmpeg_muxer *stream, int highest_priority)
|
||||
{
|
||||
struct circlebuf new_buf = {0};
|
||||
int num_frames_dropped = 0;
|
||||
|
||||
circlebuf_reserve(&new_buf, sizeof(struct encoder_packet) * 8);
|
||||
|
||||
while (stream->packets.size) {
|
||||
struct encoder_packet packet;
|
||||
circlebuf_pop_front(&stream->packets, &packet, sizeof(packet));
|
||||
|
||||
/* do not drop audio data or video keyframes */
|
||||
if (packet.type == OBS_ENCODER_AUDIO ||
|
||||
packet.drop_priority >= highest_priority) {
|
||||
circlebuf_push_back(&new_buf, &packet, sizeof(packet));
|
||||
} else {
|
||||
num_frames_dropped++;
|
||||
obs_encoder_packet_release(&packet);
|
||||
}
|
||||
}
|
||||
|
||||
circlebuf_free(&stream->packets);
|
||||
stream->packets = new_buf;
|
||||
|
||||
if (stream->min_priority < highest_priority)
|
||||
stream->min_priority = highest_priority;
|
||||
|
||||
stream->dropped_frames += num_frames_dropped;
|
||||
}
|
||||
|
||||
static bool find_first_video_packet(struct ffmpeg_muxer *stream,
|
||||
struct encoder_packet *first)
|
||||
{
|
||||
size_t count = stream->packets.size / sizeof(*first);
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
struct encoder_packet *cur =
|
||||
circlebuf_data(&stream->packets, i * sizeof(*first));
|
||||
if (cur->type == OBS_ENCODER_VIDEO && !cur->keyframe) {
|
||||
*first = *cur;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void check_to_drop_frames(struct ffmpeg_muxer *stream, bool pframes)
|
||||
{
|
||||
struct encoder_packet first;
|
||||
int64_t buffer_duration_usec;
|
||||
int priority = pframes ? OBS_NAL_PRIORITY_HIGHEST
|
||||
: OBS_NAL_PRIORITY_HIGH;
|
||||
int keyint_sec = stream->keyint_sec;
|
||||
int64_t drop_threshold_sec = keyint_sec ? 2 * keyint_sec : 10;
|
||||
|
||||
if (!find_first_video_packet(stream, &first))
|
||||
return;
|
||||
|
||||
buffer_duration_usec = stream->last_dts_usec - first.dts_usec;
|
||||
|
||||
if (buffer_duration_usec > drop_threshold_sec * 1000000)
|
||||
drop_frames(stream, priority);
|
||||
}
|
||||
|
||||
static bool add_video_packet(struct ffmpeg_muxer *stream,
|
||||
struct encoder_packet *packet)
|
||||
{
|
||||
check_to_drop_frames(stream, false);
|
||||
check_to_drop_frames(stream, true);
|
||||
|
||||
/* if currently dropping frames, drop packets until it reaches the
|
||||
* desired priority */
|
||||
if (packet->drop_priority < stream->min_priority) {
|
||||
stream->dropped_frames++;
|
||||
return false;
|
||||
} else {
|
||||
stream->min_priority = 0;
|
||||
}
|
||||
|
||||
stream->last_dts_usec = packet->dts_usec;
|
||||
return write_packet_to_buf(stream, packet);
|
||||
}
|
||||
|
||||
void ffmpeg_hls_mux_data(void *data, struct encoder_packet *packet)
|
||||
{
|
||||
struct ffmpeg_muxer *stream = data;
|
||||
struct encoder_packet new_packet;
|
||||
struct encoder_packet tmp_packet;
|
||||
bool added_packet = false;
|
||||
|
||||
if (!active(stream))
|
||||
return;
|
||||
|
||||
/* encoder failure */
|
||||
if (!packet) {
|
||||
deactivate(stream, OBS_OUTPUT_ENCODE_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stream->sent_headers) {
|
||||
if (!send_headers(stream))
|
||||
return;
|
||||
stream->sent_headers = true;
|
||||
}
|
||||
|
||||
if (stopping(stream)) {
|
||||
if (packet->sys_dts_usec >= stream->stop_ts) {
|
||||
deactivate(stream, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (packet->type == OBS_ENCODER_VIDEO) {
|
||||
obs_parse_avc_packet(&tmp_packet, packet);
|
||||
packet->drop_priority = tmp_packet.priority;
|
||||
obs_encoder_packet_release(&tmp_packet);
|
||||
}
|
||||
obs_encoder_packet_ref(&new_packet, packet);
|
||||
|
||||
pthread_mutex_lock(&stream->write_mutex);
|
||||
|
||||
if (active(stream)) {
|
||||
added_packet =
|
||||
(packet->type == OBS_ENCODER_VIDEO)
|
||||
? add_video_packet(stream, &new_packet)
|
||||
: write_packet_to_buf(stream, &new_packet);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&stream->write_mutex);
|
||||
|
||||
if (added_packet)
|
||||
os_sem_post(stream->write_sem);
|
||||
else
|
||||
obs_encoder_packet_release(&new_packet);
|
||||
}
|
||||
|
||||
struct obs_output_info ffmpeg_hls_muxer = {
|
||||
.id = "ffmpeg_hls_muxer",
|
||||
.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK |
|
||||
OBS_OUTPUT_SERVICE,
|
||||
.encoded_video_codecs = "h264",
|
||||
.encoded_audio_codecs = "aac",
|
||||
.get_name = ffmpeg_hls_mux_getname,
|
||||
.create = ffmpeg_hls_mux_create,
|
||||
.destroy = ffmpeg_hls_mux_destroy,
|
||||
.start = ffmpeg_hls_mux_start,
|
||||
.stop = ffmpeg_mux_stop,
|
||||
.encoded_packet = ffmpeg_hls_mux_data,
|
||||
.get_total_bytes = ffmpeg_mux_total_bytes,
|
||||
.get_dropped_frames = hls_stream_dropped_frames,
|
||||
};
|
|
@ -14,17 +14,8 @@
|
|||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
******************************************************************************/
|
||||
|
||||
#include <obs-module.h>
|
||||
#include <obs-hotkey.h>
|
||||
#include <obs-avc.h>
|
||||
#include <util/dstr.h>
|
||||
#include <util/pipe.h>
|
||||
#include <util/darray.h>
|
||||
#include <util/platform.h>
|
||||
#include <util/circlebuf.h>
|
||||
#include <util/threading.h>
|
||||
#include "ffmpeg-mux/ffmpeg-mux.h"
|
||||
#include "obs-ffmpeg-mux.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "util/windows/win-version.h"
|
||||
|
@ -39,35 +30,6 @@
|
|||
#define warn(format, ...) do_log(LOG_WARNING, format, ##__VA_ARGS__)
|
||||
#define info(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__)
|
||||
|
||||
struct ffmpeg_muxer {
|
||||
obs_output_t *output;
|
||||
os_process_pipe_t *pipe;
|
||||
int64_t stop_ts;
|
||||
uint64_t total_bytes;
|
||||
struct dstr path;
|
||||
bool sent_headers;
|
||||
volatile bool active;
|
||||
volatile bool stopping;
|
||||
volatile bool capturing;
|
||||
|
||||
/* replay buffer */
|
||||
struct circlebuf packets;
|
||||
int64_t cur_size;
|
||||
int64_t cur_time;
|
||||
int64_t max_size;
|
||||
int64_t max_time;
|
||||
int64_t save_ts;
|
||||
int keyframes;
|
||||
obs_hotkey_id hotkey;
|
||||
|
||||
DARRAY(struct encoder_packet) mux_packets;
|
||||
pthread_t mux_thread;
|
||||
bool mux_thread_joinable;
|
||||
volatile bool muxing;
|
||||
|
||||
bool is_network;
|
||||
};
|
||||
|
||||
static const char *ffmpeg_mux_getname(void *type)
|
||||
{
|
||||
UNUSED_PARAMETER(type);
|
||||
|
@ -105,9 +67,13 @@ static void ffmpeg_mux_destroy(void *data)
|
|||
if (stream->mux_thread_joinable)
|
||||
pthread_join(stream->mux_thread, NULL);
|
||||
da_free(stream->mux_packets);
|
||||
circlebuf_free(&stream->packets);
|
||||
|
||||
os_process_pipe_destroy(stream->pipe);
|
||||
dstr_free(&stream->path);
|
||||
dstr_free(&stream->printable_path);
|
||||
dstr_free(&stream->stream_key);
|
||||
dstr_free(&stream->muxer_settings);
|
||||
bfree(stream);
|
||||
}
|
||||
|
||||
|
@ -134,12 +100,12 @@ static inline bool capturing(struct ffmpeg_muxer *stream)
|
|||
return os_atomic_load_bool(&stream->capturing);
|
||||
}
|
||||
|
||||
static inline bool stopping(struct ffmpeg_muxer *stream)
|
||||
bool stopping(struct ffmpeg_muxer *stream)
|
||||
{
|
||||
return os_atomic_load_bool(&stream->stopping);
|
||||
}
|
||||
|
||||
static inline bool active(struct ffmpeg_muxer *stream)
|
||||
bool active(struct ffmpeg_muxer *stream)
|
||||
{
|
||||
return os_atomic_load_bool(&stream->active);
|
||||
}
|
||||
|
@ -236,17 +202,30 @@ static void log_muxer_params(struct ffmpeg_muxer *stream, const char *settings)
|
|||
av_dict_free(&dict);
|
||||
}
|
||||
|
||||
static void add_stream_key(struct dstr *cmd, struct ffmpeg_muxer *stream)
|
||||
{
|
||||
dstr_catf(cmd, "\"%s\" ",
|
||||
dstr_is_empty(&stream->stream_key)
|
||||
? ""
|
||||
: stream->stream_key.array);
|
||||
}
|
||||
|
||||
static void add_muxer_params(struct dstr *cmd, struct ffmpeg_muxer *stream)
|
||||
{
|
||||
obs_data_t *settings = obs_output_get_settings(stream->output);
|
||||
struct dstr mux = {0};
|
||||
|
||||
dstr_copy(&mux, obs_data_get_string(settings, "muxer_settings"));
|
||||
if (dstr_is_empty(&stream->muxer_settings)) {
|
||||
obs_data_t *settings = obs_output_get_settings(stream->output);
|
||||
dstr_copy(&mux,
|
||||
obs_data_get_string(settings, "muxer_settings"));
|
||||
obs_data_release(settings);
|
||||
} else {
|
||||
dstr_copy(&mux, stream->muxer_settings.array);
|
||||
}
|
||||
|
||||
log_muxer_params(stream, mux.array);
|
||||
|
||||
dstr_replace(&mux, "\"", "\\\"");
|
||||
obs_data_release(settings);
|
||||
|
||||
dstr_catf(cmd, "\"%s\" ", mux.array ? mux.array : "");
|
||||
|
||||
|
@ -291,10 +270,11 @@ static void build_command_line(struct ffmpeg_muxer *stream, struct dstr *cmd,
|
|||
}
|
||||
}
|
||||
|
||||
add_stream_key(cmd, stream);
|
||||
add_muxer_params(cmd, stream);
|
||||
}
|
||||
|
||||
static inline void start_pipe(struct ffmpeg_muxer *stream, const char *path)
|
||||
void start_pipe(struct ffmpeg_muxer *stream, const char *path)
|
||||
{
|
||||
struct dstr cmd;
|
||||
build_command_line(stream, &cmd, path);
|
||||
|
@ -381,10 +361,19 @@ static bool ffmpeg_mux_start(void *data)
|
|||
return true;
|
||||
}
|
||||
|
||||
static int deactivate(struct ffmpeg_muxer *stream, int code)
|
||||
int deactivate(struct ffmpeg_muxer *stream, int code)
|
||||
{
|
||||
int ret = -1;
|
||||
|
||||
if (stream->is_hls) {
|
||||
if (stream->mux_thread_joinable) {
|
||||
os_event_signal(stream->stop_event);
|
||||
os_sem_post(stream->write_sem);
|
||||
pthread_join(stream->mux_thread, NULL);
|
||||
stream->mux_thread_joinable = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (active(stream)) {
|
||||
ret = os_process_pipe_destroy(stream->pipe);
|
||||
stream->pipe = NULL;
|
||||
|
@ -392,7 +381,10 @@ static int deactivate(struct ffmpeg_muxer *stream, int code)
|
|||
os_atomic_set_bool(&stream->active, false);
|
||||
os_atomic_set_bool(&stream->sent_headers, false);
|
||||
|
||||
info("Output of file '%s' stopped", stream->path.array);
|
||||
info("Output of file '%s' stopped",
|
||||
dstr_is_empty(&stream->printable_path)
|
||||
? stream->path.array
|
||||
: stream->printable_path.array);
|
||||
}
|
||||
|
||||
if (code) {
|
||||
|
@ -401,11 +393,24 @@ static int deactivate(struct ffmpeg_muxer *stream, int code)
|
|||
obs_output_end_data_capture(stream->output);
|
||||
}
|
||||
|
||||
if (stream->is_hls) {
|
||||
pthread_mutex_lock(&stream->write_mutex);
|
||||
|
||||
while (stream->packets.size) {
|
||||
struct encoder_packet packet;
|
||||
circlebuf_pop_front(&stream->packets, &packet,
|
||||
sizeof(packet));
|
||||
obs_encoder_packet_release(&packet);
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&stream->write_mutex);
|
||||
}
|
||||
|
||||
os_atomic_set_bool(&stream->stopping, false);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void ffmpeg_mux_stop(void *data, uint64_t ts)
|
||||
void ffmpeg_mux_stop(void *data, uint64_t ts)
|
||||
{
|
||||
struct ffmpeg_muxer *stream = data;
|
||||
|
||||
|
@ -451,8 +456,7 @@ static void signal_failure(struct ffmpeg_muxer *stream)
|
|||
os_atomic_set_bool(&stream->capturing, false);
|
||||
}
|
||||
|
||||
static bool write_packet(struct ffmpeg_muxer *stream,
|
||||
struct encoder_packet *packet)
|
||||
bool write_packet(struct ffmpeg_muxer *stream, struct encoder_packet *packet)
|
||||
{
|
||||
bool is_video = packet->type == OBS_ENCODER_VIDEO;
|
||||
size_t ret;
|
||||
|
@ -505,7 +509,7 @@ static bool send_video_headers(struct ffmpeg_muxer *stream)
|
|||
return write_packet(stream, &packet);
|
||||
}
|
||||
|
||||
static bool send_headers(struct ffmpeg_muxer *stream)
|
||||
bool send_headers(struct ffmpeg_muxer *stream)
|
||||
{
|
||||
obs_encoder_t *aencoder;
|
||||
size_t idx = 0;
|
||||
|
@ -567,7 +571,7 @@ static obs_properties_t *ffmpeg_mux_properties(void *unused)
|
|||
return props;
|
||||
}
|
||||
|
||||
static uint64_t ffmpeg_mux_total_bytes(void *data)
|
||||
uint64_t ffmpeg_mux_total_bytes(void *data)
|
||||
{
|
||||
struct ffmpeg_muxer *stream = data;
|
||||
return stream->total_bytes;
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
#pragma once
|
||||
|
||||
#include <obs-avc.h>
|
||||
#include <obs-module.h>
|
||||
#include <obs-hotkey.h>
|
||||
#include <util/circlebuf.h>
|
||||
#include <util/darray.h>
|
||||
#include <util/dstr.h>
|
||||
#include <util/pipe.h>
|
||||
#include <util/platform.h>
|
||||
#include <util/threading.h>
|
||||
|
||||
struct ffmpeg_muxer {
|
||||
obs_output_t *output;
|
||||
os_process_pipe_t *pipe;
|
||||
int64_t stop_ts;
|
||||
uint64_t total_bytes;
|
||||
bool sent_headers;
|
||||
volatile bool active;
|
||||
volatile bool capturing;
|
||||
volatile bool stopping;
|
||||
struct dstr path;
|
||||
struct dstr printable_path;
|
||||
struct dstr muxer_settings;
|
||||
struct dstr stream_key;
|
||||
|
||||
/* replay buffer */
|
||||
int64_t cur_size;
|
||||
int64_t cur_time;
|
||||
int64_t max_size;
|
||||
int64_t max_time;
|
||||
int64_t save_ts;
|
||||
int keyframes;
|
||||
obs_hotkey_id hotkey;
|
||||
volatile bool muxing;
|
||||
DARRAY(struct encoder_packet) mux_packets;
|
||||
|
||||
/* these are accessed both by replay buffer and by HLS */
|
||||
pthread_t mux_thread;
|
||||
bool mux_thread_joinable;
|
||||
struct circlebuf packets;
|
||||
|
||||
/* HLS only */
|
||||
int keyint_sec;
|
||||
pthread_mutex_t write_mutex;
|
||||
os_sem_t *write_sem;
|
||||
os_event_t *stop_event;
|
||||
bool is_hls;
|
||||
int dropped_frames;
|
||||
int min_priority;
|
||||
int64_t last_dts_usec;
|
||||
|
||||
bool is_network;
|
||||
};
|
||||
|
||||
bool stopping(struct ffmpeg_muxer *stream);
|
||||
bool active(struct ffmpeg_muxer *stream);
|
||||
void start_pipe(struct ffmpeg_muxer *stream, const char *path);
|
||||
bool write_packet(struct ffmpeg_muxer *stream, struct encoder_packet *packet);
|
||||
bool send_headers(struct ffmpeg_muxer *stream);
|
||||
int deactivate(struct ffmpeg_muxer *stream, int code);
|
||||
void ffmpeg_mux_stop(void *data, uint64_t ts);
|
||||
uint64_t ffmpeg_mux_total_bytes(void *data);
|
|
@ -24,6 +24,7 @@ extern struct obs_output_info ffmpeg_output;
|
|||
extern struct obs_output_info ffmpeg_muxer;
|
||||
extern struct obs_output_info ffmpeg_mpegts_muxer;
|
||||
extern struct obs_output_info replay_buffer;
|
||||
extern struct obs_output_info ffmpeg_hls_muxer;
|
||||
extern struct obs_encoder_info aac_encoder_info;
|
||||
extern struct obs_encoder_info opus_encoder_info;
|
||||
extern struct obs_encoder_info nvenc_encoder_info;
|
||||
|
@ -231,6 +232,7 @@ bool obs_module_load(void)
|
|||
obs_register_output(&ffmpeg_output);
|
||||
obs_register_output(&ffmpeg_muxer);
|
||||
obs_register_output(&ffmpeg_mpegts_muxer);
|
||||
obs_register_output(&ffmpeg_hls_muxer);
|
||||
obs_register_output(&replay_buffer);
|
||||
obs_register_encoder(&aac_encoder_info);
|
||||
obs_register_encoder(&opus_encoder_info);
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"url": "https://obsproject.com/obs2_update/rtmp-services",
|
||||
"version": 148,
|
||||
"version": 149,
|
||||
"files": [
|
||||
{
|
||||
"name": "services.json",
|
||||
"version": 148
|
||||
"version": 149
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"format_version": 2,
|
||||
"format_version": 3,
|
||||
"services": [
|
||||
{
|
||||
"name": "Twitch",
|
||||
|
@ -198,8 +198,32 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"name": "YouTube / YouTube Gaming",
|
||||
"name": "YouTube - HLS",
|
||||
"common": false,
|
||||
"more_info_link": "https://developers.google.com/youtube/v3/live/guides/ingestion-protocol-comparison",
|
||||
"servers": [
|
||||
{
|
||||
"name": "Primary YouTube ingest server",
|
||||
"url": "https://a.upload.youtube.com/http_upload_hls?cid={stream_key}©=0&file=out.m3u8"
|
||||
},
|
||||
{
|
||||
"name": "Backup YouTube ingest server",
|
||||
"url": "https://b.upload.youtube.com/http_upload_hls?cid={stream_key}©=1&file=out.m3u8"
|
||||
}
|
||||
],
|
||||
"recommended": {
|
||||
"keyint": 2,
|
||||
"output": "ffmpeg_hls_muxer",
|
||||
"max video bitrate": 51000,
|
||||
"max audio bitrate": 160
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "YouTube - RTMP",
|
||||
"common": true,
|
||||
"alt_names": [
|
||||
"YouTube / YouTube Gaming"
|
||||
],
|
||||
"servers": [
|
||||
{
|
||||
"name": "Primary YouTube ingest server",
|
||||
|
|
|
@ -370,6 +370,15 @@ static void fill_servers(obs_property_t *servers_prop, json_t *service,
|
|||
}
|
||||
}
|
||||
|
||||
static void fill_more_info_link(json_t *service, obs_data_t *settings)
|
||||
{
|
||||
const char *more_info_link;
|
||||
|
||||
more_info_link = get_string_val(service, "more_info_link");
|
||||
if (more_info_link)
|
||||
obs_data_set_string(settings, "more_info_link", more_info_link);
|
||||
}
|
||||
|
||||
static inline json_t *find_service(json_t *root, const char *name,
|
||||
const char **p_new_name)
|
||||
{
|
||||
|
@ -432,7 +441,7 @@ static bool service_selected(obs_properties_t *props, obs_property_t *p,
|
|||
}
|
||||
|
||||
fill_servers(obs_properties_get(props, "server"), service, name);
|
||||
|
||||
fill_more_info_link(service, settings);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
#define RTMP_SERVICES_FORMAT_VERSION 2
|
||||
#define RTMP_SERVICES_FORMAT_VERSION 3
|
||||
|
|
Loading…
Reference in New Issue