mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-09-24 23:31:36 +08:00
UI: Implement transitions and preview/program mode
Implements transitions, and introduces "Studio Mode" which allows live
editing of the same or different scenes while preserving what's
currently being displayed.
Studio Mode offers a number of new features:
- The ability to edit different scenes or the same scene without
modifying what's currently being displayed (of course)
- The ability to set up "quick transitions" with a desired transition
and duration that can be assigned hotkeys
- The option to create full copies of all sources in the program scene
to allow editing of source properties of the same scene live without
modifying the output, or (by default) just use references. (Note
however that certain sources cannot be duplicated, such as capture
sources, media sources, and device sources)
- Swap Mode (enabled by default) which swaps the program scene with
the preview scene when a transition completes
Currently, only non-configurable transitions (transitions without
properties) are listed, and the only transitions available as of this
writing are fade and cut. In future versions more transitions will be
added, such as swipe, stingers, and many other various sort of
transitions, and the UI will support being able to add/configure/remove
those sort of configurable transitions.
This commit is contained in:
+160
-40
@@ -56,8 +56,6 @@
|
||||
#include <QScreen>
|
||||
#include <QWindow>
|
||||
|
||||
#define PREVIEW_EDGE_SIZE 10
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace {
|
||||
@@ -247,7 +245,9 @@ static void SaveAudioDevice(const char *name, int channel, obs_data_t *parent,
|
||||
obs_source_release(source);
|
||||
}
|
||||
|
||||
static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder)
|
||||
static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder,
|
||||
obs_data_array_t *quickTransitionData, int transitionDuration,
|
||||
OBSScene &scene, OBSSource &curProgramScene)
|
||||
{
|
||||
obs_data_t *saveData = obs_data_create();
|
||||
|
||||
@@ -273,18 +273,26 @@ static obs_data_t *GenerateSaveData(obs_data_array_t *sceneOrder)
|
||||
return (*static_cast<FilterAudioSources_t*>(data))(source);
|
||||
}, static_cast<void*>(&FilterAudioSources));
|
||||
|
||||
obs_source_t *currentScene = obs_get_output_source(0);
|
||||
obs_source_t *transition = obs_get_output_source(0);
|
||||
obs_source_t *currentScene = obs_scene_get_source(scene);
|
||||
const char *sceneName = obs_source_get_name(currentScene);
|
||||
const char *programName = obs_source_get_name(curProgramScene);
|
||||
|
||||
const char *sceneCollection = config_get_string(App()->GlobalConfig(),
|
||||
"Basic", "SceneCollection");
|
||||
|
||||
obs_data_set_string(saveData, "current_scene", sceneName);
|
||||
obs_data_set_string(saveData, "current_program_scene", programName);
|
||||
obs_data_set_array(saveData, "scene_order", sceneOrder);
|
||||
obs_data_set_string(saveData, "name", sceneCollection);
|
||||
obs_data_set_array(saveData, "sources", sourcesArray);
|
||||
obs_data_set_array(saveData, "quick_transitions", quickTransitionData);
|
||||
obs_data_array_release(sourcesArray);
|
||||
obs_source_release(currentScene);
|
||||
|
||||
obs_data_set_string(saveData, "current_transition",
|
||||
obs_source_get_name(transition));
|
||||
obs_data_set_int(saveData, "transition_duration", transitionDuration);
|
||||
obs_source_release(transition);
|
||||
|
||||
return saveData;
|
||||
}
|
||||
@@ -338,14 +346,23 @@ obs_data_array_t *OBSBasic::SaveSceneListOrder()
|
||||
|
||||
void OBSBasic::Save(const char *file)
|
||||
{
|
||||
OBSScene scene = GetCurrentScene();
|
||||
OBSSource curProgramScene = OBSGetStrongRef(programScene);
|
||||
if (!curProgramScene)
|
||||
curProgramScene = obs_scene_get_source(scene);
|
||||
|
||||
obs_data_array_t *sceneOrder = SaveSceneListOrder();
|
||||
obs_data_t *saveData = GenerateSaveData(sceneOrder);
|
||||
obs_data_array_t *quickTrData = SaveQuickTransitions();
|
||||
obs_data_t *saveData = GenerateSaveData(sceneOrder, quickTrData,
|
||||
ui->transitionDuration->value(),
|
||||
scene, curProgramScene);
|
||||
|
||||
if (!obs_data_save_json_safe(saveData, file, "tmp", "bak"))
|
||||
blog(LOG_ERROR, "Could not save scene data to %s", file);
|
||||
|
||||
obs_data_release(saveData);
|
||||
obs_data_array_release(sceneOrder);
|
||||
obs_data_array_release(quickTrData);
|
||||
}
|
||||
|
||||
static void LoadAudioDevice(const char *name, int channel, obs_data_t *parent)
|
||||
@@ -399,14 +416,18 @@ void OBSBasic::CreateDefaultScene(bool firstStart)
|
||||
disableSaving++;
|
||||
|
||||
ClearSceneData();
|
||||
InitDefaultTransitions();
|
||||
CreateDefaultQuickTransitions();
|
||||
ui->transitionDuration->setValue(300);
|
||||
SetTransition(fadeTransition);
|
||||
|
||||
obs_scene_t *scene = obs_scene_create(Str("Basic.Scene"));
|
||||
|
||||
if (firstStart)
|
||||
CreateFirstRunSources();
|
||||
|
||||
obs_set_output_source(0, obs_scene_get_source(scene));
|
||||
AddScene(obs_scene_get_source(scene));
|
||||
SetCurrentScene(scene, true);
|
||||
obs_scene_release(scene);
|
||||
|
||||
disableSaving--;
|
||||
@@ -463,11 +484,23 @@ void OBSBasic::Load(const char *file)
|
||||
}
|
||||
|
||||
ClearSceneData();
|
||||
InitDefaultTransitions();
|
||||
|
||||
obs_data_array_t *sceneOrder = obs_data_get_array(data, "scene_order");
|
||||
obs_data_array_t *sources = obs_data_get_array(data, "sources");
|
||||
const char *sceneName = obs_data_get_string(data,
|
||||
"current_scene");
|
||||
const char *programSceneName = obs_data_get_string(data,
|
||||
"current_program_scene");
|
||||
const char *transitionName = obs_data_get_string(data,
|
||||
"current_transition");
|
||||
|
||||
int newDuration = obs_data_get_int(data, "transition_duration");
|
||||
if (!newDuration)
|
||||
newDuration = 300;
|
||||
|
||||
if (!transitionName)
|
||||
transitionName = obs_source_get_name(fadeTransition);
|
||||
|
||||
const char *curSceneCollection = config_get_string(
|
||||
App()->GlobalConfig(), "Basic", "SceneCollection");
|
||||
@@ -476,6 +509,8 @@ void OBSBasic::Load(const char *file)
|
||||
|
||||
const char *name = obs_data_get_string(data, "name");
|
||||
obs_source_t *curScene;
|
||||
obs_source_t *curProgramScene;
|
||||
obs_source_t *curTransition;
|
||||
|
||||
if (!name || !*name)
|
||||
name = curSceneCollection;
|
||||
@@ -491,9 +526,25 @@ void OBSBasic::Load(const char *file)
|
||||
if (sceneOrder)
|
||||
LoadSceneListOrder(sceneOrder);
|
||||
|
||||
curTransition = FindTransition(transitionName);
|
||||
if (!curTransition)
|
||||
curTransition = fadeTransition;
|
||||
|
||||
ui->transitionDuration->setValue(newDuration);
|
||||
SetTransition(curTransition);
|
||||
|
||||
curScene = obs_get_source_by_name(sceneName);
|
||||
obs_set_output_source(0, curScene);
|
||||
curProgramScene = obs_get_source_by_name(programSceneName);
|
||||
if (!curProgramScene) {
|
||||
curProgramScene = curScene;
|
||||
obs_source_addref(curScene);
|
||||
}
|
||||
|
||||
SetCurrentScene(curScene, true);
|
||||
if (IsPreviewProgramMode())
|
||||
TransitionToScene(curProgramScene, true);
|
||||
obs_source_release(curScene);
|
||||
obs_source_release(curProgramScene);
|
||||
|
||||
obs_data_array_release(sources);
|
||||
obs_data_array_release(sceneOrder);
|
||||
@@ -506,6 +557,13 @@ void OBSBasic::Load(const char *file)
|
||||
config_set_string(App()->GlobalConfig(), "Basic", "SceneCollectionFile",
|
||||
file_base.c_str());
|
||||
|
||||
obs_data_array_t *quickTransitionData = obs_data_get_array(data,
|
||||
"quick_transitions");
|
||||
LoadQuickTransitions(quickTransitionData);
|
||||
obs_data_array_release(quickTransitionData);
|
||||
|
||||
RefreshQuickTransitions();
|
||||
|
||||
obs_data_release(data);
|
||||
|
||||
disableSaving--;
|
||||
@@ -772,8 +830,6 @@ void OBSBasic::InitOBSCallbacks()
|
||||
OBSBasic::SourceLoaded, this);
|
||||
signalHandlers.emplace_back(obs_get_signal_handler(), "source_remove",
|
||||
OBSBasic::SourceRemoved, this);
|
||||
signalHandlers.emplace_back(obs_get_signal_handler(), "channel_change",
|
||||
OBSBasic::ChannelChanged, this);
|
||||
signalHandlers.emplace_back(obs_get_signal_handler(), "source_activate",
|
||||
OBSBasic::SourceActivated, this);
|
||||
signalHandlers.emplace_back(obs_get_signal_handler(), "source_deactivate",
|
||||
@@ -885,6 +941,15 @@ void OBSBasic::OBSInit()
|
||||
|
||||
InitPrimitives();
|
||||
|
||||
sceneDuplicationMode = config_get_bool(App()->GlobalConfig(),
|
||||
"BasicWindow", "SceneDuplicationMode");
|
||||
swapScenesMode = config_get_bool(App()->GlobalConfig(),
|
||||
"BasicWindow", "SwapScenesMode");
|
||||
editPropertiesMode = config_get_bool(App()->GlobalConfig(),
|
||||
"BasicWindow", "EditPropertiesMode");
|
||||
SetPreviewProgramMode(config_get_bool(App()->GlobalConfig(),
|
||||
"BasicWindow", "PreviewProgramMode"));
|
||||
|
||||
{
|
||||
ProfileScope("OBSBasic::Load");
|
||||
disableSaving--;
|
||||
@@ -895,11 +960,13 @@ void OBSBasic::OBSInit()
|
||||
TimedCheckForUpdates();
|
||||
loaded = true;
|
||||
|
||||
bool previewEnabled = config_get_bool(App()->GlobalConfig(),
|
||||
previewEnabled = config_get_bool(App()->GlobalConfig(),
|
||||
"BasicWindow", "PreviewEnabled");
|
||||
if (!previewEnabled)
|
||||
QMetaObject::invokeMethod(this, "TogglePreview",
|
||||
Qt::QueuedConnection);
|
||||
|
||||
if (!previewEnabled && !IsPreviewProgramMode())
|
||||
QMetaObject::invokeMethod(this, "EnablePreviewDisplay",
|
||||
Qt::QueuedConnection,
|
||||
Q_ARG(bool, previewEnabled));
|
||||
|
||||
#ifdef _WIN32
|
||||
uint32_t winVer = GetWindowsVersion();
|
||||
@@ -1121,8 +1188,36 @@ void OBSBasic::CreateHotkeys()
|
||||
this, this);
|
||||
LoadHotkeyPair(recordingHotkeys,
|
||||
"OBSBasic.StartRecording", "OBSBasic.StopRecording");
|
||||
|
||||
#undef MAKE_CALLBACK
|
||||
|
||||
auto togglePreviewProgram = [] (void *data, obs_hotkey_id,
|
||||
obs_hotkey_t*, bool pressed)
|
||||
{
|
||||
if (pressed)
|
||||
QMetaObject::invokeMethod(static_cast<OBSBasic*>(data),
|
||||
"on_modeSwitch_clicked",
|
||||
Qt::QueuedConnection);
|
||||
};
|
||||
|
||||
togglePreviewProgramHotkey = obs_hotkey_register_frontend(
|
||||
"OBSBasic.TogglePreviewProgram",
|
||||
Str("Basic.TogglePreviewProgramMode"),
|
||||
togglePreviewProgram, this);
|
||||
LoadHotkey(togglePreviewProgramHotkey, "OBSBasic.TogglePreviewProgram");
|
||||
|
||||
auto transition = [] (void *data, obs_hotkey_id, obs_hotkey_t*,
|
||||
bool pressed)
|
||||
{
|
||||
if (pressed)
|
||||
QMetaObject::invokeMethod(static_cast<OBSBasic*>(data),
|
||||
"TransitionClicked",
|
||||
Qt::QueuedConnection);
|
||||
};
|
||||
|
||||
transitionHotkey = obs_hotkey_register_frontend(
|
||||
"OBSBasic.Transition",
|
||||
Str("Transition"), transition, this);
|
||||
LoadHotkey(transitionHotkey, "OBSBasic.Transition");
|
||||
}
|
||||
|
||||
void OBSBasic::ClearHotkeys()
|
||||
@@ -1130,11 +1225,14 @@ void OBSBasic::ClearHotkeys()
|
||||
obs_hotkey_pair_unregister(streamingHotkeys);
|
||||
obs_hotkey_pair_unregister(recordingHotkeys);
|
||||
obs_hotkey_unregister(forceStreamingStopHotkey);
|
||||
obs_hotkey_unregister(togglePreviewProgramHotkey);
|
||||
obs_hotkey_unregister(transitionHotkey);
|
||||
}
|
||||
|
||||
OBSBasic::~OBSBasic()
|
||||
{
|
||||
bool previewEnabled = obs_display_enabled(ui->preview->GetDisplay());
|
||||
delete programOptions;
|
||||
delete program;
|
||||
|
||||
/* XXX: any obs data must be released before calling obs_shutdown.
|
||||
* currently, we can't automate this with C++ RAII because of the
|
||||
@@ -1206,6 +1304,14 @@ OBSBasic::~OBSBasic()
|
||||
previewEnabled);
|
||||
config_set_bool(App()->GlobalConfig(), "BasicWindow", "AlwaysOnTop",
|
||||
alwaysOnTop);
|
||||
config_set_bool(App()->GlobalConfig(), "BasicWindow",
|
||||
"SceneDuplicationMode", sceneDuplicationMode);
|
||||
config_set_bool(App()->GlobalConfig(), "BasicWindow",
|
||||
"SwapScenesMode", swapScenesMode);
|
||||
config_set_bool(App()->GlobalConfig(), "BasicWindow",
|
||||
"EditPropertiesMode", editPropertiesMode);
|
||||
config_set_bool(App()->GlobalConfig(), "BasicWindow",
|
||||
"PreviewProgramMode", IsPreviewProgramMode());
|
||||
config_save_safe(App()->GlobalConfig(), "tmp", nullptr);
|
||||
|
||||
#ifdef _WIN32
|
||||
@@ -1358,10 +1464,13 @@ void OBSBasic::AddScene(OBSSource source)
|
||||
[](void *data,
|
||||
obs_hotkey_id, obs_hotkey_t*, bool pressed)
|
||||
{
|
||||
OBSBasic *main =
|
||||
reinterpret_cast<OBSBasic*>(App()->GetMainWindow());
|
||||
|
||||
auto potential_source = static_cast<obs_source_t*>(data);
|
||||
auto source = obs_source_get_ref(potential_source);
|
||||
if (source && pressed)
|
||||
obs_set_output_source(0, source);
|
||||
main->SetCurrentScene(source);
|
||||
obs_source_release(source);
|
||||
}, static_cast<obs_source_t*>(source));
|
||||
|
||||
@@ -1771,10 +1880,10 @@ void OBSBasic::DuplicateSelectedScene()
|
||||
obs_scene_t *scene = obs_scene_duplicate(curScene,
|
||||
name.c_str(), OBS_SCENE_DUP_REFS);
|
||||
source = obs_scene_get_source(scene);
|
||||
AddScene(source);
|
||||
SetCurrentScene(source, true);
|
||||
obs_scene_release(scene);
|
||||
|
||||
obs_set_output_source(0, source);
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1967,17 +2076,6 @@ void OBSBasic::SourceRenamed(void *data, calldata_t *params)
|
||||
Q_ARG(QString, QT_UTF8(prevName)));
|
||||
}
|
||||
|
||||
void OBSBasic::ChannelChanged(void *data, calldata_t *params)
|
||||
{
|
||||
obs_source_t *source = (obs_source_t*)calldata_ptr(params, "source");
|
||||
uint32_t channel = (uint32_t)calldata_int(params, "channel");
|
||||
|
||||
if (channel == 0)
|
||||
QMetaObject::invokeMethod(static_cast<OBSBasic*>(data),
|
||||
"UpdateSceneSelection",
|
||||
Q_ARG(OBSSource, OBSSource(source)));
|
||||
}
|
||||
|
||||
void OBSBasic::DrawBackdrop(float cx, float cy)
|
||||
{
|
||||
if (!box)
|
||||
@@ -2029,7 +2127,14 @@ void OBSBasic::RenderMain(void *data, uint32_t cx, uint32_t cy)
|
||||
|
||||
window->DrawBackdrop(float(ovi.base_width), float(ovi.base_height));
|
||||
|
||||
obs_render_main_view();
|
||||
if (window->IsPreviewProgramMode()) {
|
||||
OBSScene scene = window->GetCurrentScene();
|
||||
obs_source_t *source = obs_scene_get_source(scene);
|
||||
if (source)
|
||||
obs_source_video_render(source);
|
||||
} else {
|
||||
obs_render_main_view();
|
||||
}
|
||||
gs_load_vertexbuffer(nullptr);
|
||||
|
||||
/* --------------------------------------- */
|
||||
@@ -2267,6 +2372,8 @@ void OBSBasic::ClearSceneData()
|
||||
ClearVolumeControls();
|
||||
ClearListItems(ui->scenes);
|
||||
ClearListItems(ui->sources);
|
||||
ClearQuickTransitions();
|
||||
ui->transitions->clear();
|
||||
|
||||
obs_set_output_source(0, nullptr);
|
||||
obs_set_output_source(1, nullptr);
|
||||
@@ -2274,6 +2381,9 @@ void OBSBasic::ClearSceneData()
|
||||
obs_set_output_source(3, nullptr);
|
||||
obs_set_output_source(4, nullptr);
|
||||
obs_set_output_source(5, nullptr);
|
||||
lastScene = nullptr;
|
||||
swapScene = nullptr;
|
||||
programScene = nullptr;
|
||||
|
||||
auto cb = [](void *unused, obs_source_t *source)
|
||||
{
|
||||
@@ -2393,8 +2503,7 @@ void OBSBasic::on_scenes_currentItemChanged(QListWidgetItem *current,
|
||||
source = obs_scene_get_source(scene);
|
||||
}
|
||||
|
||||
/* TODO: allow transitions */
|
||||
obs_set_output_source(0, source);
|
||||
SetCurrentScene(source);
|
||||
|
||||
UNUSED_PARAMETER(prev);
|
||||
}
|
||||
@@ -2519,7 +2628,7 @@ void OBSBasic::on_actionAddScene_triggered()
|
||||
obs_scene_t *scene = obs_scene_create(name.c_str());
|
||||
source = obs_scene_get_source(scene);
|
||||
AddScene(source);
|
||||
obs_set_output_source(0, source);
|
||||
SetCurrentScene(source);
|
||||
obs_scene_release(scene);
|
||||
}
|
||||
}
|
||||
@@ -2627,6 +2736,8 @@ void OBSBasic::CreateSourcePopupMenu(QListWidgetItem *item, bool preview)
|
||||
action->setCheckable(true);
|
||||
action->setChecked(
|
||||
obs_display_enabled(ui->preview->GetDisplay()));
|
||||
if (IsPreviewProgramMode())
|
||||
action->setEnabled(false);
|
||||
|
||||
previewProjector = new QMenu(QTStr("PreviewProjector"));
|
||||
AddProjectorMenuMonitors(previewProjector, this,
|
||||
@@ -3694,12 +3805,17 @@ void OBSBasic::on_actionCenterToScreen_triggered()
|
||||
obs_scene_enum_items(GetCurrentScene(), func, nullptr);
|
||||
}
|
||||
|
||||
void OBSBasic::EnablePreviewDisplay(bool enable)
|
||||
{
|
||||
obs_display_set_enabled(ui->preview->GetDisplay(), enable);
|
||||
ui->preview->setVisible(enable);
|
||||
ui->previewDisabledLabel->setVisible(!enable);
|
||||
}
|
||||
|
||||
void OBSBasic::TogglePreview()
|
||||
{
|
||||
bool enabled = !obs_display_enabled(ui->preview->GetDisplay());
|
||||
obs_display_set_enabled(ui->preview->GetDisplay(), enabled);
|
||||
ui->preview->setVisible(enabled);
|
||||
ui->previewDisabledLabel->setVisible(!enabled);
|
||||
previewEnabled = !previewEnabled;
|
||||
EnablePreviewDisplay(previewEnabled);
|
||||
}
|
||||
|
||||
void OBSBasic::Nudge(int dist, MoveDir dir)
|
||||
@@ -3791,7 +3907,11 @@ void OBSBasic::UpdateTitleBar()
|
||||
const char *sceneCollection = config_get_string(App()->GlobalConfig(),
|
||||
"Basic", "SceneCollection");
|
||||
|
||||
name << "OBS " << App()->GetVersionString();
|
||||
name << "OBS ";
|
||||
if (previewProgramMode)
|
||||
name << "Studio ";
|
||||
|
||||
name << App()->GetVersionString();
|
||||
name << " - " << Str("TitleBar.Profile") << ": " << profile;
|
||||
name << " - " << Str("TitleBar.Scenes") << ": " << sceneCollection;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user