Skip to content

add websocket servers for amcp and monitor data #1654

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

TomKaltz
Copy link
Member

@TomKaltz TomKaltz commented Jul 4, 2025

WIP FEEDBACK NEEDED

implement websocket server using boost beast for AMCP and monitoring with RFC7386 JSON MERGE PATCH change deltas.

Exposes 2 websocket ports…One for AMCP(5251)…the other port for monitoring(5252). When a client connects to the monitor websocket, a welcome message is sent to the client...
When you first connect, you'll receive a welcome message with instructions:

{
  "commands": {
    "request_full_state": {
      "description": "Get a one-time snapshot of all current monitor data",
      "example": {
        "command": "request_full_state"
      }
    },
    "subscribe": {
      "description": "Set up ongoing filtered monitor data subscription",
      "example": {
        "command": "subscribe",
        "exclude": [
          "channel/*/mixer/audio/*"
        ],
        "include": [
          "channel/1/*",
          "channel/*/mixer/audio/volume"
        ]
      }
    }
  },
  "connection_id": "monitor_1",
  "important_note": "Use '*' to subscribe to all monitor data (full firehose). Be careful with bandwidth!",
  "instructions": "Send a subscription command to start receiving monitor data, or request_full_state for a one-time snapshot",
  "message": "Welcome to CasparCG Monitor WebSocket",
  "supported_patterns": {
    "alternation": [
      "[1|3|5] (specific single digits)",
      "[90|100|150] (specific numbers)",
      "[abc] (specific characters)"
    ],
    "array_indexing": [
      "[0] (single element)",
      "[0:2] (range slice)",
      "[*] (all elements)"
    ],
    "character_ranges": [
      "[a-z] (lowercase letters)",
      "[A-Z] (uppercase letters)"
    ],
    "examples": [
      "* - Full state firehose (all monitor data)",
      "channel/1/* - All channel 1 data",
      "channel/*/mixer/* - All mixer data",
      "channel/[1-9]/* - Single digit channels (1-9)",
      "channel/[1-20]/* - Channels 1 through 20",
      "channel/[1|3|5]/* - Specific channels (1, 3, and 5 only)",
      "channel/*/stage/layer/[90|100|150]/* - Specific layers only",
      "channel/[!1-5]/* - All channels except 1-5",
      "channel/*/mixer/volume[0:2] - First 2 audio channels only",
      "channel/1/mixer/volume[0] - First audio channel from channel 1",
      "channel/*/stage/framerate - Framerate from all channels",
      "channel/1/mixer/volume - All volume data from channel 1"
    ],
    "number_ranges": [
      "[1-9] (single digits)",
      "[1-20] (numbers 1 through 20)",
      "[!1-5] (not 1-5)"
    ],
    "wildcards": [
      "* (any sequence)"
    ]
  },
  "timestamp": 1752211405834,
  "type": "welcome"
}

after the client sends the subscribe command payload they should receive a confirmation then the filtered subscribed data will start streaming at the fastest channel fps. Please note that a subscribe command is NOT additive. It will overwrite the previous subscription include and exclude arrays on the server.

Streaming data comes with message type filtered_state.

{
    "type": "filtered_state",
    "timestamp": 1640995200000,
    "data": {
        "channel": {
            "1": {
                "stage": {
                    "framerate": [50, 1]
                },
                "mixer": {
                    "audio": {
                        "volume": [0.8, 0.7, 0.6]
                    }
                }
            }
        }
    }
}

The client can request a full_state message by sending {“command”:”request_full_state”} then a one-time message of type full_state will be sent with all unfiltered server monitor data.

Note: This PR will not implement WSS so standard web browsers will NOT allow a non-secure websocket connection to the server across a network. Browsers can still connect if the CasparCG Servver is on localhost. This allows the use case of an [html] template connecting to the websocket server over websockets for control and/or realtime status data.

This was referenced Jul 4, 2025
@MauriceVeraart
Copy link
Contributor

I think this is very use full. What changes does it send. does it send continuous data like audio levels and clip position? if so may be a 3rd socket is needed as for some applications you only need layer states and not the potential loud data. But always better then the bulky / loud OSC that we have now.

@Julusian
Copy link
Member

Julusian commented Jul 5, 2025

I haven't read the code or thought about this much yet, but a couple of ideas:

Perhaps this should operate in a way of letting the client say what they want to listen to (and perhaps potentially the format of the data?)
That way a client can listen to only the channels it is interested in, and optionally turn on/off noisier messages. I don't know how granular this should be, there are a few ways this could be approached.
In a follow up change it could allow for opt into the full diag data needed for building the diag window.

And while I like that this is using json-patch, I wonder if that might be frustrating for some uses because of the extra complexity needed (while this works well for something like js, I dont know how well supported it is in other languages). so it would be nice to be able to specify this, perhaps as part of some 'subscribe' calls the client makes. json-patch can be the default though.

@TomKaltz
Copy link
Member Author

TomKaltz commented Jul 5, 2025

@MauriceVeraart currently the full state of the server (all OSC paths) is sent upon connection then an update is sent per frame with any change deltas.

@Julusian I do believe we need a subscription system for the monitor connections although currently the JSON patch implementation really cuts down on bandwidth. The one thing I like immensely about the JSON patch mechanism is that we can now know exactly when a stage goes empty because we’ll get a remove op for that path. That has been the most awkward part of the OSC system for me.

