andreykaipov / goobs

Go client library for OBS Studio
Apache License 2.0
137 stars 22 forks source link

More examples please and how do you select a source in a scene correctly? #52

Closed SonnyWalkman closed 11 months ago

SonnyWalkman commented 1 year ago

I'm having difficulty using the library.

//SetScene
    myscene := "TowerCam"
    n, err := client.Scenes.SetCurrentProgramScene(&scenes.SetCurrentProgramSceneParams{
        SceneName: myscene,
    })
    if err != nil {
        logger.Fatalf("šŸ›‘ %s", err)
        fmt.Println(n)
        return
    } else {
        logger.Infof("Scene %s selected\n", myscene)
    }

Works fine for scene

How do you properly select a source within a scene? Below pulls no errors however, the source isn't selected. What is the need for SceneItemIndex: 3, SceneItemId: 3, SceneName: "MyScene"? Any combination breaks.

g, err := client.SceneItems.GetSceneItemId(&sceneitems.GetSceneItemIdParams{SourceName: "SMPTE-GS", SceneName: "TestCharts"})
    if err != nil {
        logger.Fatalf("šŸ›‘ %s", err)
        fmt.Println(g)
        return
    } else {
        fmt.Println(g)
    }
    //SetSource 1234
    mysource := "1234"
    d, err := client.SceneItems.SetSceneItemIndex(&sceneitems.SetSceneItemIndexParams{
        SceneItemIndex: 3,
        SceneItemId:    3,
        SceneName:      "MyScene",
    })
    if err != nil {
        logger.Fatalf("šŸ›‘ %s", err)
        fmt.Println(d)
        return
    } else {
        logger.Infof("Source %s selected\n", mysource)
        fmt.Println(d)
    }

Is there a method to shut OBS down? Apparently OBS can be closed via websockets?

andreykaipov commented 1 year ago

Hi thanks for your questions!

How do you properly select a source within a scene?

Not sure I entirely understand. Since we're programmatically interacting with OBS, there's no need to "select" a scene. Instead we specify the scene and how we want to interact with it.

Below pulls no errors however, the source isn't selected. What is the need for SceneItemIndex: 3, SceneItemId: 3, SceneName: "MyScene"? Any combination breaks.

I added another example that creates some sources in a scene and then reverses them at https://github.com/andreykaipov/goobs/blob/master/examples/sceneitems/main.go. It uses the SetSceneItemIndex request you tried. The idea is to first iterate over all the scene items to figure out what IDs and indexes to use.

sceneitems


Below is some general advice.

The way I usually use this library is by first looking through the obs-websocket protocol docs to see all the available requests and if any involve what I wanna do. This library is generated from those docs so all requests, events, parameters, documentation are practically 1-1. https://github.com/obsproject/obs-websocket/blob/5.1.0/docs/generated/protocol.md

Any time I don't know what parameters to use for some request, I listen for the client events and inspect the logs. For example in the above example - dshow_input corresponds to the Video Capture Device source, and the way I found this was by running go run examples/sources/main.go, creating a Video Capture Device source manually, and finding the following log:

2023/09/05 19:36:05 Unhandled: &events.InputCreated{DefaultInputSettings:map[string]interface {}{"active":true, "audio_output_mode":0, "autorotation":true, "color_range":"default", "color_space":"default", "frame_interval":-1, "hw_decode":false, "res_type":0, "video_format":0}, InputKind:"dshow_input", InputName:"Video Capture Device", InputSettings:map[string]interface {}{}, UnversionedInputKind:"dshow_input"}
2023/09/05 19:36:05 Unhandled: &events.SceneItemSelected{SceneItemId:39, SceneName:"Scene"}
2023/09/05 19:36:05 Unhandled: &events.SceneItemCreated{SceneItemId:39, SceneItemIndex:0, SceneName:"Scene", SourceName:"Video Capture Device"}

Specifically - InputKind:"dshow_input". Hope this helps!

andreykaipov commented 1 year ago

Is there a method to shut OBS down? Apparently OBS can be closed via websockets?

