ProTV by ArchiTechAnon | Usage Guides and Walkthroughs

[These posts are currently in WIP state and will be updated over time]

Asset Introduction and Overview

ProTV is a pay-what-you-want asset that intends to make working with video players a more enjoyable experience by enabling extensibility through event driven activity.


  1. Before importing ProTV, make sure you have reimported the VRCSDK/Plugins folder first (if using VCC, this is done via VRCSDK menu → Reload SDK). This will help alleviate issue with the VRCUrlInputField missing/breaking.
  2. Before importing ProTV, make sure you have imported UdonSharp. Importing them out of order can (and likely will) break udon references.
  3. If you want access to the free ProTV Beta, simply join the ArchiTech Assets Discord and assign yourself to the Beta Access role.

Table Of Contents:

What is ProTV exactly?

At its core, ProTV is a central management script that brokers the state and activity of video players that are connected to it. It is capable of handling multiple video players by swapping between them and retaining proper sync to other users while doing so. Based on the state and activity of the management script, it will propagate events and data to other udon scripts that have registered themselves as a listener.

Where can I get it?

The mainline stable releases (currently v2.2) are available as pay-what-you-want (including free) on:
Gumroad (
Booth (

Beta releases (currently v2.3 Beta 2.5) containing the latest feature development, is available via discord (Join Here)

Monetary support for the project also accepted on Ko-Fi

How do I use it?

If you are looking for an out-of-the-box solution that doesn’t take much if any effort to add to your world, check out the Assets/ArchiTechAnon/ProTV/Prefabs folder after importing. This is a selection of TVs that should #justwork. They also support multiple independent copies[1] in-world, no problem. Though if you want something a bit more custom, please continue reading though the guides to get a full grasp of how the ProTV system is setup.

[1] Note: do be aware that in excess of 8 ProTV instances in a world, some performance loss may become apparent during simultaneous use.

How does it work?

The primary driver behind this asset is the TVManagerV2.cs script (aka “main script”). When the developer is setting up the world with a new TV, they will need to first declare what video players the main script will manage. This is done by attaching either a Unity Player or AVPro Video Player script into the scene and adding the VideoManagerV2.cs script to that same game object. Then drag the game object to the Video Players array of the TVManagerV2 script inspector. Repeat until the desired video players are connected. This is the fundamental setup.

With this completed, the main script will now broker state and activity between the connected video players and any udon behavior that declares itself a listener to the main script.

How can I customize it for my needs?

There are quite a number of ways that you can customize ProTV for various situations. The two most meaningful ways to customize are related to the Speakers (audio sources[2]) / Screens (mesh w/ shader) as well as various Plugins that are either provided in the asset or that you can make yourself (more on plugins in a bit).

[2] If you are wanting to customize the audio sources a lot, I highly recommend you have a look at TEXTLOGO’s Audio Demo World for ideas and general setup for certain audio considerations and situations.


Core Architecture

The architecture of ProTV has two main concepts.

  1. Centralized management of multiple video player components.
  2. Event Listener Registry for propagating information to other udon behaviors.

Centralized Management

In order to give developers freedom over what player configuration they wish to use, ProTV implements a proxy-ish script that sits on the same game object as the desired video player component (being either Unity player or AVPro player). This script is called VideoManagerV2.

Each video manager has a handful of configuration options available to it. It also has two lists that should be populated as needed: Speakers and Screens.
These lists associate any Audio Source and Mesh Renderer game objects to the script for automatic management.

These proxies are then also associated with the main script called TVManagerV2. The tv manager has a handful of config options as well for the overall operation of its particular instance of ProTV.

TV Options

  • Autoplay URL
    A url that is automatically loaded as soon as the TV has finished initializing. This is considered a “default” field because plugins are able to override this setting as needed (more on this later).

  • Autoplay URL Alt
    Part of the “Alternate URL” feature. Similar to the standard autoplay url, it will automatically load as soon as the TV has finished initializing.

  • Autoplay Start Offset
    Specifies how many seconds to wait after initialization (basically when the world is loaded in) to attempt to load the autoplay video. This is primarily used to avoid RateLimit errors or to specify load order when there are multiple TVs in the world that have some sort of autoplay defined on them. If there are one or less TVs with autoplay, this setting can be ignored.

  • Initial Volume
    Pretty straight forward. This specifies what the volume should be for the TV at load time between 0% and 100% (represented as a 0.0 to 1.0 decimal)

  • Initial Player
    This integer value specifies which index of the related Video Managers array option the TV should start off with. The array of video managers is 0-index based. This means that if you have, say, 3 video players in the list, if you wanted to have the TV default to the second video manger in that list, this option would need the value of 1 in it. If you only have one video manager, then set this option to 0.

  • Paused Resync Threshold
    This is more so a visual feedback feature where if the video is paused locally but the video owner is still playing, the video will update the current playback timestamp every some number of seconds determined by this option. If you don’t want the video to resync while paused, set this value to Infinity (yes the actual word) to have it never update until the user resumes the video.

  • Automatic Resync Interval
    Setting for telling the TV how many seconds to wait before trying to resync. This affects two separate things.

    • First is the Audio/Video sync. This particularly affects AVPro for long form videos (like movies) where after some time or excessive CPU hitching (such as repeated alt-tabbing) can cause the audio to eventually become out-of-sync with the video (usually it’s the video that falls behind).
    • Second is the Owner sync. This is where the owner of the current video determines the target timestamp that everyone should be at. ProTV has a loose owner sync mechanism where it will only try to enforce sync at critical points (like switching videos, the owner seeks to a new time in the video, owner pauses the video for everyone, etc).

    This option defines how many seconds to wait before attempting to do the above resync.

  • Retry Live Media
    This is for specifying how many retry attempts should be made when live media fails. Basically when a livestream goes down, ProTV will attempt to reload the same URL some number of times, determined by this setting, before determining that the stream should actually be considered failed/finished. This was originally created to help when doing things like swapping DJs on a VRCDN stream.

  • Start Hidden
    NOTE: This option is considered deprecated and shouldn't be used unless you know what you are doing. It is recommended to simply disable the TV gameobject in the inspector manually if you want it to start off as hidden.
    This setting automatically hides the TV’s gameobject after initialization, but before and autoplay video has been loaded. Useful if you have some plugins that need to modify the TV during the init phase.

  • Allow Master Control
    A security setting that declares whether or not the instance master (aka the user who has been in the instance the longest) has the right to lock/manage the TV.
    NOTE: The instance owner (aka the creator of the instance itself) will ALWAYS have the right to lock/manage the TV regardless of master.

  • Sync To Owner
    A flag to tell the TV whether or not it’s supposed to synchronize with the owner. The setting is typically set to true. Though, for example, it can be particularly useful when trying to make a local-only music player.

  • Video Managers
    The other part of the Initial Player setting. This is the actual list of VideoManager scripts that the TV should be handling.

Event Listener Registry (aka Subscribers)

In order to better facilitate extensibility for this asset, ProTV implements a behavior subscription structure where any Udon script can register itself to the TV and will receive specific events and data based on the TV’s activity.

These events are a bit more granular than the standard udon events for video players (though ProTV does emulate those). It is recommended to be judicious when deciding what event to listen with.


To connect your udon behavior to the TV, the minimum required is a reference to the desired TVManagerV2 script, the Start event and a reference to the current script (aka self or this). Here’s what the minimum setup looks like:


public class MyCustomScript : UdonSharpBehaviour {
    public TVManagerV2 tv; // the public TV reference
    void Start() { // the Start event
        // 'this' refers to the current script


The variable and event names are a bit long for the input field in the node, so the full string is commented below the nodes.

Once this is done, your script will begin to receive events from the TV. You can also run this logic at any point in the runtime (not just in the Start event, though that is the most common use case) and it will register to the TV correctly.

Listening for Events

There are various types of events available for different contexts. All events called from the TV script begin with the _Tv prefix to avoid event naming collision with other script usages.

Init Phase

  • _TvReady
    • Occurs when the TV has completed all initialization and is ready to be controlled.
    • If a behavior is registered to the TV after the initialization phase (Start event), then it will be called on the target behavior immediately after registering.
    • USE THIS EVENT TO “CATCH UP” WITH THE TV’S CURRENT STATE. This helps ensure that even if the plugin starts off as disabled on world load, as soon as it becomes registered, it can immediately sync up its internal state with the TV’s.

Based on TV Activity

  • _TvPlay

    • Occurs when a video has started or resumed playing
  • _TvPause

    • Occurs when a video has been paused locally
  • _TvStop

    • Occurs when a video has been stopped locally
  • _TvMediaStart

    • Occurs immediately after a video has been loaded
  • _TvMediaEnd

    • Occurs immediately after a video has finished playing
    • Can be used to trigger a new video, does NOT occur if video is set to loop
  • _TvMediaLoop

    • Occurs when a video starts over after finishing
    • Triggered only if video is set to loop OR if video is at the end and the owner pressed the play button for a one-time loop
  • _TvMediaChange

    • Occurs when a user has claimed ownership of the TV and declared a new video to play
    • This event happens before the video is actually loaded. To take action when a video is ready after it has been loaded, use the _TvMediaStart event.
  • _TvOwnerChange

    • Occurs when a different player takes control of the TV.
  • _TvVideoPlayerError

    • Occurs when the video failed to resolve and play for some reason
    • This event will attempt to set the variable OUT_TvVideoPlayerError_VideoError_Error with the VideoError value that caused the event to trigger.
    • When the VideoError is of INVALID_URL, this event is triggered immediately instead of waiting for the url to try and resolve the media.
  • _TvLoading

    • Occurs when the TV’s loading state is enabled
    • It can happen at various points, so this event is mostly used for UIs to reflect the loading state of the TV.
  • _TvLoadingEnd

    • Occurs when the TV’s loading state is disabled
    • It can happen at various points, so this event is mostly used for UIs to reflect the loading state of the TV.
  • _TvLoadingStop

    • Occurs when the TV’s loading state is interrupted
    • This is caused when the tv’s _Stop event is called while a video is loading.
    • When this event is fired, the _TvLoadingEnd event will NOT be. It is recommended to have both events present to handle both cases.

Based on Change to a TV Setting

  • _TvVideoPlayerChange

    • Occurs when the TV has swapped the video player configuration to a different one
    • This event will attempt to set the variable OUT_TvVideoPlayerChange_int_Index with the current index value of the video player configuration that has been swapped to.
  • _TvMute

    • Occurs when the local user mutes the current video
  • _TvUnMute

    • Occurs when the local user unmutes the current video
  • _TvVolumeChange

    • Occurs when the local user updates the volume percent value
    • This event will attempt to set the variable OUT_TvVolumeChange_float_Percent with the updated volume percent. This event might be called many times in a short period, especially if it is affected by a slider element modifying the TV’s volume.
  • _TvAudioMode3d

    • Occurs when the local user switches from positional to stereo audio
  • _TvAudioMode2d

    • Occurs when the local user switches from stereo to positional audio
  • _TvEnableLoop

    • Occurs when the local user enables looping for the current video.
  • _TvDisableLoop

    • Occurs when the local user disables looping for the current video.
  • _TvSync

    • Occurs when the local user enables video synchronization (disabled for owner as one cannot desync with oneself)
  • _TvDeSync

    • Occurs when the local user disables video synchronization (disabled for owner as one cannot desync with oneself)
  • _TvLock

    • Occurs when an authorized user (usually instance master) locks the TV for authorized use only.
  • _TvUnLock

    • Occurs when an authorized user (usually instance master) unlocks the TV for anyone to use.



Plugins Overview


Versatile UI for controlling the TV.
Multiple prefab styles and options.
Easily customizable theming and layout via Unity UI.


Customizable pre-made list of videos.
Virtualized UI scrollview supporting 10s of thousand of entries.
Custom playlist file format for easy storage, implementation and version control against.
Supports searching and pagination.


Dynamic list of videos that players can add to in-game.


A script and modified shader that enables 360 equirectangular (aka panoramic) video to be easily rendered as the skybox of the scene.


MediaControls Plugin Deep Dive



Playlist Plugin Deep Dive


Working on better docs currently. For now here’s a couple easy videos to get started with the playlist.

Setting up the Playlist

Modifying the Playlist Template

ProTV - Playlist Setup and Options (2.3 Beta 2.0 or later)


Queue Plugin Briefing



Skybox Plugin Briefing

This plugin encompasses a minimal plugin script that swaps the scene’s skybox material on certain TV events and the shader used to render various video modes of the skybox.
It supports equi-rectangular (aka panoramic) 360deg video and 180deg video as well as certain cubemap style 360 video renders.
It also supports Side-By-Side (SBS) and Over-Under (OU) 3D modes for VR.

Plugin Options

  • Tv
    The ProTV instance that the plugin should register to.

  • Skybox
    This is the material that the plugin should assign to the skybox material when the TV is considered ACTIVE. The required shader to use is the Video/Skybox shader that this plugin provides, as this plugin directly updates properties on the shader.

  • Fallback
    This is the material that the plugin should assign to the skybox material when the TV is considered INACTIVE. This can be left empty and the plugin will automatically use the world’s existing material as the fallback.

  • Brightness
    This is a UI Slider that is used to determine the gamma exposure of the rendered video in the shader. Helps reduce eye strain in certain situations.

  • The rest of the options are just for visual feedback of what state is active and do not have functional bearing on the plugin itself.


The plugin considers the TV to be ACTIVE for the following TV events: _TvPlay, _TvMediaStart
The plugin considers the TV to be INACTIVE for the following TV events: _TvMediaEnd, _TvStop, _TvVideoPlayerError

The following events handle updating the shader options for modifying the render output:

  • _Brightness
    Applies the value of the Brightness slider to the exposure value of the shader.

  • _Not3D
    Disables any 3D mode. Make the shader render the whole video frame to both eyes in VR.

  • _SideBySide
    Enables the SBS mode. This 3D layout mode is most commonly used for 180deg video.

  • _OverUnder
    Enables the OU mode. This 3D layout mode is most commonly used for 360deg video.

  • _Flip
    Makes the shader render the video inverted vertically. This setting generally shouldn’t be needed, but sometimes the video may need to be flipped on certain platforms or videos.

  • _SwapEyes
    Makes the shader render the video to the opposing eyes when in a 3D mode. When in desktop mode, this swaps the render between each eye, since desktop can only see one eye at a time.

  • _Deg180
    Sets the render area to 1/2 the skybox.

  • _Panoramic
    Sets the render area to the whole skybox.

  • _CubeMap
    Sets the render area to use the whole skybox as well as interpret the video as having a cubemap style layout instead of the equi-rectangular layout.

Per-URL Settings

In order to allow for these settings to be applied on a per-URL basis (different videos should be handled uniquely), there is a meta info feature that the TV will extract from the URL if it’s present.
This is done by appending a # to the URL and then specifying the settings you want applied, separating each by a ;. For example;Standard


How to integrate AudioLink

Setting up AudioLink with ProTV isn’t too difficult. Here’s demonstration video showing how to connect all the pieces together.


Tips, Tricks and Best Practices

So you want to make your own Plugin?



How to use UdonGraph with ProTV

While ProTV generally uses U# for it’s setup, it is also provides and easy way to implement custom logic within UdonGraph as well.



Misc Notes

Understanding URLs in VRChat

All video players, generally speaking, work on Quest. Just gonna get that answer out of the way.

What doesn’t always work on Quest is the urls themselves. There are a few points to make regarding this:

  1. The urls that people are most used to aren’t actually urls for media but for websites. Most commonly is Youtube, which is mostly in the form of This url is a website url, not a media (aka direct video) url. Video players do not know how to read a website url, so they would normally fail.

    BUT! VRChat has a trick up its sleeve to handle this. They use a command line tool called YouTubeDownLoad(YTDL). What this does is fetches the content of the webpage url, and fishes around that content until it finds a url that is a valid media url, then spits that back to VRChat so the program can tell the desired video player (either UnityVideoPlayer or AVPro) to play the direct url.

  2. Due to technical limitations beyond the VRChat dev’s reach, YTDL is unable to be used natively on the Quest platform. Only PC has access to this tool currently. This means that when a user tries to play a generic youtube video using the common shortform that you see in the browser URL bar… it’s not going to work.
    There are work arounds for this problem… but the free options are generally unreliable and the other options require some level of technical skill and money to accomplish.

  3. The first workaround option we have is using the YTDL tool externally to VRChat on a PC. What this does is gives you the direct media url which you can then enter into the video player which will then sync to Quest users and they’ll able to see it at that point.
    There are some drawbacks to this method:

    • First is that it requires an external computer to run the YTDL program in order to parse the website url into the longer-form media url.

    • Second is that for pretty much all large-scale media hosting platforms (such as youtube/video/dailymotion/etc) embeds a number of various parameters within the long-form urls, one of which is an expiration timestamps. So once the expiration timestamp has passed, the URL is no longer valid an will no longer load the video. This means that it is not realistic to put the URLs into the world at build time, as they would eventually not work. This also makes it difficult to simply share streams as well because most of those urls expire after an hour or two.

  4. The second workaround is to use a custom server external to VRChat that is exposed to the internet which runs the YTDL tool for the user and then returns the media url. This helps because each time the URL is fetched, it has a new timestamp in it, so it can work for extended periods of time. The difficult thing is that it does require server admin knowledge if you set it up on your own.

    • There is a free service that does this publicly, referred to as “Jinnai”. This service is quite popular, but is generally unreliable due to it’s overuse. Videos tend to fail 50% of the time.

    • There is an open source option called Vroxy if you want to host your own. It even includes setup and update scripts in the repo for any Debian-based OS (like Ubuntu) to make setup a breeze.

    • One thing to note about using a server proxy like the above two, is if there is too much suspicious traffic from the server’s IP, youtube/twitch/vimeo/etc can just rate-limit or straight up block the server’s IP causing url resolution to fail.

  5. The final workaround is much more involved as it involves hosting the videos yourself from a custom server. You’d basically use YTDL to grab the video itself, then put it on a server you have and have people access the URL directly to your server instead of the mainstream hosting platforms. This is oviously an advanced method that the average world creator is not going to do.

The cheapest option currently is to use the Jinnai service, even with its instability. Though if you are not shy of doing a bit of server stuff, deploying an instance of Qroxy will generally make the urls more reliable.

All this doesn’t even go into the actual media content, like codecs and formats, which can also contribute to video failures. But that’s for another time.

TL;DR: Youtube on Quest isn’t for the lazy.

About YoutubeDL specifically…

In order for VRChat to determine what the direct URL is for a given video, it makes use of the tool called youtubeDL. This tool is what allows desktop to be able to “watch youtube videos” (or other hosting sites like vimeo/twitch/etc). Currently due to technical reasons, this tool is not available on the Quest version of VRChat, ergo why youtube doesn’t work on quest by default.

There are two ways around this limitation when dealing with quest.

  1. On desktop, manually resolve the url outside of vrchat. This involves downloading a copy of youtubeDL onto your local machine, opening CMD (or equivalent command line program), then running either of the following commands:

    • The simplest one-liner is youtubedl -g -f best
      This will simply get (the -g option) the url that is noted as the most compatible (the -f best option).
    • If you want to see what formats are available instead of just the “best” one, you can do youtubedl --list-formats
      This will list all options available, examine the media codec types and resolution in the output to find your desired option, then copy the format ID (for youtube it’s a plain number on the left of each option) and replace the “best” term with that ID.
      Like so: youtubedl -g -f 312
      (Also if you are using vimeo with this method, I highly recommend any of the formats that start with hls-akfire_interconnect_quic- as they are good quality and compatible with Quest as well)

    Once you’ve retrieved the long-form direct URL for the youtube video, copy it and paste it into the input field for the TV.

    And additional thing to note with this method is that for most major video hosting platforms (like youtube and twitch), the long-form url has an embedded expiration timestamp in the URL. This means that the URL will only work for up to that timestamp and then will start failing for people who either late join, or reload the video. For those who already connected and are playing the video, it’ll continue to play.

  2. If you have knowledge of self-hosting stuff and a bit of programming experience, you could easily make a resolver redirect that utilizes youtubeDL remotely to resolve the URL for whoever requests it.
    This requires a server to host it on (like a VPS, AWS, or whatever), a domain (optional) and a bit of knowledge with nginx and some server language.
    The result should be where the URL is of the server and within that URL (say for example, a query parameter) is the ACTUAL url you want resolved, the server target would resolve the actual url with youtubeDL and return a 301 redirect to the requesting client which would then play that new URL.
    This would work long-term as well because every time someone tries to (re)load the URL in the world, it would cause it to be resolved to the latest expiration timestamp, thus removing the issue mentioned in the first option.
    I have tested a very rudimentary version of this and it does work, with the exception of livestreams (like twitch) on Quest. For some reason that platform does not like being redirected to live media. I’m still looking into this particular issue.

Youtube caveats

  1. If you grabbed a bunch of youtube URLs to put into a ProTV playlist and some of them don’t seem to be working, be sure that you remove any &list=PLblahblahblahblahwhatever from the URL. If there is a youtube playlist ID as a query parameter on the url, youtubeDL will attempt to fetch ALL THE DATA from every single video within that playlist. If the youtube playlist is of excessive amount (like more than 10 items) it will cause excessive lag in loading the video, or may even just straight up timeout the request causing the video load to fail completely. TL;DR: ONLY HAVE YOUTUBE PLAYLISTS IN THE URLS IF YOU ABSOLUTELY NEED THEM TO BE.

Missing script references?

If you imported ProTV and it’s dependencies (latest SDK3 and UdonSharp 0.20.3 or later), try right clicking on the ArchiTechAnon folder and selecting “Reimport”. If that doesn’t work, try closing and reopening Unity, then doing a “Reimport” on the UdonSharp folder and on the ArchiTechAnon folder as well. This should correct any missing references that SHOULD be present. If it still isn’t working, contact me on discord to help chase down the actual cause.

Random tips to be aware of

  • When you call LoadURL/PlayURL on a video player instance, if the URL is invalid, the error callback will be called IMMEDIATELY before returning from the LoadURL/PlayURL method call.
  • It appears that complex encoding (such as URL encoded characters from eastern languages) has issues on Quest. If you are doing any custom hosting, for consistency and avoiding problems in the future, it is recommended to ONLY use ASCII based characters in urls. If you are going a basic video file serve, make sure the video file names are also only ASCII based.

Tried this plugin and is working pretty good so far. I saw the PlaylistQueueDrawer component in the package, but simply draging it into the world doesn’t seem to have the function of a queue. Is the queue function still a WIP?

edit: nvm I got it. There’s a readme in the plugin folder. Didn’t notice that lol.


I have Problems with my Audio… The 3D to 2D Audio Button does not work… I need help…

1 Like

This is super helpful stuff. MANY THANKS.
I have folowing problem: Cant get stream working in Quest.

Background: Try to self host streaming server (HLS) on local computer. Exposed on the internet via Dyndns*). Streaming server seems to work as I can a) Watch in VLC locally, b) watch on mobile phone cellular, c) PCVR version of VRChat with AVPRO (Unityplayer not working).

But on Quest I cant get the stream to work in either player.
Could it be that Quest required HTTPS and will reject stream coming from http. Or do you have any other hint?

For security reasons, quest requires that ALL web requests (including video player urls) are HTTPS. This requires a custom domain with and SSL cert attached. There are multiple ways to do SSL certs, but since you are self-hosting it sounds like, I’d look into utilizing the LetsEncrypt certbot tooling as a free SSL option.

I deployed a server for parsing. I want to know how to intercept the URL in the input box, process the string, and then play it because VRC Url objects currently cannot be constructed at runtime in Udon

You cannot “intercept” the url, due the fact that it would require dynamic VRCUrl objects.
There are a couple options for dealing with user input (not going through playlists or pre-baked URLs):

  1. Have the user manually type in the redirector server link and then paste the target url as a parameter for that server’s link. (eg: https://mydomain.tld/?url=
  2. Have a separate standard text box which the user puts the desired link in that you can then modify dynamically to prepend the server’s link format. The user then needs to copy that resulting text and put it in the target VRCUrlInputField.

What sort of streaming works best to quest users? I’ve heard of things like HLS, RTMP, and http streaming, the last one when you connect you just get the data (shoutcast?)

I tried topazchat out once in a world with a different player, not sure what protocol it uses, but apparently on quest it kept giving up. Hoping for something that can retry when needed, or just keep working…

Generally speaking, the recommendation is the MPEG-TS protocol for Quest.
You’ll typically see the URL end in something like .ts.

From what I could tell in the world (as I cannot read that language), nothing out of the ordinary is happening. It looks like a standard URL input field that feeds the video into the TV as usual.