mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-08-30 17:58:37 +08:00
Merge pull request #10608 from derrod/obs-mp4-muxer
Native Hybrid MP4 Muxer
This commit is contained in:
@@ -337,6 +337,23 @@ struct OBSStudioAPI : obs_frontend_callbacks {
|
||||
}
|
||||
}
|
||||
|
||||
bool obs_frontend_recording_add_chapter(const char *name) override
|
||||
{
|
||||
if (!os_atomic_load_bool(&recording_active) ||
|
||||
os_atomic_load_bool(&recording_paused))
|
||||
return false;
|
||||
|
||||
proc_handler_t *ph = obs_output_get_proc_handler(
|
||||
main->outputHandler->fileOutput);
|
||||
|
||||
calldata cd;
|
||||
calldata_init(&cd);
|
||||
calldata_set_string(&cd, "chapter_name", name);
|
||||
bool result = proc_handler_call(ph, "add_chapter", &cd);
|
||||
calldata_free(&cd);
|
||||
return result;
|
||||
}
|
||||
|
||||
void obs_frontend_replay_buffer_start(void) override
|
||||
{
|
||||
QMetaObject::invokeMethod(main, "StartReplayBuffer");
|
||||
|
||||
@@ -732,6 +732,7 @@ Basic.Main.StopRecording="Stop Recording"
|
||||
Basic.Main.PauseRecording="Pause Recording"
|
||||
Basic.Main.UnpauseRecording="Unpause Recording"
|
||||
Basic.Main.SplitFile="Split Recording File"
|
||||
Basic.Main.AddChapterMarker="Add Chapter Marker"
|
||||
Basic.Main.StoppingRecording="Stopping Recording..."
|
||||
Basic.Main.StopReplayBuffer="Stop Replay Buffer"
|
||||
Basic.Main.StoppingReplayBuffer="Stopping Replay Buffer..."
|
||||
@@ -966,6 +967,7 @@ Basic.Settings.Output.Format.MP4="MPEG-4 (.mp4)"
|
||||
Basic.Settings.Output.Format.MOV="QuickTime (.mov)"
|
||||
Basic.Settings.Output.Format.TS="MPEG-TS (.ts)"
|
||||
Basic.Settings.Output.Format.HLS="HLS (.m3u8 + .ts)"
|
||||
Basic.Settings.Output.Format.hMP4="Hybrid MP4 [BETA] (.mp4)"
|
||||
Basic.Settings.Output.Format.fMP4="Fragmented MP4 (.mp4)"
|
||||
Basic.Settings.Output.Format.fMOV="Fragmented MOV (.mov)"
|
||||
Basic.Settings.Output.Format.TT.fragmented_mov="Fragmented MOV writes the recording in chunks and does not require the same finalization as traditional MOV files.\nThis ensures the file remains playable even if writing to disk is interrupted, for example, as a result of a BSOD or power loss.\n\nThis may not be compatible with all players and editors. Use File → Remux Recordings to convert the file into a more compatible format if necessary."
|
||||
|
||||
@@ -185,6 +185,20 @@ static const unordered_map<string, unordered_set<string>> codec_compat = {
|
||||
"pcm_f32le",
|
||||
#endif
|
||||
}},
|
||||
// Not part of FFmpeg, see obs-outputs module
|
||||
{"hybrid_mp4",
|
||||
{
|
||||
"h264",
|
||||
"hevc",
|
||||
"av1",
|
||||
"aac",
|
||||
"opus",
|
||||
"alac",
|
||||
"flac",
|
||||
"pcm_s16le",
|
||||
"pcm_s24le",
|
||||
"pcm_f32le",
|
||||
}},
|
||||
{"mov",
|
||||
{
|
||||
"h264",
|
||||
|
||||
@@ -1783,6 +1783,8 @@ string GetFormatExt(const char *container)
|
||||
string ext = container;
|
||||
if (ext == "fragmented_mp4")
|
||||
ext = "mp4";
|
||||
if (ext == "hybrid_mp4")
|
||||
ext = "mp4";
|
||||
else if (ext == "fragmented_mov")
|
||||
ext = "mov";
|
||||
else if (ext == "hls")
|
||||
|
||||
@@ -285,6 +285,12 @@ bool obs_frontend_recording_split_file(void)
|
||||
: false;
|
||||
}
|
||||
|
||||
bool obs_frontend_recording_add_chapter(const char *name)
|
||||
{
|
||||
return !!callbacks_valid() ? c->obs_frontend_recording_add_chapter(name)
|
||||
: false;
|
||||
}
|
||||
|
||||
void obs_frontend_replay_buffer_start(void)
|
||||
{
|
||||
if (callbacks_valid())
|
||||
|
||||
@@ -190,6 +190,7 @@ EXPORT bool obs_frontend_recording_active(void);
|
||||
EXPORT void obs_frontend_recording_pause(bool pause);
|
||||
EXPORT bool obs_frontend_recording_paused(void);
|
||||
EXPORT bool obs_frontend_recording_split_file(void);
|
||||
EXPORT bool obs_frontend_recording_add_chapter(const char *name);
|
||||
|
||||
EXPORT void obs_frontend_replay_buffer_start(void);
|
||||
EXPORT void obs_frontend_replay_buffer_save(void);
|
||||
|
||||
@@ -53,6 +53,7 @@ struct obs_frontend_callbacks {
|
||||
virtual void obs_frontend_recording_pause(bool pause) = 0;
|
||||
virtual bool obs_frontend_recording_paused(void) = 0;
|
||||
virtual bool obs_frontend_recording_split_file(void) = 0;
|
||||
virtual bool obs_frontend_recording_add_chapter(const char *name) = 0;
|
||||
|
||||
virtual void obs_frontend_replay_buffer_start(void) = 0;
|
||||
virtual void obs_frontend_replay_buffer_save(void) = 0;
|
||||
|
||||
@@ -697,6 +697,9 @@ SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_)
|
||||
if (!ffmpegOutput) {
|
||||
bool useReplayBuffer = config_get_bool(main->Config(),
|
||||
"SimpleOutput", "RecRB");
|
||||
const char *recFormat = config_get_string(
|
||||
main->Config(), "SimpleOutput", "RecFormat2");
|
||||
|
||||
if (useReplayBuffer) {
|
||||
OBSDataAutoRelease hotkey;
|
||||
const char *str = config_get_string(
|
||||
@@ -728,8 +731,10 @@ SimpleOutput::SimpleOutput(OBSBasic *main_) : BasicOutputHandler(main_)
|
||||
OBSReplayBufferSaved, this);
|
||||
}
|
||||
|
||||
bool use_native = strcmp(recFormat, "hybrid_mp4") == 0;
|
||||
fileOutput = obs_output_create(
|
||||
"ffmpeg_muxer", "simple_file_output", nullptr, nullptr);
|
||||
use_native ? "mp4_output" : "ffmpeg_muxer",
|
||||
"simple_file_output", nullptr, nullptr);
|
||||
if (!fileOutput)
|
||||
throw "Failed to create recording output "
|
||||
"(simple output)";
|
||||
@@ -1568,6 +1573,8 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_)
|
||||
config_get_string(main->Config(), "AdvOut", "RecEncoder");
|
||||
const char *recAudioEncoder =
|
||||
config_get_string(main->Config(), "AdvOut", "RecAudioEncoder");
|
||||
const char *recFormat =
|
||||
config_get_string(main->Config(), "AdvOut", "RecFormat2");
|
||||
#ifdef __APPLE__
|
||||
translate_macvth264_encoder(streamEncoder);
|
||||
translate_macvth264_encoder(recordEncoder);
|
||||
@@ -1623,8 +1630,10 @@ AdvancedOutput::AdvancedOutput(OBSBasic *main_) : BasicOutputHandler(main_)
|
||||
OBSReplayBufferSaved, this);
|
||||
}
|
||||
|
||||
bool native_muxer = strcmp(recFormat, "hybrid_mp4") == 0;
|
||||
fileOutput = obs_output_create(
|
||||
"ffmpeg_muxer", "adv_file_output", nullptr, nullptr);
|
||||
native_muxer ? "mp4_output" : "ffmpeg_muxer",
|
||||
"adv_file_output", nullptr, nullptr);
|
||||
if (!fileOutput)
|
||||
throw "Failed to create recording output "
|
||||
"(advanced output)";
|
||||
|
||||
@@ -2808,6 +2808,23 @@ void OBSBasic::CreateHotkeys()
|
||||
this);
|
||||
LoadHotkey(splitFileHotkey, "OBSBasic.SplitFile");
|
||||
|
||||
/* Adding chapters is only supported by the native MP4 output */
|
||||
const string_view output_id =
|
||||
obs_output_get_id(outputHandler->fileOutput);
|
||||
if (output_id == "mp4_output") {
|
||||
addChapterHotkey = obs_hotkey_register_frontend(
|
||||
"OBSBasic.AddChapterMarker",
|
||||
Str("Basic.Main.AddChapterMarker"),
|
||||
[](void *, obs_hotkey_id, obs_hotkey_t *,
|
||||
bool pressed) {
|
||||
if (pressed)
|
||||
obs_frontend_recording_add_chapter(
|
||||
nullptr);
|
||||
},
|
||||
this);
|
||||
LoadHotkey(addChapterHotkey, "OBSBasic.AddChapterMarker");
|
||||
}
|
||||
|
||||
replayBufHotkeys = obs_hotkey_pair_register_frontend(
|
||||
"OBSBasic.StartReplayBuffer",
|
||||
Str("Basic.Main.StartReplayBuffer"),
|
||||
@@ -2936,6 +2953,7 @@ void OBSBasic::ClearHotkeys()
|
||||
obs_hotkey_pair_unregister(recordingHotkeys);
|
||||
obs_hotkey_pair_unregister(pauseHotkeys);
|
||||
obs_hotkey_unregister(splitFileHotkey);
|
||||
obs_hotkey_unregister(addChapterHotkey);
|
||||
obs_hotkey_pair_unregister(replayBufHotkeys);
|
||||
obs_hotkey_pair_unregister(vcamHotkeys);
|
||||
obs_hotkey_pair_unregister(togglePreviewHotkeys);
|
||||
|
||||
@@ -459,7 +459,8 @@ private:
|
||||
obs_hotkey_pair_id streamingHotkeys, recordingHotkeys, pauseHotkeys,
|
||||
replayBufHotkeys, vcamHotkeys, togglePreviewHotkeys,
|
||||
contextBarHotkeys;
|
||||
obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey;
|
||||
obs_hotkey_id forceStreamingStopHotkey, splitFileHotkey,
|
||||
addChapterHotkey;
|
||||
|
||||
void InitDefaultTransitions();
|
||||
void InitTransition(obs_source_t *transition);
|
||||
|
||||
@@ -1157,6 +1157,7 @@ void OBSBasicSettings::LoadFormats()
|
||||
ui->simpleOutRecFormat->addItem(FORMAT_STR("MKV"), "mkv");
|
||||
ui->simpleOutRecFormat->addItem(FORMAT_STR("MP4"), "mp4");
|
||||
ui->simpleOutRecFormat->addItem(FORMAT_STR("MOV"), "mov");
|
||||
ui->simpleOutRecFormat->addItem(FORMAT_STR("hMP4"), "hybrid_mp4");
|
||||
ui->simpleOutRecFormat->addItem(FORMAT_STR("fMP4"), "fragmented_mp4");
|
||||
ui->simpleOutRecFormat->addItem(FORMAT_STR("fMOV"), "fragmented_mov");
|
||||
ui->simpleOutRecFormat->addItem(FORMAT_STR("TS"), "mpegts");
|
||||
@@ -1165,6 +1166,7 @@ void OBSBasicSettings::LoadFormats()
|
||||
ui->advOutRecFormat->addItem(FORMAT_STR("MKV"), "mkv");
|
||||
ui->advOutRecFormat->addItem(FORMAT_STR("MP4"), "mp4");
|
||||
ui->advOutRecFormat->addItem(FORMAT_STR("MOV"), "mov");
|
||||
ui->advOutRecFormat->addItem(FORMAT_STR("hMP4"), "hybrid_mp4");
|
||||
ui->advOutRecFormat->addItem(FORMAT_STR("fMP4"), "fragmented_mp4");
|
||||
ui->advOutRecFormat->addItem(FORMAT_STR("fMOV"), "fragmented_mov");
|
||||
ui->advOutRecFormat->addItem(FORMAT_STR("TS"), "mpegts");
|
||||
|
||||
@@ -36,6 +36,10 @@ target_sources(
|
||||
librtmp/rtmp.c
|
||||
librtmp/rtmp.h
|
||||
librtmp/rtmp_sys.h
|
||||
mp4-mux-internal.h
|
||||
mp4-mux.c
|
||||
mp4-mux.h
|
||||
mp4-output.c
|
||||
net-if.c
|
||||
net-if.h
|
||||
null-output.c
|
||||
@@ -60,6 +64,7 @@ target_link_libraries(
|
||||
obs-outputs
|
||||
PRIVATE OBS::libobs
|
||||
OBS::happy-eyeballs
|
||||
OBS::opts-parser
|
||||
MbedTLS::MbedTLS
|
||||
ZLIB::ZLIB
|
||||
$<$<PLATFORM_ID:Windows>:OBS::w32-pthreads>
|
||||
|
||||
@@ -7,6 +7,11 @@ FLVOutput="FLV File Output"
|
||||
FLVOutput.FilePath="File Path"
|
||||
Default="Default"
|
||||
|
||||
MP4Output="MP4 File Output"
|
||||
MP4Output.FilePath="File Path"
|
||||
MP4Output.StartChapter="Start"
|
||||
MP4Output.UnnamedChapter="Unnamed"
|
||||
|
||||
IPFamily="IP Address Family"
|
||||
IPFamily.Both="IPv4 and IPv6 (Default)"
|
||||
IPFamily.V4Only="IPv4 Only"
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
/******************************************************************************
|
||||
Copyright (C) 2024 by Dennis Sädtler <dennis@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/>.
|
||||
******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "mp4-mux.h"
|
||||
|
||||
#include <util/darray.h>
|
||||
#include <util/deque.h>
|
||||
#include <util/serializer.h>
|
||||
|
||||
/* Flavour for target compatibility */
|
||||
enum mp4_flavour {
|
||||
MP4, /* ISO/IEC 14496-12 */
|
||||
MOV, /* Apple QuickTime */
|
||||
CMAF, /* ISO/IEC 23000-19 */
|
||||
};
|
||||
|
||||
enum mp4_track_type {
|
||||
TRACK_UNKNOWN,
|
||||
TRACK_VIDEO,
|
||||
TRACK_AUDIO,
|
||||
TRACK_CHAPTERS,
|
||||
};
|
||||
|
||||
enum mp4_codec {
|
||||
CODEC_UNKNOWN,
|
||||
|
||||
/* Video Codecs */
|
||||
CODEC_H264,
|
||||
CODEC_HEVC,
|
||||
CODEC_AV1,
|
||||
|
||||
/* Audio Codecs */
|
||||
CODEC_AAC,
|
||||
CODEC_OPUS,
|
||||
CODEC_FLAC,
|
||||
CODEC_ALAC,
|
||||
CODEC_PCM_I16,
|
||||
CODEC_PCM_I24,
|
||||
CODEC_PCM_F32,
|
||||
|
||||
/* Text/Chapter trakcs */
|
||||
CODEC_TEXT,
|
||||
};
|
||||
|
||||
struct chunk {
|
||||
uint64_t offset;
|
||||
uint32_t size;
|
||||
uint32_t samples;
|
||||
};
|
||||
|
||||
struct sample_delta {
|
||||
uint32_t count;
|
||||
uint32_t delta;
|
||||
};
|
||||
|
||||
struct sample_offset {
|
||||
uint32_t count;
|
||||
int32_t offset;
|
||||
};
|
||||
|
||||
struct fragment_sample {
|
||||
uint32_t size;
|
||||
int32_t offset;
|
||||
uint32_t duration;
|
||||
};
|
||||
|
||||
struct mp4_track {
|
||||
enum mp4_track_type type;
|
||||
enum mp4_codec codec;
|
||||
|
||||
/* Track ID in container */
|
||||
uint8_t track_id;
|
||||
/* Number of samples for this track */
|
||||
uint64_t samples;
|
||||
/* Duration for this track */
|
||||
uint64_t duration;
|
||||
|
||||
/* Encoder associated with this track */
|
||||
obs_encoder_t *encoder;
|
||||
|
||||
/* Time Base (1/FPS for video, 1/sample rate for audio) */
|
||||
uint32_t timebase_num;
|
||||
uint32_t timebase_den;
|
||||
/* Output timescale calculated from time base (Video only) */
|
||||
uint32_t timescale;
|
||||
|
||||
/* First PTS this track has seen (in track timescale) */
|
||||
int64_t first_pts;
|
||||
/* Highest PTS this track has seen (in usec) */
|
||||
int64_t last_pts_usec;
|
||||
|
||||
/* deque of encoder_packet belonging to this track */
|
||||
struct deque packets;
|
||||
|
||||
/* Sample sizes (fixed for PCM) */
|
||||
uint32_t sample_size;
|
||||
DARRAY(uint32_t) sample_sizes;
|
||||
/* Data chunks in file containing samples for this track */
|
||||
DARRAY(struct chunk) chunks;
|
||||
/* Time delta between samples */
|
||||
DARRAY(struct sample_delta) deltas;
|
||||
|
||||
/* Sample CT-DT offset, i.e. DTS-PTS offset (Video only) */
|
||||
bool needs_ctts;
|
||||
int32_t dts_offset;
|
||||
DARRAY(struct sample_offset) offsets;
|
||||
/* Sync samples, i.e. keyframes (Video only) */
|
||||
DARRAY(uint32_t) sync_samples;
|
||||
|
||||
/* Temporary array with information about the samples to be included
|
||||
* in the next fragment. */
|
||||
DARRAY(struct fragment_sample) fragment_samples;
|
||||
};
|
||||
|
||||
struct mp4_mux {
|
||||
obs_output_t *output;
|
||||
struct serializer *serializer;
|
||||
|
||||
/* Target format compatibility */
|
||||
enum mp4_flavour mode;
|
||||
|
||||
/* Flags */
|
||||
enum mp4_mux_flags flags;
|
||||
|
||||
uint32_t fragments_written;
|
||||
/* PTS where next fragmentation should take place */
|
||||
int64_t next_frag_pts;
|
||||
|
||||
/* Creation time (seconds since Jan 1 1904) */
|
||||
uint64_t creation_time;
|
||||
|
||||
/* Offset of placeholder atom/box to contain final mdat header */
|
||||
size_t placeholder_offset;
|
||||
|
||||
uint8_t track_ctr;
|
||||
/* Audio/Video tracks */
|
||||
DARRAY(struct mp4_track) tracks;
|
||||
/* Special tracks */
|
||||
struct mp4_track *chapter_track;
|
||||
};
|
||||
|
||||
/* clang-format off */
|
||||
// Defined in ISO/IEC 14496-12:2015 Section 8.2.2.1
|
||||
const int32_t UNITY_MATRIX[9] = {
|
||||
0x00010000, 0, 0,
|
||||
0, 0x00010000, 0,
|
||||
0, 0, 0x40000000
|
||||
};
|
||||
/* clang-format on */
|
||||
|
||||
enum tfhd_flags {
|
||||
BASE_DATA_OFFSET_PRESENT = 0x000001,
|
||||
SAMPLE_DESCRIPTION_INDEX_PRESENT = 0x000002,
|
||||
DEFAULT_SAMPLE_DURATION_PRESENT = 0x000008,
|
||||
DEFAULT_SAMPLE_SIZE_PRESENT = 0x000010,
|
||||
DEFAULT_SAMPLE_FLAGS_PRESENT = 0x000020,
|
||||
DURATION_IS_EMPTY = 0x010000,
|
||||
DEFAULT_BASE_IS_MOOF = 0x020000,
|
||||
};
|
||||
|
||||
enum trun_flags {
|
||||
DATA_OFFSET_PRESENT = 0x000001,
|
||||
FIRST_SAMPLE_FLAGS_PRESENT = 0x000004,
|
||||
SAMPLE_DURATION_PRESENT = 0x000100,
|
||||
SAMPLE_SIZE_PRESENT = 0x000200,
|
||||
SAMPLE_FLAGS_PRESENT = 0x000400,
|
||||
SAMPLE_COMPOSITION_TIME_OFFSETS_PRESENT = 0x000800,
|
||||
};
|
||||
|
||||
/*
|
||||
* ISO Standard structure (big endian so we can't easily use it):
|
||||
*
|
||||
* struct sample_flags {
|
||||
* uint32_t reserved : 4;
|
||||
* uint32_t is_leading : 2;
|
||||
* uint32_t sample_depends_on : 2;
|
||||
* uint32_t sample_is_depended_on : 2;
|
||||
* uint32_t sample_has_redundancy : 2;
|
||||
* uint32_t sample_padding_value : 3;
|
||||
* uint32_t sample_is_non_sync_sample : 1;
|
||||
* uint32_t sample_degradation_priority : 16;
|
||||
};
|
||||
*/
|
||||
|
||||
enum sample_flags {
|
||||
SAMPLE_FLAG_IS_NON_SYNC = 0x00010000,
|
||||
SAMPLE_FLAG_DEPENDS_YES = 0x01000000,
|
||||
SAMPLE_FLAG_DEPENDS_NO = 0x02000000,
|
||||
};
|
||||
|
||||
#ifndef _WIN32
|
||||
static inline size_t min(size_t a, size_t b)
|
||||
{
|
||||
return a < b ? a : b;
|
||||
}
|
||||
#endif
|
||||
|
||||
static inline void get_speaker_positions(enum speaker_layout layout,
|
||||
uint8_t *arr, uint8_t *size,
|
||||
uint8_t *iso_layout)
|
||||
{
|
||||
switch (layout) {
|
||||
case SPEAKERS_MONO:
|
||||
arr[0] = 2; // FC
|
||||
*size = 1;
|
||||
*iso_layout = 1;
|
||||
break;
|
||||
case SPEAKERS_UNKNOWN:
|
||||
case SPEAKERS_STEREO:
|
||||
arr[0] = 0; // FL
|
||||
arr[1] = 1; // FR
|
||||
*size = 2;
|
||||
*iso_layout = 2;
|
||||
break;
|
||||
case SPEAKERS_2POINT1:
|
||||
arr[0] = 0; // FL
|
||||
arr[1] = 1; // FR
|
||||
arr[2] = 3; // LFE
|
||||
*size = 3;
|
||||
break;
|
||||
case SPEAKERS_4POINT0:
|
||||
arr[0] = 0; // FL
|
||||
arr[1] = 1; // FR
|
||||
arr[2] = 2; // FC
|
||||
arr[3] = 10; // RC
|
||||
*size = 4;
|
||||
*iso_layout = 4;
|
||||
break;
|
||||
case SPEAKERS_4POINT1:
|
||||
arr[0] = 0; // FL
|
||||
arr[1] = 1; // FR
|
||||
arr[2] = 2; // FC
|
||||
arr[3] = 3; // LFE
|
||||
arr[4] = 10; // RC
|
||||
*size = 5;
|
||||
break;
|
||||
case SPEAKERS_5POINT1:
|
||||
arr[0] = 0; // FL
|
||||
arr[1] = 1; // FR
|
||||
arr[2] = 2; // FC
|
||||
arr[3] = 3; // LFE
|
||||
arr[4] = 8; // RL
|
||||
arr[5] = 9; // RR
|
||||
*size = 6;
|
||||
break;
|
||||
case SPEAKERS_7POINT1:
|
||||
arr[0] = 0; // FL
|
||||
arr[1] = 1; // FR
|
||||
arr[2] = 2; // FC
|
||||
arr[3] = 3; // LFE
|
||||
arr[4] = 8; // RL
|
||||
arr[5] = 9; // RR
|
||||
arr[6] = 13; // SL
|
||||
arr[7] = 14; // SR
|
||||
*size = 8;
|
||||
*iso_layout = 12;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static inline void get_colour_information(obs_encoder_t *enc, uint16_t *pri,
|
||||
uint16_t *trc, uint16_t *spc,
|
||||
uint8_t *full_range)
|
||||
{
|
||||
video_t *video = obs_encoder_video(enc);
|
||||
const struct video_output_info *info = video_output_get_info(video);
|
||||
|
||||
*full_range = info->range == VIDEO_RANGE_FULL ? 1 : 0;
|
||||
|
||||
switch (info->colorspace) {
|
||||
case VIDEO_CS_601:
|
||||
*pri = 6; // OBSCOL_PRI_SMPTE170M
|
||||
*trc = 6;
|
||||
*spc = 6;
|
||||
break;
|
||||
case VIDEO_CS_DEFAULT:
|
||||
case VIDEO_CS_709:
|
||||
*pri = 1; // OBSCOL_PRI_BT709
|
||||
*trc = 1;
|
||||
*spc = 1;
|
||||
break;
|
||||
case VIDEO_CS_SRGB:
|
||||
*pri = 1; // OBSCOL_PRI_BT709
|
||||
*trc = 13; // OBSCOL_TRC_IEC61966_2_1
|
||||
*spc = 1; // OBSCOL_PRI_BT709
|
||||
break;
|
||||
case VIDEO_CS_2100_PQ:
|
||||
*pri = 9; // OBSCOL_PRI_BT2020
|
||||
*trc = 16; // OBSCOL_TRC_SMPTE2084
|
||||
*spc = 9; // OBSCOL_SPC_BT2020_NCL
|
||||
break;
|
||||
case VIDEO_CS_2100_HLG:
|
||||
*pri = 9; // OBSCOL_PRI_BT2020
|
||||
*trc = 18; // OBSCOL_TRC_ARIB_STD_B67
|
||||
*spc = 9; // OBSCOL_SPC_BT2020_NCL
|
||||
}
|
||||
}
|
||||
|
||||
/* Chapter stubs (from libavformat/movenc.c) */
|
||||
|
||||
static const uint8_t TEXT_STUB_HEADER[] = {
|
||||
// TextSampleEntry
|
||||
0x00, 0x00, 0x00, 0x01, // displayFlags
|
||||
0x00, 0x00, // horizontal + vertical justification
|
||||
0x00, 0x00, 0x00, 0x00, // bgColourRed/Green/Blue/Alpha
|
||||
// BoxRecord
|
||||
0x00, 0x00, 0x00, 0x00, // defTextBoxTop/Left
|
||||
0x00, 0x00, 0x00, 0x00, // defTextBoxBottom/Right
|
||||
// StyleRecord
|
||||
0x00, 0x00, 0x00, 0x00, // startChar + endChar
|
||||
0x00, 0x01, // fontID
|
||||
0x00, 0x00, // fontStyleFlags + fontSize
|
||||
0x00, 0x00, 0x00, 0x00, // fgColourRed/Green/Blue/Alpha
|
||||
// FontTableBox
|
||||
0x00, 0x00, 0x00, 0x0D, // box size
|
||||
'f', 't', 'a', 'b', // box atom name
|
||||
0x00, 0x01, // entry count
|
||||
// FontRecord
|
||||
0x00, 0x01, // font ID
|
||||
0x00, // font name length
|
||||
};
|
||||
|
||||
/* clang-format off */
|
||||
static const char CHAPTER_PKT_FOOTER[12] = {
|
||||
0x00, 0x00, 0x00, 0x0C,
|
||||
'e', 'n', 'c', 'd',
|
||||
0x00, 0x00, 0x01, 0x00
|
||||
};
|
||||
/* clang-format on */
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
/******************************************************************************
|
||||
Copyright (C) 2024 by Dennis Sädtler <dennis@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/>.
|
||||
******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <obs.h>
|
||||
#include <util/serializer.h>
|
||||
|
||||
struct mp4_mux;
|
||||
|
||||
enum mp4_mux_flags {
|
||||
/* Uses mdta key/value list for metadata instead of QuickTime keys */
|
||||
MP4_USE_MDTA_KEY_VALUE = 1 << 0,
|
||||
/* Write encoder configuration to trak udat */
|
||||
MP4_WRITE_ENCODER_INFO = 1 << 1,
|
||||
/* Skip "soft-remux" and leave file in fragmented state */
|
||||
MP4_SKIP_FINALISATION = 1 << 2,
|
||||
/* Use negative CTS instead of edit lists */
|
||||
MP4_USE_NEGATIVE_CTS = 1 << 3,
|
||||
};
|
||||
|
||||
struct mp4_mux *mp4_mux_create(obs_output_t *output,
|
||||
struct serializer *serializer,
|
||||
enum mp4_mux_flags flags);
|
||||
void mp4_mux_destroy(struct mp4_mux *mux);
|
||||
bool mp4_mux_submit_packet(struct mp4_mux *mux, struct encoder_packet *pkt);
|
||||
bool mp4_mux_add_chapter(struct mp4_mux *mux, int64_t dts_usec,
|
||||
const char *name);
|
||||
bool mp4_mux_finalise(struct mp4_mux *mux);
|
||||
@@ -0,0 +1,613 @@
|
||||
/******************************************************************************
|
||||
Copyright (C) 2024 by Dennis Sädtler <dennis@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 "mp4-mux.h"
|
||||
|
||||
#include <inttypes.h>
|
||||
|
||||
#include <obs-module.h>
|
||||
#include <util/platform.h>
|
||||
#include <util/dstr.h>
|
||||
#include <util/threading.h>
|
||||
#include <util/buffered-file-serializer.h>
|
||||
|
||||
#include <opts-parser.h>
|
||||
|
||||
#define do_log(level, format, ...) \
|
||||
blog(level, "[mp4 output: '%s'] " format, \
|
||||
obs_output_get_name(out->output), ##__VA_ARGS__)
|
||||
|
||||
#define warn(format, ...) do_log(LOG_WARNING, format, ##__VA_ARGS__)
|
||||
#define info(format, ...) do_log(LOG_INFO, format, ##__VA_ARGS__)
|
||||
|
||||
struct chapter {
|
||||
int64_t dts_usec;
|
||||
char *name;
|
||||
};
|
||||
|
||||
struct mp4_output {
|
||||
obs_output_t *output;
|
||||
struct dstr path;
|
||||
|
||||
struct serializer serializer;
|
||||
|
||||
volatile bool active;
|
||||
volatile bool stopping;
|
||||
uint64_t stop_ts;
|
||||
|
||||
bool allow_overwrite;
|
||||
uint64_t total_bytes;
|
||||
|
||||
pthread_mutex_t mutex;
|
||||
|
||||
struct mp4_mux *muxer;
|
||||
int flags;
|
||||
|
||||
int64_t last_dts_usec;
|
||||
DARRAY(struct chapter) chapters;
|
||||
|
||||
/* File splitting stuff */
|
||||
bool split_file_enabled;
|
||||
bool split_file_ready;
|
||||
volatile bool manual_split;
|
||||
|
||||
size_t cur_size;
|
||||
size_t max_size;
|
||||
|
||||
int64_t start_time;
|
||||
int64_t max_time;
|
||||
|
||||
bool found_video[MAX_OUTPUT_VIDEO_ENCODERS];
|
||||
bool found_audio[MAX_OUTPUT_AUDIO_ENCODERS];
|
||||
int64_t video_pts_offsets[MAX_OUTPUT_VIDEO_ENCODERS];
|
||||
int64_t audio_dts_offsets[MAX_OUTPUT_AUDIO_ENCODERS];
|
||||
|
||||
/* Buffer for packets while we reinitialise the muxer after splitting */
|
||||
DARRAY(struct encoder_packet) split_buffer;
|
||||
};
|
||||
|
||||
static inline bool stopping(struct mp4_output *out)
|
||||
{
|
||||
return os_atomic_load_bool(&out->stopping);
|
||||
}
|
||||
|
||||
static inline bool active(struct mp4_output *out)
|
||||
{
|
||||
return os_atomic_load_bool(&out->active);
|
||||
}
|
||||
|
||||
static inline int64_t packet_pts_usec(struct encoder_packet *packet)
|
||||
{
|
||||
return packet->pts * 1000000 / packet->timebase_den;
|
||||
}
|
||||
|
||||
static inline void ts_offset_clear(struct mp4_output *out)
|
||||
{
|
||||
for (size_t i = 0; i < MAX_OUTPUT_VIDEO_ENCODERS; i++) {
|
||||
out->found_video[i] = false;
|
||||
out->video_pts_offsets[i] = 0;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < MAX_OUTPUT_AUDIO_ENCODERS; i++) {
|
||||
out->found_audio[i] = false;
|
||||
out->audio_dts_offsets[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static inline void ts_offset_update(struct mp4_output *out,
|
||||
struct encoder_packet *packet)
|
||||
{
|
||||
int64_t *offset;
|
||||
bool *found;
|
||||
|
||||
if (packet->type == OBS_ENCODER_VIDEO) {
|
||||
offset = &out->video_pts_offsets[packet->track_idx];
|
||||
found = &out->found_video[packet->track_idx];
|
||||
} else {
|
||||
offset = &out->audio_dts_offsets[packet->track_idx];
|
||||
found = &out->found_audio[packet->track_idx];
|
||||
}
|
||||
|
||||
if (*found)
|
||||
return;
|
||||
|
||||
*offset = packet->dts;
|
||||
*found = true;
|
||||
}
|
||||
|
||||
static const char *mp4_output_name(void *unused)
|
||||
{
|
||||
UNUSED_PARAMETER(unused);
|
||||
return obs_module_text("MP4Output");
|
||||
}
|
||||
|
||||
static void mp4_output_destory(void *data)
|
||||
{
|
||||
struct mp4_output *out = data;
|
||||
|
||||
for (size_t i = 0; i < out->chapters.num; i++)
|
||||
bfree(out->chapters.array[i].name);
|
||||
da_free(out->chapters);
|
||||
|
||||
pthread_mutex_destroy(&out->mutex);
|
||||
dstr_free(&out->path);
|
||||
bfree(out);
|
||||
}
|
||||
|
||||
static void mp4_add_chapter_proc(void *data, calldata_t *cd)
|
||||
{
|
||||
struct mp4_output *out = data;
|
||||
struct dstr name = {0};
|
||||
|
||||
dstr_copy(&name, calldata_string(cd, "chapter_name"));
|
||||
|
||||
if (name.len == 0) {
|
||||
/* Generate name if none provided. */
|
||||
dstr_catf(&name, "%s %zu",
|
||||
obs_module_text("MP4Output.UnnamedChapter"),
|
||||
out->chapters.num + 1);
|
||||
}
|
||||
|
||||
int64_t totalRecordSeconds = out->last_dts_usec / 1000 / 1000;
|
||||
int seconds = (int)totalRecordSeconds % 60;
|
||||
int totalMinutes = (int)totalRecordSeconds / 60;
|
||||
int minutes = totalMinutes % 60;
|
||||
int hours = totalMinutes / 60;
|
||||
|
||||
info("Adding chapter \"%s\" at %02d:%02d:%02d", name.array, hours,
|
||||
minutes, seconds);
|
||||
|
||||
pthread_mutex_lock(&out->mutex);
|
||||
struct chapter *chap = da_push_back_new(out->chapters);
|
||||
chap->dts_usec = out->last_dts_usec;
|
||||
chap->name = name.array;
|
||||
pthread_mutex_unlock(&out->mutex);
|
||||
}
|
||||
|
||||
static void split_file_proc(void *data, calldata_t *cd)
|
||||
{
|
||||
struct mp4_output *out = data;
|
||||
|
||||
calldata_set_bool(cd, "split_file_enabled", out->split_file_enabled);
|
||||
if (!out->split_file_enabled)
|
||||
return;
|
||||
|
||||
os_atomic_set_bool(&out->manual_split, true);
|
||||
}
|
||||
|
||||
static void *mp4_output_create(obs_data_t *settings, obs_output_t *output)
|
||||
{
|
||||
struct mp4_output *out = bzalloc(sizeof(struct mp4_output));
|
||||
out->output = output;
|
||||
pthread_mutex_init(&out->mutex, NULL);
|
||||
|
||||
signal_handler_t *sh = obs_output_get_signal_handler(output);
|
||||
signal_handler_add(sh, "void file_changed(string next_file)");
|
||||
|
||||
proc_handler_t *ph = obs_output_get_proc_handler(output);
|
||||
proc_handler_add(ph, "void split_file(out bool split_file_enabled)",
|
||||
split_file_proc, out);
|
||||
proc_handler_add(ph, "void add_chapter(string chapter_name)",
|
||||
mp4_add_chapter_proc, out);
|
||||
|
||||
UNUSED_PARAMETER(settings);
|
||||
return out;
|
||||
}
|
||||
|
||||
static inline void apply_flag(int *flags, const char *value, int flag_value)
|
||||
{
|
||||
if (atoi(value))
|
||||
*flags |= flag_value;
|
||||
else
|
||||
*flags &= ~flag_value;
|
||||
}
|
||||
|
||||
static int parse_custom_options(const char *opts_str)
|
||||
{
|
||||
int flags = MP4_USE_NEGATIVE_CTS;
|
||||
|
||||
struct obs_options opts = obs_parse_options(opts_str);
|
||||
|
||||
for (size_t i = 0; i < opts.count; i++) {
|
||||
struct obs_option opt = opts.options[i];
|
||||
|
||||
if (strcmp(opt.name, "skip_soft_remux") == 0) {
|
||||
apply_flag(&flags, opt.value, MP4_SKIP_FINALISATION);
|
||||
} else if (strcmp(opt.name, "write_encoder_info") == 0) {
|
||||
apply_flag(&flags, opt.value, MP4_WRITE_ENCODER_INFO);
|
||||
} else if (strcmp(opt.name, "use_metadata_tags") == 0) {
|
||||
apply_flag(&flags, opt.value, MP4_USE_MDTA_KEY_VALUE);
|
||||
} else if (strcmp(opt.name, "use_negative_cts") == 0) {
|
||||
apply_flag(&flags, opt.value, MP4_USE_NEGATIVE_CTS);
|
||||
} else {
|
||||
blog(LOG_WARNING, "Unknown muxer option: %s = %s",
|
||||
opt.name, opt.value);
|
||||
}
|
||||
}
|
||||
|
||||
obs_free_options(opts);
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
static bool mp4_output_start(void *data)
|
||||
{
|
||||
struct mp4_output *out = data;
|
||||
|
||||
if (!obs_output_can_begin_data_capture(out->output, 0))
|
||||
return false;
|
||||
if (!obs_output_initialize_encoders(out->output, 0))
|
||||
return false;
|
||||
|
||||
os_atomic_set_bool(&out->stopping, false);
|
||||
|
||||
/* get path */
|
||||
obs_data_t *settings = obs_output_get_settings(out->output);
|
||||
const char *path = obs_data_get_string(settings, "path");
|
||||
dstr_copy(&out->path, path);
|
||||
|
||||
out->max_time = obs_data_get_int(settings, "max_time_sec") * 1000000LL;
|
||||
out->max_size = obs_data_get_int(settings, "max_size_mb") * 1024 * 1024;
|
||||
out->split_file_enabled = obs_data_get_bool(settings, "split_file");
|
||||
out->allow_overwrite = obs_data_get_bool(settings, "allow_overwrite");
|
||||
out->cur_size = 0;
|
||||
|
||||
/* Allow skipping the remux step for debugging purposes. */
|
||||
const char *muxer_settings =
|
||||
obs_data_get_string(settings, "muxer_settings");
|
||||
out->flags = parse_custom_options(muxer_settings);
|
||||
|
||||
obs_data_release(settings);
|
||||
|
||||
if (!buffered_file_serializer_init_defaults(&out->serializer,
|
||||
out->path.array)) {
|
||||
warn("Unable to open MP4 file '%s'", out->path.array);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Initialise muxer and start capture */
|
||||
out->muxer = mp4_mux_create(out->output, &out->serializer, out->flags);
|
||||
os_atomic_set_bool(&out->active, true);
|
||||
obs_output_begin_data_capture(out->output, 0);
|
||||
|
||||
info("Writing Hybrid MP4 file '%s'...", out->path.array);
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool should_split(struct mp4_output *out,
|
||||
struct encoder_packet *packet)
|
||||
{
|
||||
/* split at video frame on primary track */
|
||||
if (packet->type != OBS_ENCODER_VIDEO || packet->track_idx > 0)
|
||||
return false;
|
||||
|
||||
/* don't split group of pictures */
|
||||
if (!packet->keyframe)
|
||||
return false;
|
||||
|
||||
if (os_atomic_load_bool(&out->manual_split))
|
||||
return true;
|
||||
|
||||
/* reached maximum file size */
|
||||
if (out->max_size > 0 &&
|
||||
out->cur_size + (int64_t)packet->size >= out->max_size)
|
||||
return true;
|
||||
|
||||
/* reached maximum duration */
|
||||
if (out->max_time > 0 &&
|
||||
packet->dts_usec - out->start_time >= out->max_time)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void find_best_filename(struct dstr *path, bool space)
|
||||
{
|
||||
int num = 2;
|
||||
|
||||
if (!os_file_exists(path->array))
|
||||
return;
|
||||
|
||||
const char *ext = strrchr(path->array, '.');
|
||||
if (!ext)
|
||||
return;
|
||||
|
||||
size_t extstart = ext - path->array;
|
||||
struct dstr testpath;
|
||||
dstr_init_copy_dstr(&testpath, path);
|
||||
for (;;) {
|
||||
dstr_resize(&testpath, extstart);
|
||||
dstr_catf(&testpath, space ? " (%d)" : "_%d", num++);
|
||||
dstr_cat(&testpath, ext);
|
||||
|
||||
if (!os_file_exists(testpath.array)) {
|
||||
dstr_free(path);
|
||||
dstr_init_move(path, &testpath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void generate_filename(struct mp4_output *out, struct dstr *dst,
|
||||
bool overwrite)
|
||||
{
|
||||
obs_data_t *settings = obs_output_get_settings(out->output);
|
||||
const char *dir = obs_data_get_string(settings, "directory");
|
||||
const char *fmt = obs_data_get_string(settings, "format");
|
||||
const char *ext = obs_data_get_string(settings, "extension");
|
||||
bool space = obs_data_get_bool(settings, "allow_spaces");
|
||||
|
||||
char *filename = os_generate_formatted_filename(ext, space, fmt);
|
||||
|
||||
dstr_copy(dst, dir);
|
||||
dstr_replace(dst, "\\", "/");
|
||||
if (dstr_end(dst) != '/')
|
||||
dstr_cat_ch(dst, '/');
|
||||
dstr_cat(dst, filename);
|
||||
|
||||
char *slash = strrchr(dst->array, '/');
|
||||
if (slash) {
|
||||
*slash = 0;
|
||||
os_mkdirs(dst->array);
|
||||
*slash = '/';
|
||||
}
|
||||
|
||||
if (!overwrite)
|
||||
find_best_filename(dst, space);
|
||||
|
||||
bfree(filename);
|
||||
obs_data_release(settings);
|
||||
}
|
||||
|
||||
static bool change_file(struct mp4_output *out, struct encoder_packet *pkt)
|
||||
{
|
||||
uint64_t start_time = os_gettime_ns();
|
||||
|
||||
/* finalise file */
|
||||
for (size_t i = 0; i < out->chapters.num; i++) {
|
||||
struct chapter *chap = &out->chapters.array[i];
|
||||
mp4_mux_add_chapter(out->muxer, chap->dts_usec, chap->name);
|
||||
}
|
||||
|
||||
mp4_mux_finalise(out->muxer);
|
||||
|
||||
info("Waiting for file writer to finish...");
|
||||
|
||||
/* flush/close file and destroy old muxer */
|
||||
buffered_file_serializer_free(&out->serializer);
|
||||
mp4_mux_destroy(out->muxer);
|
||||
|
||||
for (size_t i = 0; i < out->chapters.num; i++)
|
||||
bfree(out->chapters.array[i].name);
|
||||
|
||||
da_clear(out->chapters);
|
||||
|
||||
info("MP4 file split complete. Finalization took %" PRIu64 " ms.",
|
||||
(os_gettime_ns() - start_time) / 1000000);
|
||||
|
||||
/* open new file */
|
||||
generate_filename(out, &out->path, out->allow_overwrite);
|
||||
info("Changing output file to '%s'", out->path.array);
|
||||
|
||||
if (!buffered_file_serializer_init_defaults(&out->serializer,
|
||||
out->path.array)) {
|
||||
warn("Unable to open MP4 file '%s'", out->path.array);
|
||||
return false;
|
||||
}
|
||||
|
||||
out->muxer = mp4_mux_create(out->output, &out->serializer, out->flags);
|
||||
|
||||
calldata_t cd = {0};
|
||||
signal_handler_t *sh = obs_output_get_signal_handler(out->output);
|
||||
calldata_set_string(&cd, "next_file", out->path.array);
|
||||
signal_handler_signal(sh, "file_changed", &cd);
|
||||
calldata_free(&cd);
|
||||
|
||||
out->cur_size = 0;
|
||||
out->start_time = pkt->dts_usec;
|
||||
ts_offset_clear(out);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void mp4_output_stop(void *data, uint64_t ts)
|
||||
{
|
||||
struct mp4_output *out = data;
|
||||
out->stop_ts = ts / 1000;
|
||||
os_atomic_set_bool(&out->stopping, true);
|
||||
}
|
||||
|
||||
static void mp4_output_actual_stop(struct mp4_output *out, int code)
|
||||
{
|
||||
os_atomic_set_bool(&out->active, false);
|
||||
|
||||
uint64_t start_time = os_gettime_ns();
|
||||
|
||||
for (size_t i = 0; i < out->chapters.num; i++) {
|
||||
struct chapter *chap = &out->chapters.array[i];
|
||||
mp4_mux_add_chapter(out->muxer, chap->dts_usec, chap->name);
|
||||
}
|
||||
|
||||
mp4_mux_finalise(out->muxer);
|
||||
|
||||
if (code) {
|
||||
obs_output_signal_stop(out->output, code);
|
||||
} else {
|
||||
obs_output_end_data_capture(out->output);
|
||||
}
|
||||
|
||||
info("Waiting for file writer to finish...");
|
||||
|
||||
/* Flush/close output file and destroy muxer */
|
||||
buffered_file_serializer_free(&out->serializer);
|
||||
mp4_mux_destroy(out->muxer);
|
||||
out->muxer = NULL;
|
||||
|
||||
/* Clear chapter data */
|
||||
for (size_t i = 0; i < out->chapters.num; i++)
|
||||
bfree(out->chapters.array[i].name);
|
||||
|
||||
da_clear(out->chapters);
|
||||
|
||||
info("MP4 file output complete. Finalization took %" PRIu64 " ms.",
|
||||
(os_gettime_ns() - start_time) / 1000000);
|
||||
}
|
||||
|
||||
static void push_back_packet(struct mp4_output *out,
|
||||
struct encoder_packet *packet)
|
||||
{
|
||||
struct encoder_packet pkt;
|
||||
obs_encoder_packet_ref(&pkt, packet);
|
||||
da_push_back(out->split_buffer, &pkt);
|
||||
}
|
||||
|
||||
static inline bool submit_packet(struct mp4_output *out,
|
||||
struct encoder_packet *pkt)
|
||||
{
|
||||
out->total_bytes += pkt->size;
|
||||
|
||||
if (!out->split_file_enabled)
|
||||
return mp4_mux_submit_packet(out->muxer, pkt);
|
||||
|
||||
out->cur_size += pkt->size;
|
||||
|
||||
/* Apply DTS/PTS offset local packet copy */
|
||||
struct encoder_packet modified = *pkt;
|
||||
|
||||
if (modified.type == OBS_ENCODER_VIDEO) {
|
||||
modified.dts -= out->video_pts_offsets[modified.track_idx];
|
||||
modified.pts -= out->video_pts_offsets[modified.track_idx];
|
||||
} else {
|
||||
modified.dts -= out->audio_dts_offsets[modified.track_idx];
|
||||
modified.pts -= out->audio_dts_offsets[modified.track_idx];
|
||||
}
|
||||
|
||||
return mp4_mux_submit_packet(out->muxer, &modified);
|
||||
}
|
||||
|
||||
static void mp4_output_packet(void *data, struct encoder_packet *packet)
|
||||
{
|
||||
struct mp4_output *out = data;
|
||||
|
||||
pthread_mutex_lock(&out->mutex);
|
||||
|
||||
if (!active(out))
|
||||
goto unlock;
|
||||
|
||||
if (!packet) {
|
||||
mp4_output_actual_stop(out, OBS_OUTPUT_ENCODE_ERROR);
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
if (stopping(out)) {
|
||||
if (packet->sys_dts_usec >= (int64_t)out->stop_ts) {
|
||||
mp4_output_actual_stop(out, 0);
|
||||
goto unlock;
|
||||
}
|
||||
}
|
||||
|
||||
if (out->split_file_enabled) {
|
||||
if (out->split_buffer.num) {
|
||||
int64_t pts_usec = packet_pts_usec(packet);
|
||||
struct encoder_packet *first_pkt =
|
||||
out->split_buffer.array;
|
||||
int64_t first_pts_usec = packet_pts_usec(first_pkt);
|
||||
|
||||
if (pts_usec >= first_pts_usec) {
|
||||
if (packet->type != OBS_ENCODER_AUDIO) {
|
||||
push_back_packet(out, packet);
|
||||
goto unlock;
|
||||
}
|
||||
|
||||
if (!change_file(out, first_pkt)) {
|
||||
mp4_output_actual_stop(
|
||||
out, OBS_OUTPUT_ERROR);
|
||||
goto unlock;
|
||||
}
|
||||
out->split_file_ready = true;
|
||||
}
|
||||
} else if (should_split(out, packet)) {
|
||||
push_back_packet(out, packet);
|
||||
goto unlock;
|
||||
}
|
||||
}
|
||||
|
||||
if (out->split_file_ready) {
|
||||
for (size_t i = 0; i < out->split_buffer.num; i++) {
|
||||
struct encoder_packet *pkt =
|
||||
&out->split_buffer.array[i];
|
||||
ts_offset_update(out, pkt);
|
||||
submit_packet(out, pkt);
|
||||
obs_encoder_packet_release(pkt);
|
||||
}
|
||||
|
||||
da_free(out->split_buffer);
|
||||
out->split_file_ready = false;
|
||||
os_atomic_set_bool(&out->manual_split, false);
|
||||
}
|
||||
|
||||
if (out->split_file_enabled)
|
||||
ts_offset_update(out, packet);
|
||||
|
||||
/* Update PTS for chapter markers */
|
||||
if (packet->type == OBS_ENCODER_VIDEO && packet->track_idx == 0)
|
||||
out->last_dts_usec = packet->dts_usec - out->start_time;
|
||||
|
||||
submit_packet(out, packet);
|
||||
|
||||
if (serializer_get_pos(&out->serializer) == -1)
|
||||
mp4_output_actual_stop(out, OBS_OUTPUT_ERROR);
|
||||
|
||||
unlock:
|
||||
pthread_mutex_unlock(&out->mutex);
|
||||
}
|
||||
|
||||
static obs_properties_t *mp4_output_properties(void *unused)
|
||||
{
|
||||
UNUSED_PARAMETER(unused);
|
||||
|
||||
obs_properties_t *props = obs_properties_create();
|
||||
|
||||
obs_properties_add_text(props, "path",
|
||||
obs_module_text("MP4Output.FilePath"),
|
||||
OBS_TEXT_DEFAULT);
|
||||
obs_properties_add_text(props, "muxer_settings", "muxer_settings",
|
||||
OBS_TEXT_DEFAULT);
|
||||
return props;
|
||||
}
|
||||
|
||||
uint64_t mp4_output_total_bytes(void *data)
|
||||
{
|
||||
struct mp4_output *out = data;
|
||||
return out->total_bytes;
|
||||
}
|
||||
|
||||
struct obs_output_info mp4_output_info = {
|
||||
.id = "mp4_output",
|
||||
.flags = OBS_OUTPUT_AV | OBS_OUTPUT_ENCODED |
|
||||
OBS_OUTPUT_MULTI_TRACK_AV | OBS_OUTPUT_CAN_PAUSE,
|
||||
.encoded_video_codecs = "h264;hevc;av1",
|
||||
.encoded_audio_codecs = "aac",
|
||||
.get_name = mp4_output_name,
|
||||
.create = mp4_output_create,
|
||||
.destroy = mp4_output_destory,
|
||||
.start = mp4_output_start,
|
||||
.stop = mp4_output_stop,
|
||||
.encoded_packet = mp4_output_packet,
|
||||
.get_properties = mp4_output_properties,
|
||||
.get_total_bytes = mp4_output_total_bytes,
|
||||
};
|
||||
@@ -15,6 +15,7 @@ MODULE_EXPORT const char *obs_module_description(void)
|
||||
extern struct obs_output_info rtmp_output_info;
|
||||
extern struct obs_output_info null_output_info;
|
||||
extern struct obs_output_info flv_output_info;
|
||||
extern struct obs_output_info mp4_output_info;
|
||||
#if defined(FTL_FOUND)
|
||||
extern struct obs_output_info ftl_output_info;
|
||||
#endif
|
||||
@@ -65,6 +66,7 @@ bool obs_module_load(void)
|
||||
obs_register_output(&rtmp_output_info);
|
||||
obs_register_output(&null_output_info);
|
||||
obs_register_output(&flv_output_info);
|
||||
obs_register_output(&mp4_output_info);
|
||||
#if defined(FTL_FOUND)
|
||||
obs_register_output(&ftl_output_info);
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user