I don't think so. There's this open feature request https://ideas.obsproject.com/posts/2225/allow-clean-shutdown-via-api which links to the currently open PR https://github.com/obsproject/obs-studio/pull/8889. So the functionality is not even in OBS itself.

Depending on what you wanna do, maybe you could kill the program externally and listen for the ExitStarted event like in https://github.com/andreykaipov/goobs/blob/v0.12.0/examples/sources/main.go#L28-L31?

SonnyWalkman commented 1 year ago

@andreykaipov thanks for your prompt reply.

I'm using obs studio as a remote switcher (minimised no need to see UI) I switch scenes and sources in a scenes. My go app starts obs studio by executing a external script. Running a internal call directly to start obs64.exe (windows) and also for linux seems to not start clean and correctly load configuration. The script method works properly as a work around. I believe it to do with the path and how the run path relates to where the ini and json files are called?

Below pulls no errors however, the source isn't selected. What is the need for SceneItemIndex: 3, SceneItemId: 3, SceneName: "MyScene"? Any combination breaks.

After spending a few hours, I managed to get source switching working using a 'for range' and iterating through the source list matching the source name then enabling the source I wanted to show with disabling the other sources in that scene.

I'll post the code here shortly to let others know how to do so for additional help.

As far as shutting down obs, I've tried killing its pid however doesn't shut down cleanly since obs spawns children threads which need to kill in a specific sequence. Far to dirty IMHO. The Exit button on obs UI is the best approach to shutdown obs however, seems there is yet to be a WebSocket method to do such.

I've request to add the shutdown method in the next release on the obs forums however, I'm not sure if it be a priority for the obs team? Time will tell.

Sorry for my incorrect statement?

Is there a method to shut OBS down? Apparently OBS can be closed via websockets?

There is a plugin to shut down obs which interacts with the obs api directly. https://github.com/norihiro/obs-shutdown-plugin/ not sure if the shutdown can be added to register a callback in your library ?

if (!obs_websocket_vendor_register_request(vendor, "shutdown", shutdown_callback, NULL)) {
        blog(LOG_ERROR, "Failed to register 'shutdown' request with obs-websocket.");
        return;
    }

static void shutdown_callback(obs_data_t *request_data, obs_data_t *response_data, void *priv_data)
{
    UNUSED_PARAMETER(priv_data);
    UNUSED_PARAMETER(response_data); // TDOO: Implement
    blog(LOG_INFO, "shutdown is called...");

    const char *reason = obs_data_get_string(request_data, "reason");
    if (!reason || strlen(reason) < MIN_REASON) {
        blog(LOG_ERROR, "shutdown requires reason with at least 8 characters");
        return;
    }

    const char *support_url = obs_data_get_string(request_data, "support_url");
    if (!support_url) {
        blog(LOG_ERROR, "shutdown requires support_url pointing a valid URL.");
        return;
    }

    const auto main_window = static_cast<QMainWindow *>(obs_frontend_get_main_window());
    if (!main_window) {
        blog(LOG_ERROR, "main_window is %p", main_window);
        return;
    }

    if (obs_data_get_bool(request_data, "force")) {
        bool confirmOnExit = config_get_bool(obs_frontend_get_global_config(), "General", "ConfirmOnExit");
        if (confirmOnExit) {
            blog(LOG_INFO, "Temporarily setting General/ConfirmOnExit to false");
            obs_frontend_add_event_callback(revert_confirmOnExit_at_exit, NULL);
            config_set_bool(obs_frontend_get_global_config(), "General", "ConfirmOnExit", false);
        }
        // TODO: Remux
    }

    blog(LOG_INFO, "Shutdown obs-studio with reason: %s", reason);
    blog(LOG_INFO, "If you need support, visit %s", support_url);

    QMetaObject::invokeMethod(main_window, "close", Qt::QueuedConnection);
}

Be nice to have the Shutdown properly added to the Websocket and add it to your library. I see there is already a method stub ready to go.

andreykaipov commented 1 year ago

There is a plugin to shut down obs which interacts with the obs api directly. https://github.com/norihiro/obs-shutdown-plugin/

