Skip to content

Let's Talk About StreamElements

First up, I'd like to be clear... I moved from StreamLabs to StreamElements because I felt it offered a better overall product offering. This is not a complaint about the portion of their product that most people will use (i.e. the overlays, the store, merch etc.) but I do have a big gripe about the API, specifically the lack of good, clear supporting documentation.

So, if you've tried to engage with their API and have been struggling, read on as there may be some nuggets here that will help you. This is by no means a comprehensive reference, but it is a rundown of the issues I've faced and overcome in writing my StreamElements interface for Delphi.

First up, lets clarify their setup... the API consists of two elements, the HTTP based request processing side you can use to control shed loads of elements of your StreamElements configuration and the realtime event service you can use to look for things like loyalty point store redemptions.

They do have OAuth functionality, but I have thus far been unable to register for this, so I'm relying on the JWTToken authentication approach for talking to both parts of the API.

As best as I can tell, the request service is a straight forward HTTP server. The realtime service on the other hand uses a Socket.IO server, specifically a Socket.IO protocol version 2 server, so if you're using Node.js or Javascript, don't just blindly install the Socket.IO client and if you're using another language make sure your client can talk to version 2. For Delphi I'm using the excellent WebSockets component set from ESEGECE (more on that below).

For more details about connecting and authenticating using the JWTToken approach, check out this post.

Using the request service is very straight forward, simply add a HTTP header 'Authorization: Bearer <YOURJWTTOKEN>' and you're good to go. However, the requests themselves... it's a bit tricky understanding their documentation.

My first act was to enable/disable some points store items. With the update request (PUT /store/{channel}/items/{itemId}), the request body is simply shown as an object which I took to imply I could simply send the fields I was updating. You must also send at least the name and description fields. I know this because I sent only the enabled field and I was told it was a bad request and that I should include the aforementioned fields, so I modified my client to include the name and description and whilst this worked in that items were enabled/disabled as required, their cost was also reset to 0. As best as I can tell, this is the only piece of data that was defaulted, nothing else was changed, the same applies I believe to the child objects. I do not send them and they do not appear to be being modified. So, some clarity on this would greatly enhance the documentation.

I've not engaged much with the request service at the moment as enabling/disabling store items was my primary goal, but I have no doubt this service is littered with similarly opaque behaviour.

Sadly the realtime service's issues don't end with their choice of Socket.IO... the documentation is practically non-existent (or at least I've not been able to find any form of clear information that explains exactly what gets sent and when), so if you're looking to hook some of this for live interactions (I use store redemptions to trigger web cam effects) you may want to read on.

Let's start with the events you can expect to receive... if like me you elected to emulate some of the requests you want to get using the overlay editor, the actual events you'll receive are different to those raised by the emulator.

This is a sample of the data containing the events raised by the emulator (as far as I can remember this is the data that accompanied a collection of event:test messages):-

{"listener":"redemption-latest","event":{"item":"SE T-shirt","itemId":"test","name":"Devonna","type":"perk","message":"Do not fear a man that spams 1000 memes, instead fear a man that spams a meme 1000 times"}}

{"listener":"redemption-count","event":{"item":"SE T-shirt","itemId":"test","name":"Devonna","type":"perk","message":"Do not fear a man that spams 1000 memes, instead fear a man that spams a meme 1000 times"}}

{"listener":"redemption-total","event":{"item":"SE T-shirt","itemId":"test","name":"Devonna","type":"perk","message":"Do not fear a man that spams 1000 memes, instead fear a man that spams a meme 1000 times"}}

{"listener":"redemption-top","event":{"item":"SE T-shirt","itemId":"test","name":"Devonna","type":"perk","message":"Do not fear a man that spams 1000 memes, instead fear a man that spams a meme 1000 times"}}

{"listener":"redemption-goal","event":{"item":"SE T-shirt","itemId":"test","name":"Devonna","type":"perk","message":"Do not fear a man that spams 1000 memes, instead fear a man that spams a meme 1000 times"}}

{"listener":"redemption-recent","event":[{"type":"redemption","name":"Augusta","amount":5,"count":9,"tier":3000,"isTest":true},{"type":"redemption","name":"Jaclin","amount":26,"count":45,"tier":3000,"isTest":true},{"type":"redemption","name":"Sheena","amount":37,"count":24,"tier":3000,"isTest":true},{"type":"redemption","name":"Dot","amount":6,"count":11,"tier":3000,"isTest":true},{"type":"redemption","name":"Mirelle","amount":19,"count":33,"tier":3000,"isTest":true},{"type":"redemption","name":"Nesta","amount":20,"count":27,"tier":3000,"isTest":true},{"type":"redemption","name":"Fawnia","amount":18,"count":35,"tier":3000,"isTest":true},{"type":"redemption","name":"Eugine","amount":35,"count":35,"tier":3000,"isTest":true},{"type":"redemption","name":"Ardelle","amount":23,"count":41,"tier":3000,"isTest":true},{"type":"redemption","name":"Deva","amount":47,"count":24,"tier":3000,"isTest":true}]}

{"listener":"redemption-points","event":{"item":"SE T-shirt","itemId":"test","name":"Devonna","type":"perk","message":"Do not fear a man that spams 1000 memes, instead fear a man that spams a meme 1000 times"}}

Yes, they were all raised for one single emulated redemption but these are nothing like what you will actually receive (the format is "messagetype",{data}):-

"event:update",{"name":"redemption-latest","data":{"itemId":"ffffffffffffffffffff","type":"perk","name":"athenaofdelphi","item":"8 Bit Era Webcam"}}

"redemption",{"redeemerType":"Channel","completed":false,"input":[],"_id":"ffffffffffffffffffff","channel":"ffffffffffffffffffff","redeemer":{"_id":"ffffffffffffffffffff","avatar":"https://static-cdn.jtvnw.net/jtv_user_pictures/2611f7d5-b3a1-4f19-b43b-3f7443dd1984-profile_image-300x300.png","username":"athenaofdelphi","inactive":false,"isPartner":true},"item":{"alert":{"graphics":{"duration":8,"type":"image","src":"https://cdn.streamelements.com/uploads/8baabd2d-721f-44a6-9c81-0640746cc061.png"},"audio":{"volume":0.5,"src":null},"enabled":false},"userInput":[],"_id":"ffffffffffffffffffff","name":"8 Bit Era Webcam","type":"perk","cost":0},"source":"website","createdAt":"2021-07-11T20:34:22.012Z","updatedAt":"2021-07-11T20:34:22.012Z"}

"event",{"_id":"ffffffffffffffffffff","channel":"ffffffffffffffffffff","type":"redemption","data":{"amount":0,"username":"athenaofdelphi","redemption":"8 Bit Era Webcam","avatar":"https://cdn.streamelements.com/static/default-avatar.png"},"createdAt":"2021-07-11T20:34:22.204Z","updatedAt":"2021-07-11T20:34:22.204Z"}

In JavaScript the Socket.IO client has the 'on' method. I've chosen to use the 'event:update' message because it contains everything I need with no baggage. If you want to allow processing of test events, then extracting the data you need is relatively straight forward.

{"listener":"redemption-latest","event":{"item":"SE T-shirt","itemId":"test","name":"Devonna","type":"perk","message":"Do not fear a man that spams 1000 memes, instead fear a man that spams a meme 1000 times"}}

VERSUS

{"name":"redemption-latest","data":{"itemId":"ffffffffffffffffffff","type":"perk","name":"athenaofdelphi","item":"8 Bit Era Webcam"}}

The event type is contained by "listener" (test) or "name" (update) whilst the actual data for the event is in "event" (test) or "data" (update). As best as I can tell, this is the only difference between the data packets, but it does lead me onto another gripe.

The test events use completely random data. You have no mechanism to select a particular redemption, so the only way I've found to use actual redemptions for testing is to set the price of an item to 0, enable it in the store and actually redeem it. The emulator would benefit immensely from the ability to redeem one of your items. The rest of the emulated events are for the most part acceptable as sources for testing, but store items really should be a selectable item from your store.

And so concludes my general moan about the StreamElements API... it clearly offers some functionality for those who are technically capable to create some great streaming experiences, but document it fully, please.... it could be I'm just stupid and so out of touch with modern development that I simply don't get it, but I've scoured the internet for information and it's just not there.

On the Delphi front... the lack of web socket support out of the box so to speak is immensely irritating considering how long web sockets have been around and how prevalent they are. So, what can you do if you need a web socket interface... well, my OBS interface currently uses a component called TIdSimpleWebSocketClient which appears to be a non-maintained basic implementation of a web socket client. It works, but provides no means of supporting anything more, or at least it doesn't appear to.

As for Socket.IO, components are in even shorter supply, I found one commercial offering (ESEGECE's WebSocket Components - which I elected to purchase) and one free offering which I tried (it didn't work). I'm liking ESEGECE's components but for this purpose there's a couple of things you need to do...

  fEventSock := TsgcWSClient.Create(nil);
  fSocketIO := TsgcWS_API_SocketIO.Create(nil);
  fSocketIO.Client := fEventSock;

  fSocketIO.SocketIO.API := TwsSocketIOAPI.ioAPI2;
  fSocketIO.SocketIO.HandShakeTimestamp := false;
  fSocketIO.SocketIO.Polling := false;

  fEventSock.port := 443;
  fEventSock.host := 'realtime.streamelements.com';
  fEventSock.TLS := true;
  fEventSock.TLSOptions.Version := TwsTLSVersions.tls1_2;

  fEventSock.active := true;

That should get you a working connection to the realtime service. Once connected you will need to authenticate...

procedure TStreamElementsInterface.eventsockemit(event: string; data: string);
var
  msg: string;
begin
  msg := '42["' + event + '",' + data + ']';
  datalog('[>> SE EVENTS] ' + msg);
  fEventSock.writeData(msg);
end;

procedure TStreamElementsInterface.eventSockConnect(Connection: TsgcWSConnection);
begin
  fTimerRecon.enabled := false;
  log('Realtime client connected, sending authentication message');
  eventSockEmit('authenticate', '{"method":"jwt","token":"' + fJWTToken + '"}');
end;

The method 'eventsockemit' is basically an implementation of the 'emit' method on the Socket.IO JavaScript client and illustrates part of the Socket.IO protocol. In essence, as soon as we have a working Socket.IO connection (transported by the web socket client fEventSock) we need to emit an 'authenticate' event before the service will send us events.

If successful, the server will raise an 'authenticated' event (which will provide your client ID and the channel ID which you may require for other purposes), if the token is bogus, you'll get an 'unauthorized' event.

So in Delphi, ESEGECE's components raise a message event (onMessage) when a data packet is received. It's up to you to actually decode the content so a little RegularExpression magic can make this relatively straight forward.

match := TRegEx.Match(text, '42\["(event:test|event:update|authenticated|unauthorized)",(.*)\]', [roNotEmpty, roIgnoreCase]);

This expression will match when we receive message type 42 (this translate to 'string message' [4] event [2]) containing the events event:test, event:update, authenticated or unauthorized. match.groups[1] will contain the event type and match.groups[2] will contain the accompanying data.

This is the only thing I feel is missing from ESEGECE's, a fuller implementation of the SocketIO client interface. In JavaScript you do this...

sockio.on('event:update', (data) => {
  // My handler for event:update
});

Even an onEvent method would be nice that provided say the event name and a TJSONValue parameter containing the data. But, what I have is working and it appears to be working well, thanks in part to Segio at ESEGECE who pointed me in the right direction for getting the Socket.IO connection working (the code I've included above shows the tweaks required to the component defaults to get a connection (mainly the TLS version and the Socket.IO options). His whole setup is great, I purchased the components around midday and by late afternoon I'd tried them out, struggled, dropped him an email and received the answer that solved my problem. Thanks Sergio, amazing service 😄

And so concludes this somewhat lengthy post about talking to StreamElements and some of the issues I've been having. Until next time, take care and stay safe 😁

Addendum - 12th July

So, when an item in the store is updated an event occurs... storeitems:update... great I thought... I can read this event and update my internal cache with the changed data, but no... you have to actually make another request to get the item... why not just send the changes with the event? I'm sure someone will say "because that would increase the payload size and impact performance"... yes, but now I have to make another request to another part of the API which will also impact performance. New items get an event 'storeitems:new' (which again, pretty much just contains the ID of the new item) but deletes don't raise an event, or at least not one that is broadcast with the realtime API. Why?

This isn't meant to be a bashing of StreamElements, as I say, I much prefer their overall product offering, but the public API documentation leaves a lot to be desired.

Comments