Merge pull request #3114 from ushadow/add-youtube-hls-streaming

Add HLS streaming output
master
Jim 2020-10-13 14:52:30 -07:00 committed by GitHub
commit 19cb3b9463
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 708 additions and 79 deletions

View File

@ -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..."

View File

@ -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">

View File

@ -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">

View File

@ -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) {

View File

@ -195,6 +195,7 @@ public slots:
void on_useStreamKey_clicked();
void ServiceChanged();
void UpdateKeyLink();
void UpdateMoreInfoLink();
void UpdateServerList();
void UpdateCompleted();
};

View File

@ -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 =

View File

@ -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();

View File

@ -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)

View File

@ -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")

View File

@ -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(&params->printable_file, params->file);
get_opt_str(argc, argv, &global_stream_key, "stream key");
if (strcmp(global_stream_key, "") != 0) {
dstr_replace(&params->printable_file, global_stream_key,
"{stream_key}");
}
#ifdef DEBUG_FFMPEG
av_log_set_callback(ffmpeg_log_callback);
#endif
get_opt_str(argc, argv, &params->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);

View File

@ -16,6 +16,7 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
enum ffm_packet_type {

View File

@ -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,
};

View File

@ -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;

View File

@ -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);

View File

@ -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);

View File

@ -1,10 +1,10 @@
{
"url": "https://obsproject.com/obs2_update/rtmp-services",
"version": 148,
"version": 149,
"files": [
{
"name": "services.json",
"version": 148
"version": 149
}
]
}

View File

@ -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}&copy=0&file=out.m3u8"
},
{
"name": "Backup YouTube ingest server",
"url": "https://b.upload.youtube.com/http_upload_hls?cid={stream_key}&copy=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",

View File

@ -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;
}

View File

@ -1,3 +1,3 @@
#pragma once
#define RTMP_SERVICES_FORMAT_VERSION 2
#define RTMP_SERVICES_FORMAT_VERSION 3