JSON patch seems to have decent community support in most languages. OSC is awkward enough, I’m not sure we need to worry about taking a stance on sending change deltas in a specified format. There is RFC 7386 JSON Merge Patch that might be less awkward and still provide the same removal functionality. I will look into that.

I will work on implementing a wildcard enabled path subscription system with a default subscription configurable on the server.

We’ll give the user control over these options…but the big question is what will the defaults look like? Do we send nothing and require subscription command/s for paths we are interested in? Do we always send full state of all subscribed paths on every frame or send full state then subsequent change deltas?

@TomKaltz
Copy link
Member Author

TomKaltz commented Jul 5, 2025

I've now switched the change deltas to come as RFC7386 format which are less awkward to clients. The user can implement their own merge strategy quite easily in their language if it is not supported.

Copy link
Member

@ronag ronag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Though I think the patching is not strictly necessary. It's not so much data. Why not just always send the whole state and the server diff and apply changes?

@ronag
Copy link
Member

ronag commented Jul 9, 2025

We’ll give the user control over these options…but the big question is what will the defaults look like? Do we send nothing and require subscription command/s for paths we are interested in? Do we always send full state of all subscribed paths on every frame or send full state then subsequent change deltas?

I think you need to explicitly subscribe to paths + some kind of wildcard + update interval, e.g. subscribe('/channel/2/*/data', 100)

@ronag
Copy link
Member

ronag commented Jul 9, 2025

Another thing. Add a server timestamp to every update.

@TomKaltz
Copy link
Member Author

TomKaltz commented Jul 9, 2025 via email

@TomKaltz
Copy link
Member Author

Path based subscriptions implemented. See PR description.

@TomKaltz TomKaltz marked this pull request as ready for review July 11, 2025 19:04
@TomKaltz TomKaltz requested a review from Julusian July 11, 2025 19:04
@TomKaltz TomKaltz marked this pull request as draft July 12, 2025 11:53
@TomKaltz
Copy link
Member Author

This isn't ready, there are performance issues. Marking as draft for now.

@dimitry-ishenko
Copy link
Contributor

@TomKaltz just curious, is there a problem with using OSC?

@TomKaltz
Copy link
Member Author

@TomKaltz just curious, is there a problem with using OSC?

@dimitry-ishenko I've used OSC very reliably for many years with great success but there were things that always bothered me. One big issue I have is that you can't immediately definitively know when a stage is empty/cleared since that stage will just stop producing OSC messages. This required awkward checks with timeouts to detect when a stage/layer stopped producing data. With this implementation, as long as you aren't using a wildcard for the layer subscriptions, the layer will transmit producer: "empty" immediately when a stage/layer is cleared.

The advantages I see to adding websockets are as follows...

  • more reliable tcp transport (udp data delivery is not guaranteed)
  • immediate cleared/empty stage status when subscribing to specific layers
  • support for control and realtime status for web browser clients (or html cg templates)
  • bandwidth reduction due to optional per message compression and/or path subscription model

I'm hoping hoping this PR will add value to CasparCG Server. I always suspected it would. I will be testing it heavily with a client project I'm working on right now so I hope to get it as bullet-proof as possible ASAP.

@sirfnomi
Copy link

Hi @TomKaltz,

First of all, thank you for your work on this pull request!

I have a question regarding the scope of the information that will be exposed via this new WebSocket. Beyond general server status, would it be possible to include more granular internal state information for individual layer? Specifically, I'm referring to states such as:

  • Loading: When a clip is being loaded into server.
  • Loaded: When a clip is loaded into server and ready to play.
  • Playing: When a clip is actively being played out.
  • Paused: If a clip is currently paused.
  • Stopped: When a clip has finished playing or has been explicitly stopped.

Currently, when developing clients that interact with CasparCG, determining the precise state of a clip using OSC is quite challenging. We often have to infer a clip's state through a combination of its name, time, and duration, and then manage multiple conditional checks on the client-side. This approach is prone to inaccuracies and adds significant complexity to client development. This is especially problematic when replaying the same clip or play another clip on same layer, as the server often initially sends outdated time and duration information for a brief period before providing the actual, current details of the clip.

Exposing explicit clip states via the WebSocket would greatly simplify client-side logic and enable more robust and responsive UIs. It would eliminate the need for clients to "guess" the state and instead allow them to react directly to definitive state changes from the server.

Is this something that you've considered, or do you think it would be feasible to incorporate into this WebSocket implementation?

Thanks again for your efforts!

@TomKaltz
Copy link
Member Author

@sirfnomi this PR IMHO starts the journey toward less awkward realtime state. Though, given how the core of CasparCG server is architected, it's not entirely trivial to generically derive and distribute exact playback state for producers in the system. Please open another issue to start a discussion about a better semantic for playback/layer statuses.

@TomKaltz
Copy link
Member Author

Also @sirfnomi currently the websocket implementation has access to any path/value that is currently being output to OSC. The idea though is to add more diagnostic information to the status monitor system and to move away from the old DIAG window. We'll have to think carefully about this because currently all paths are output to OSC without filtering so we might not be able to add too much unless we add filtering capability to the OSC broadcasts. It will be an ongoing disucssion as we migrate to more modern realtime status mechanism.

@TomKaltz TomKaltz marked this pull request as ready for review July 14, 2025 17:04
@sirfnomi
Copy link

That makes perfect sense, and I appreciate your candid explanation regarding the architectural challenges. I completely understand if this specific feature isn't trivial to integrate into the system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants