K-9's Totally Tubular Blog

TELEVOID Breakdown & Future

tags: televoid, unity, world, udon, vrchat
@K-9 18/02/2025

A brief overview of the TELEVOID, how it works (and gets around some VRC limitations), and what’s next.

(This is an “unrolled” version of a long Bluesky thread I posted in December 2024. Some new images and videos have been added. A small, newly-written section has been added to the bottom.)

The TELEVOID is a world I made to replicate the feeling of watching TV in the early 00’s, when I was a kid. I ended up making a fake TV with 4 channels that play scheduled content. An in-world TV guide lets users view what’s coming up next and see details about current/future airings.

(Video: a demonstration of the world.)

(Video: A timelapse of the world operating with the guide open over many hours. Warning, flashing lights!)

The live channel feeds are produced by a broadcast server running ErsatzTV hooked up to a Jellyfin server for media. ErsatzTV is open-source software that controls scheduling and stream generation, and this couldn’t have happened without it. Thanks, ETV contributors!

ETV generates the feeds and XMLTV guide info. A small helper program I wrote runs every minute, reads the guide information, and converts it into a smaller JSON file. As well, it gets the poster image for each entry and bakes them all into a single image representing an 8x8 grid of posters, squishing each one vertically to fit. Each grid position has an int index, and this index is inserted into each guide entry in the JSON to store where to find that entry’s poster. The completed JSON guide information and baked poster grid are written to disk.

An eight-by-eight grid of vertically squished posters for a variety of TV shows and internet shows. The grid is about 2/3rds occupied, displaying white space at the bottom.
An eight-by-eight grid of vertically squished posters for a variety of TV shows and internet shows. The grid is about 2/3rds occupied, displaying white space at the bottom.

Finally, to make this all available in VRChat, an NGINX server acts as reverse proxy for the ETV server feeds, as well as serving the JSON and poster grid. Once all that is done we can get everything in two requests in-game. Yippee! (The image baking is necessary to work around VRChat limitations, particularly that you can only make one image request every 10 seconds, which makes loading posters individually impractical.)

When you interact with the remote in the world we fetch the prepared guide info, parse the JSON, spawn a guide template prefab, and populate it with the data. I’ll share three tricks that make this work:

One, when we parse the JSON we feed it into an abomination I like to call the object[][][]. Udon doesn’t have official support for classes or lists and at the time I wrote this they were not yet available for UdonSharp so instead we read everything into a 3-deep nested object array where the format is ’entries[channel_no][entry_no][data_type]’. When we read from the object[][][] we just cast everything back to what it’s supposed to be. This is an absolutely disgusting trick but it works.

Green code comments in an editor. The comments read: // future me: jagged array format is as such: // FIRST LAYER INDEX IS THE CHANNEL // SECOND LAYER INDEX IS THE EPISODE // 0: show name, string // 1: episode start date, DateTime // 2: episode number, string // 3: episode name, string // 4: episode plot, string // 5: episode preview index, double
Green code comments in an editor. The comments read: // future me: jagged array format is as such: // FIRST LAYER INDEX IS THE CHANNEL // SECOND LAYER INDEX IS THE EPISODE // 0: show name, string // 1: episode start date, DateTime // 2: episode number, string // 3: episode name, string // 4: episode plot, string // 5: episode preview index, double

The second trick is that, as far as I can tell, there’s no way to directly detect input from a UI button in an UdonBehaviour if the button was spawned at runtime. Ordinarily you place a static button in your world and have it send a custom message to your script, but when you spawn one from a prefab at runtime there is no way go give it a reference to the script which exists outside the prefab. To solve this, each button also has a Light component on it. The light itself is always disabled so it doesn’t do anything or cause performance issues, but we steal some of its parameters to store data in. When we spawn each button prefab, we use the light’s intensity value to store the channel_no, and the range to store the entry_no.

A screenshot of the TELEVOID guide from within the Unity Editor, which indicates that each button on the guide contains a light. The light gizmos stretch past the UI panel boundaries.
A screenshot of the TELEVOID guide from within the Unity Editor, which indicates that each button on the guide contains a light. The light gizmos stretch past the UI panel boundaries.

The last part is that the Button component on each prefab is configured to set its own light component color temperature to a known value that indicates that button was clicked. The guide script checks each button on update to see if any have had their color temperature change. If we find one, then we can read its corresponding light values to know the indexes to get its info from the object[][][]. There’s probably a better way to do this, but I haven’t found it, and this seems to scale well even to mobile.

A screenshot of a button's properties in the Unity game engine editor. The button is configured to set the Color Temperature of a light source to "6969".
A screenshot of a button's properties in the Unity game engine editor. The button is configured to set the Color Temperature of a light source to "6969".

Third trick: how do we display only one poster from the grid and stretch it to fit? The answer in this case is UV trickery. By changing the XY tiling to 1/8 on the material, and by changing the XY offsets by 1/8 intervals, we can “slide” which portion of the grid the material shows.

Applying this material to a stretched rect transform stretches the poster back out for us automatically. There are slight seams around the edges, but overall I’m very pleased with the result.

Anyways, that’s it! If you happen to be the, like, one person that reads this to the end I wanted to extend a big “thank you”. This is the first VRC project I’ve released and I am very excited to talk about it in case any of my silly tricks inspire anyone out there. I hope to have more to post about– and release– in the near future! I also hope to update and improve the TELEVOID so more people may use and enjoy it.

Addendum

What’s next? Well, there’s a few things I’d like to improve:

  • First, content is scheduled too naively. Any eligible program for a channel is chosen at random and scheduled for broadcast. This can lead to some really weird lineups and fails to maintain programming cohesion. I’d like to make blocks of themed programming, different schedules for different days, seasonal programming, etc. That’s step one!

  • Step two is working out commercials. The server currently has about 4000 commercials, which is a lot, but you still start to notice repeats after a time. I’d like to expand it with more.

    • As well, similar to the shows, scheduling for commercials is pretty dumb. The server just plays 3 commercials between programs on eligible channels. This means that you can get totally incongruent commercials in the same break. You might get a commercial for a children’s toy followed by a commercial for DOOM (1994), and stuff like that. I need to organize all of the commercials into their age-appropriateness and what they advertise so that I can program them to play more cohesively with programming, but it’ll take a long while to sort through and organize all of them by hand.
  • Third up, I’d like to clean the codebase up a little and get it ready for public release. I really would like to release the “TELEVOID Television System” as a public prefab!

    • There’s a problem with this that I haven’t yet solved whichis this: should the prefab include access to the TELEVOID feeds? I want to ask people to set up their own feeds, but it takes a lot of work and technical knowledge to do so, so I doubt many would go to the trouble. I could provide the feeds, but if the prefab ends up being used a lot, the concurrent watching across worlds may exceed what my server/bandwidth are capable of. As well, being in more worlds makes the TELEVOID more conspicuous. I don’t exactly have the rights to the things that are there. I’m avoiding broadcasting anything too dangerous (as a rule of thumb, I only try to include something if it’s the sort of show you can already find on Youtube for free), but there’s always the possibility I get unwanted attention if the TELEVOID happens to spread outside of my control.
  • Lastly, I’d like to put something out there. In the VOID. There’s nothing now, but I yearn to hide something out there…