Ah I see, in the forum thread, that plugin was created as a workaround until https://github.com/obsproject/obs-studio/pull/8889 and https://github.com/obsproject/obs-websocket/pull/1138 are both in.

It has an example which uses CallVendorRequest. We can do the same thing!

    _, err := client.General.CallVendorRequest(&general.CallVendorRequestParams{
        VendorName:  "obs-shutdown-plugin",
        RequestType: "shutdown",
        RequestData: map[string]interface{}{
            "reason":      "cleaning up",
            "support_url": "https://github.com/norihiro/obs-shutdown-plugin/issues",
            "force":       true,
        },
    })

You'd have to first install that plugin by copying its release into your OBS directory though. https://github.com/norihiro/obs-shutdown-plugin/releases

SonnyWalkman commented 1 year ago

Hello @andreykaipov, Thanks for the valuable information.. I'll use the above code to buy time until the Shutdown method is added to the obs websockets API.

Below is my snippet of test code which I've put together to select a specific source within a Scene. Thought it could help others using the library. I do a getSceneList query and map to use for a local copy than having to websockets returning all the Scenes. Same principle can be extended to take a snapshot of all Sources per Scene.

// map obs scenes and TestChart sources
    sceneMap := make(map[int]string)
    sourceMap := make(map[int]string)

    resp, _ := client.Scenes.GetSceneList()
    for _, v := range resp.Scenes {
        sceneMap[v.SceneIndex] = v.SceneName
        //fmt.Printf(" Scene -  %2d %s\n", v.SceneIndex, v.SceneName)
    }

    //SetScene MyScene
    myscene := "MyScene"
    n, err := client.Scenes.SetCurrentProgramScene(&scenes.SetCurrentProgramSceneParams{
        SceneName: myscene,
    })
    if err != nil {
        log.Fatalf("šŸ›‘ %s", err)
        fmt.Println(n)
        return
    } else {
        log.Infof("šŸ–µ  Scene %s selected", myscene)
    }

    time.Sleep(2 * time.Second)

    // First, get the scene items (sources) in your scene

    // Fetch sources for the Scene name "MyScene"
    sceneItems, _ := client.SceneItems.GetSceneItemList(&sceneitems.GetSceneItemListParams{SceneName: "MyScene"})
    if err != nil {
        log.Fatalf("šŸ›‘ Could not get scene items: %s", err)
        return
    }
    for idx, v := range sceneItems.SceneItems {
        sourceMap[idx] = v.SourceName
        //fmt.Printf("Scene sources  - %2d %s\n", idx, v.SourceName)
    }

    // Next, loop through the scene items and set visibility
    enabled := true
    disabled := false
    for _, item := range sceneItems.SceneItems {
        if item.SourceName == "MySource" {
            log.Infof("šŸ–µ  Source %s selected", item.SourceName)
            // Set this source to visible
            _, err := client.SceneItems.SetSceneItemEnabled(&sceneitems.SetSceneItemEnabledParams{
                SceneItemEnabled: &enabled, // Set to true to enable
                SceneItemId:      float64(item.SceneItemID),
                SceneName:        "TMyScene",
            })
            if err != nil {
                log.Errorf("šŸ›‘ Error setting %s to visible: %s", item.SourceName, err)
            }
        } else {
            // Set all other sources to invisible
            _, err := client.SceneItems.SetSceneItemEnabled(&sceneitems.SetSceneItemEnabledParams{
                SceneItemEnabled: &disabled, // Set to false to disable
                SceneItemId:      float64(item.SceneItemID),
                SceneName:        "MyScene",
            })
            if err != nil {
                log.Errorf("šŸ›‘ Error setting %s to non-visible: %s", item.SourceName, err)
            }
        }
    }
andreykaipov commented 11 months ago

Since this issue I've added a few more examples to the repo and added a "dev walkthrough" of sorts to https://github.com/andreykaipov/goobs/tree/main/docs#making-requests that hopefully provides some techniques for figuring out things like this for any folks from the future :)