obs-studio/plugins/obs-outputs/flv-output.c
2024-05-24 16:22:16 -04:00

660 lines
18 KiB
C

/******************************************************************************
Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>
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 <http://www.gnu.org/licenses/>.
******************************************************************************/
#include <stdio.h>
#include <obs-module.h>
#include <obs-avc.h>
#ifdef ENABLE_HEVC
#include "rtmp-hevc.h"
#include <obs-hevc.h>
#endif
#include "rtmp-av1.h"
#include <util/platform.h>
#include <util/dstr.h>
#include <util/threading.h>
#include <inttypes.h>
#include "flv-mux.h"
#define do_log(level, format, ...) \
blog(level, "[flv output: '%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__)
struct flv_output {
obs_output_t *output;
struct dstr path;
FILE *file;
volatile bool active;
volatile bool stopping;
uint64_t stop_ts;
bool sent_headers;
int64_t last_packet_ts;
enum audio_id_t audio_codec[MAX_OUTPUT_AUDIO_ENCODERS];
enum video_id_t video_codec[MAX_OUTPUT_VIDEO_ENCODERS];
pthread_mutex_t mutex;
bool got_first_video;
int32_t start_dts_offset;
};
/* Adapted from FFmpeg's libavutil/pixfmt.h
*
* Renamed to make it apparent that these are not imported as this module does
* not use or link against FFmpeg.
*/
/* clang-format off */
/**
* Chromaticity Coordinates of the Source Primaries
* These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.1 and ITU-T H.273.
*/
enum OBSColorPrimaries {
OBSCOL_PRI_RESERVED0 = 0,
OBSCOL_PRI_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP 177 Annex B
OBSCOL_PRI_UNSPECIFIED = 2,
OBSCOL_PRI_RESERVED = 3,
OBSCOL_PRI_BT470M = 4, ///< also FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
OBSCOL_PRI_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM
OBSCOL_PRI_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
OBSCOL_PRI_SMPTE240M = 7, ///< identical to above, also called "SMPTE C" even though it uses D65
OBSCOL_PRI_FILM = 8, ///< colour filters using Illuminant C
OBSCOL_PRI_BT2020 = 9, ///< ITU-R BT2020
OBSCOL_PRI_SMPTE428 = 10, ///< SMPTE ST 428-1 (CIE 1931 XYZ)
OBSCOL_PRI_SMPTEST428_1 = OBSCOL_PRI_SMPTE428,
OBSCOL_PRI_SMPTE431 = 11, ///< SMPTE ST 431-2 (2011) / DCI P3
OBSCOL_PRI_SMPTE432 = 12, ///< SMPTE ST 432-1 (2010) / P3 D65 / Display P3
OBSCOL_PRI_EBU3213 = 22, ///< EBU Tech. 3213-E (nothing there) / one of JEDEC P22 group phosphors
OBSCOL_PRI_JEDEC_P22 = OBSCOL_PRI_EBU3213,
OBSCOL_PRI_NB ///< Not part of ABI
};
/**
* Color Transfer Characteristic
* These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.2.
*/
enum OBSColorTransferCharacteristic {
OBSCOL_TRC_RESERVED0 = 0,
OBSCOL_TRC_BT709 = 1, ///< also ITU-R BT1361
OBSCOL_TRC_UNSPECIFIED = 2,
OBSCOL_TRC_RESERVED = 3,
OBSCOL_TRC_GAMMA22 = 4, ///< also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM
OBSCOL_TRC_GAMMA28 = 5, ///< also ITU-R BT470BG
OBSCOL_TRC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 or 625 / ITU-R BT1358 525 or 625 / ITU-R BT1700 NTSC
OBSCOL_TRC_SMPTE240M = 7,
OBSCOL_TRC_LINEAR = 8, ///< "Linear transfer characteristics"
OBSCOL_TRC_LOG = 9, ///< "Logarithmic transfer characteristic (100:1 range)"
OBSCOL_TRC_LOG_SQRT = 10, ///< "Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range)"
OBSCOL_TRC_IEC61966_2_4 = 11, ///< IEC 61966-2-4
OBSCOL_TRC_BT1361_ECG = 12, ///< ITU-R BT1361 Extended Colour Gamut
OBSCOL_TRC_IEC61966_2_1 = 13, ///< IEC 61966-2-1 (sRGB or sYCC)
OBSCOL_TRC_BT2020_10 = 14, ///< ITU-R BT2020 for 10-bit system
OBSCOL_TRC_BT2020_12 = 15, ///< ITU-R BT2020 for 12-bit system
OBSCOL_TRC_SMPTE2084 = 16, ///< SMPTE ST 2084 for 10-, 12-, 14- and 16-bit systems
OBSCOL_TRC_SMPTEST2084 = OBSCOL_TRC_SMPTE2084,
OBSCOL_TRC_SMPTE428 = 17, ///< SMPTE ST 428-1
OBSCOL_TRC_SMPTEST428_1 = OBSCOL_TRC_SMPTE428,
OBSCOL_TRC_ARIB_STD_B67 = 18, ///< ARIB STD-B67, known as "Hybrid log-gamma"
OBSCOL_TRC_NB ///< Not part of ABI
};
/**
* YUV Colorspace Type
* These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.3.
*/
enum OBSColorSpace {
OBSCOL_SPC_RGB = 0, ///< order of coefficients is actually GBR, also IEC 61966-2-1 (sRGB), YZX and ST 428-1
OBSCOL_SPC_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 xvYCC709 / derived in SMPTE RP 177 Annex B
OBSCOL_SPC_UNSPECIFIED = 2,
OBSCOL_SPC_RESERVED = 3, ///< reserved for future use by ITU-T and ISO/IEC just like 15-255 are
OBSCOL_SPC_FCC = 4, ///< FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
OBSCOL_SPC_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM / IEC 61966-2-4 xvYCC601
OBSCOL_SPC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC / functionally identical to above
OBSCOL_SPC_SMPTE240M = 7, ///< derived from 170M primaries and D65 white point, 170M is derived from BT470 System M's primaries
OBSCOL_SPC_YCGCO = 8, ///< used by Dirac / VC-2 and H.264 FRext, see ITU-T SG16
OBSCOL_SPC_YCOCG = OBSCOL_SPC_YCGCO,
OBSCOL_SPC_BT2020_NCL = 9, ///< ITU-R BT2020 non-constant luminance system
OBSCOL_SPC_BT2020_CL = 10, ///< ITU-R BT2020 constant luminance system
OBSCOL_SPC_SMPTE2085 = 11, ///< SMPTE 2085, Y'D'zD'x
OBSCOL_SPC_CHROMA_DERIVED_NCL = 12, ///< Chromaticity-derived non-constant luminance system
OBSCOL_SPC_CHROMA_DERIVED_CL = 13, ///< Chromaticity-derived constant luminance system
OBSCOL_SPC_ICTCP = 14, ///< ITU-R BT.2100-0, ICtCp
OBSCOL_SPC_NB ///< Not part of ABI
};
/* clang-format on */
static inline bool stopping(struct flv_output *stream)
{
return os_atomic_load_bool(&stream->stopping);
}
static inline bool active(struct flv_output *stream)
{
return os_atomic_load_bool(&stream->active);
}
static const char *flv_output_getname(void *unused)
{
UNUSED_PARAMETER(unused);
return obs_module_text("FLVOutput");
}
static void flv_output_stop(void *data, uint64_t ts);
static void flv_output_destroy(void *data)
{
struct flv_output *stream = data;
pthread_mutex_destroy(&stream->mutex);
dstr_free(&stream->path);
bfree(stream);
}
static void *flv_output_create(obs_data_t *settings, obs_output_t *output)
{
struct flv_output *stream = bzalloc(sizeof(struct flv_output));
stream->output = output;
pthread_mutex_init(&stream->mutex, NULL);
UNUSED_PARAMETER(settings);
return stream;
}
static int write_packet(struct flv_output *stream,
struct encoder_packet *packet, bool is_header)
{
uint8_t *data;
size_t size;
int ret = 0;
stream->last_packet_ts = get_ms_time(packet, packet->dts);
flv_packet_mux(packet, is_header ? 0 : stream->start_dts_offset, &data,
&size, is_header);
fwrite(data, 1, size, stream->file);
bfree(data);
return ret;
}
static int write_packet_ex(struct flv_output *stream,
struct encoder_packet *packet, bool is_header,
bool is_footer, size_t idx)
{
uint8_t *data;
size_t size = 0;
int ret = 0;
if (is_header) {
flv_packet_start(packet, stream->video_codec[idx], &data, &size,
idx);
} else if (is_footer) {
flv_packet_end(packet, stream->video_codec[idx], &data, &size,
idx);
} else {
flv_packet_frames(packet, stream->video_codec[idx],
stream->start_dts_offset, &data, &size, idx);
}
fwrite(data, 1, size, stream->file);
bfree(data);
// manually created packets
if (is_header || is_footer)
bfree(packet->data);
else
obs_encoder_packet_release(packet);
return ret;
}
static int write_audio_packet_ex(struct flv_output *stream,
struct encoder_packet *packet, bool is_header,
size_t idx)
{
uint8_t *data;
size_t size = 0;
int ret = 0;
if (is_header) {
flv_packet_audio_start(packet, stream->audio_codec[idx], &data,
&size, idx);
} else {
flv_packet_audio_frames(packet, stream->audio_codec[idx],
stream->start_dts_offset, &data, &size,
idx);
}
fwrite(data, 1, size, stream->file);
bfree(data);
return ret;
}
static void write_meta_data(struct flv_output *stream)
{
uint8_t *meta_data;
size_t meta_data_size;
flv_meta_data(stream->output, &meta_data, &meta_data_size, true);
fwrite(meta_data, 1, meta_data_size, stream->file);
bfree(meta_data);
}
static bool write_audio_header(struct flv_output *stream, size_t idx)
{
obs_output_t *context = stream->output;
obs_encoder_t *aencoder = obs_output_get_audio_encoder(context, idx);
struct encoder_packet packet = {.type = OBS_ENCODER_AUDIO,
.timebase_den = 1};
if (!aencoder)
return false;
if (obs_encoder_get_extra_data(aencoder, &packet.data, &packet.size)) {
if (idx == 0) {
write_packet(stream, &packet, true);
} else {
write_audio_packet_ex(stream, &packet, true, idx);
}
}
return true;
}
static bool write_video_header(struct flv_output *stream, size_t idx)
{
obs_output_t *context = stream->output;
obs_encoder_t *vencoder = obs_output_get_video_encoder2(context, idx);
uint8_t *header;
size_t size;
struct encoder_packet packet = {.type = OBS_ENCODER_VIDEO,
.timebase_den = 1,
.keyframe = true};
if (!vencoder)
return false;
if (!obs_encoder_get_extra_data(vencoder, &header, &size))
return false;
switch (stream->video_codec[idx]) {
case CODEC_NONE:
do_log(LOG_ERROR,
"Codec not initialized for track %zu while sending header",
idx);
return false;
case CODEC_H264:
packet.size = obs_parse_avc_header(&packet.data, header, size);
// Always send H.264 on track 0 as old style for compatibility.
if (idx == 0) {
write_packet(stream, &packet, true);
} else {
write_packet_ex(stream, &packet, true, false, idx);
}
return true;
case CODEC_HEVC:
#ifdef ENABLE_HEVC
packet.size = obs_parse_hevc_header(&packet.data, header, size);
break;
#else
return false;
#endif
case CODEC_AV1:
packet.size = obs_parse_av1_header(&packet.data, header, size);
break;
}
write_packet_ex(stream, &packet, true, false, idx);
return true;
}
// only returns false if there's an error, not if no metadata needs to be sent
static bool write_video_metadata(struct flv_output *stream, size_t idx)
{
// send metadata only if HDR
obs_encoder_t *encoder =
obs_output_get_video_encoder2(stream->output, idx);
if (!encoder)
return false;
video_t *video = obs_encoder_video(encoder);
if (!video)
return false;
const struct video_output_info *info = video_output_get_info(video);
enum video_colorspace colorspace = info->colorspace;
if (!(colorspace == VIDEO_CS_2100_PQ ||
colorspace == VIDEO_CS_2100_HLG))
return true;
// Y2023 spec
if (stream->video_codec[idx] == CODEC_H264)
return true;
uint8_t *data;
size_t size;
enum video_format format = info->format;
int bits_per_raw_sample;
switch (format) {
case VIDEO_FORMAT_I010:
case VIDEO_FORMAT_P010:
case VIDEO_FORMAT_I210:
bits_per_raw_sample = 10;
break;
case VIDEO_FORMAT_I412:
case VIDEO_FORMAT_YA2L:
bits_per_raw_sample = 12;
break;
default:
bits_per_raw_sample = 8;
}
int pri = 0;
int trc = 0;
int spc = 0;
switch (colorspace) {
case VIDEO_CS_601:
pri = OBSCOL_PRI_SMPTE170M;
trc = OBSCOL_PRI_SMPTE170M;
spc = OBSCOL_PRI_SMPTE170M;
break;
case VIDEO_CS_DEFAULT:
case VIDEO_CS_709:
pri = OBSCOL_PRI_BT709;
trc = OBSCOL_PRI_BT709;
spc = OBSCOL_PRI_BT709;
break;
case VIDEO_CS_SRGB:
pri = OBSCOL_PRI_BT709;
trc = OBSCOL_TRC_IEC61966_2_1;
spc = OBSCOL_PRI_BT709;
break;
case VIDEO_CS_2100_PQ:
pri = OBSCOL_PRI_BT2020;
trc = OBSCOL_TRC_SMPTE2084;
spc = OBSCOL_SPC_BT2020_NCL;
break;
case VIDEO_CS_2100_HLG:
pri = OBSCOL_PRI_BT2020;
trc = OBSCOL_TRC_ARIB_STD_B67;
spc = OBSCOL_SPC_BT2020_NCL;
}
int max_luminance = 0;
if (trc == OBSCOL_TRC_ARIB_STD_B67)
max_luminance = 1000;
else if (trc == OBSCOL_TRC_SMPTE2084)
max_luminance = (int)obs_get_video_hdr_nominal_peak_level();
flv_packet_metadata(stream->video_codec[idx], &data, &size,
bits_per_raw_sample, pri, trc, spc, 0,
max_luminance, idx);
fwrite(data, 1, size, stream->file);
bfree(data);
return true;
}
static void write_headers(struct flv_output *stream)
{
for (size_t i = 0; i < MAX_OUTPUT_AUDIO_ENCODERS; i++) {
obs_encoder_t *enc =
obs_output_get_audio_encoder(stream->output, i);
if (!enc)
break;
const char *codec = obs_encoder_get_codec(enc);
stream->audio_codec[i] = to_audio_type(codec);
}
for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; i++) {
obs_encoder_t *enc =
obs_output_get_video_encoder2(stream->output, i);
if (!enc)
break;
const char *codec = obs_encoder_get_codec(enc);
stream->video_codec[i] = to_video_type(codec);
}
write_meta_data(stream);
write_audio_header(stream, 0);
for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; i++) {
obs_encoder_t *enc =
obs_output_get_video_encoder2(stream->output, i);
if (!enc)
continue;
if (!write_video_header(stream, i) ||
!write_video_metadata(stream, i))
return;
}
for (size_t i = 1; write_audio_header(stream, i); i++)
;
}
static bool write_video_footer(struct flv_output *stream, size_t idx)
{
struct encoder_packet packet = {.type = OBS_ENCODER_VIDEO,
.timebase_den = 1,
.keyframe = false};
packet.size = 0;
return write_packet_ex(stream, &packet, false, true, idx) >= 0;
}
static inline bool write_footers(struct flv_output *stream)
{
for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; i++) {
obs_encoder_t *encoder =
obs_output_get_video_encoder2(stream->output, i);
if (!encoder)
continue;
if (i == 0 && stream->video_codec[i] == CODEC_H264)
continue;
if (!write_video_footer(stream, i))
return false;
}
return true;
}
static bool flv_output_start(void *data)
{
struct flv_output *stream = data;
obs_data_t *settings;
const char *path;
if (!obs_output_can_begin_data_capture(stream->output, 0))
return false;
if (!obs_output_initialize_encoders(stream->output, 0))
return false;
stream->got_first_video = false;
stream->sent_headers = false;
os_atomic_set_bool(&stream->stopping, false);
/* get path */
settings = obs_output_get_settings(stream->output);
path = obs_data_get_string(settings, "path");
dstr_copy(&stream->path, path);
obs_data_release(settings);
stream->file = os_fopen(stream->path.array, "wb");
if (!stream->file) {
warn("Unable to open FLV file '%s'", stream->path.array);
return false;
}
/* write headers and start capture */
os_atomic_set_bool(&stream->active, true);
obs_output_begin_data_capture(stream->output, 0);
info("Writing FLV file '%s'...", stream->path.array);
return true;
}
static void flv_output_stop(void *data, uint64_t ts)
{
struct flv_output *stream = data;
stream->stop_ts = ts / 1000;
os_atomic_set_bool(&stream->stopping, true);
}
static void flv_output_actual_stop(struct flv_output *stream, int code)
{
os_atomic_set_bool(&stream->active, false);
if (stream->file) {
write_footers(stream);
write_file_info(stream->file, stream->last_packet_ts,
os_ftelli64(stream->file));
fclose(stream->file);
}
if (code) {
obs_output_signal_stop(stream->output, code);
} else {
obs_output_end_data_capture(stream->output);
}
info("FLV file output complete");
}
static void flv_output_data(void *data, struct encoder_packet *packet)
{
struct flv_output *stream = data;
struct encoder_packet parsed_packet;
pthread_mutex_lock(&stream->mutex);
if (!active(stream))
goto unlock;
if (!packet) {
flv_output_actual_stop(stream, OBS_OUTPUT_ENCODE_ERROR);
goto unlock;
}
if (stopping(stream)) {
if (packet->sys_dts_usec >= (int64_t)stream->stop_ts) {
flv_output_actual_stop(stream, 0);
goto unlock;
}
}
if (!stream->sent_headers) {
write_headers(stream);
stream->sent_headers = true;
}
if (packet->type == OBS_ENCODER_VIDEO) {
if (!stream->got_first_video) {
stream->start_dts_offset =
get_ms_time(packet, packet->dts);
stream->got_first_video = true;
}
switch (stream->video_codec[packet->track_idx]) {
case CODEC_NONE:
do_log(LOG_ERROR, "Codec not initialized for track %zu",
packet->track_idx);
goto unlock;
case CODEC_H264:
obs_parse_avc_packet(&parsed_packet, packet);
break;
case CODEC_HEVC:
#ifdef ENABLE_HEVC
obs_parse_hevc_packet(&parsed_packet, packet);
break;
#else
goto unlock;
#endif
case CODEC_AV1:
obs_parse_av1_packet(&parsed_packet, packet);
break;
}
if (stream->video_codec[packet->track_idx] != CODEC_H264 ||
(stream->video_codec[packet->track_idx] == CODEC_H264 &&
packet->track_idx != 0)) {
write_packet_ex(stream, &parsed_packet, false, false,
packet->track_idx);
} else {
write_packet(stream, &parsed_packet, false);
}
obs_encoder_packet_release(&parsed_packet);
} else {
if (packet->track_idx != 0) {
write_audio_packet_ex(stream, packet, false,
packet->track_idx);
} else {
write_packet(stream, packet, false);
}
}
unlock:
pthread_mutex_unlock(&stream->mutex);
}
static obs_properties_t *flv_output_properties(void *unused)
{
UNUSED_PARAMETER(unused);
obs_properties_t *props = obs_properties_create();
obs_properties_add_text(props, "path",
obs_module_text("FLVOutput.FilePath"),
OBS_TEXT_DEFAULT);
return props;
}
struct obs_output_info flv_output_info = {
.id = "flv_output",
.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED | OBS_OUTPUT_MULTI_TRACK_AV,
#ifdef ENABLE_HEVC
.encoded_video_codecs = "h264;hevc;av1",
#else
.encoded_video_codecs = "h264;av1",
#endif
.encoded_audio_codecs = "aac",
.get_name = flv_output_getname,
.create = flv_output_create,
.destroy = flv_output_destroy,
.start = flv_output_start,
.stop = flv_output_stop,
.encoded_packet = flv_output_data,
.get_properties = flv_output_properties,
};