[VRChat] Setting Up a Video Player: A Screen Everyone Watches Together

Created: 2026-09-08Last updated: 2026-09-09

Playing video on a screen so late arrivals see the same moment. Covers syncing only the URL and start time, and why you align the position after loading finishes.

You want a screen in the room so everyone can watch the same video together.

It played on your screen. Then you go in with a friend and it's already running from the start on theirs. And a late joiner starts at zero seconds while everyone else is twenty minutes in.

Video isn't shared the way synced variables are. This article builds a screen where everyone sees the same moment, however differently they arrived.

Two people watching the same moment on a screen, side by side

What You'll Learn

  • That only the URL and start time get distributed
  • Why you align the position after loading finishes
  • Why URLs can't be built at runtime
  • How the components connect

Start from having tried networking basics.

Sponsored


The video isn't distributed — the meeting point is

Here's the most important part first.

The video itself isn't shared. Each computer goes and fetches the video and plays it itself. What the world distributes is only the arrangement to meet.

Only the URL and start time are shared; each person fetches the video themselves

Only these three are held as synced variables.

Synced valueMeaning
Whether it's playingIs it running right now
Which URLWhat's playing
The server time it startedWhen the "zero second" point was

The third is the heart of the mechanism. Knowing when it started, each person can work out what second to be at with subtraction.

seconds to be at = current server time - the server time it started

For a video that started twenty minutes ago, a late joiner computes "1200 seconds in" and jumps there.

Using server time is the point. Each computer's clock differs, and Networking.GetServerTimeInMilliseconds() returns the shared clock held by VRChat's servers. Everyone measures with the same ruler.

There's something to know about the URL too.

What you pass to PlayURL() isn't a string but a VRCUrl. And it can't be built from a string at runtime.

// This won't play
videoPlayer.PlayURL(new VRCUrl("https://example.com/movie.mp4"));

It's a safety restriction. If URLs could be assembled freely inside a world, you could be made to connect somewhere the author never intended. There are only two acceptable sources.

  • A VRCUrl placed in the Inspector (what this article uses)
  • A URL a player pasted into a VRC Url Input Field
Sponsored

You can't move the position until loading finishes

"Subtract from the start time and call SetTime()." The idea is right, and calling it in the wrong place does nothing.

SetTime before loading is ignored; after Ready it takes effect

Right after calling LoadURL(), the video hasn't arrived. Calling SetTime(1200) in that state has nothing to move, so nothing happens, and once loading finishes it starts playing from zero.

That's the true form of "only late joiners watch from the beginning."

The correct order goes like this.

  1. Start loading with LoadURL()
  2. Wait for OnVideoReady() to fire
  3. Then SetTime() and Play()

OnVideoReady() is the "ready" notification. Remember that touching the position is only allowed from there on.

Hands-On: A screen everyone watches together

Build a screen where pressing a button starts the video and late arrivals see the same moment.

1. Assemble the picture and sound

Video splits into three parts: the loader, the surface it's shown on, and the thing that plays the sound. Start with the surface.

Right-click in the Project window and make ScreenTexture with "Create → Render Texture." Set Size to 1280 × 720.

Then make ScreenMaterial with "Create → Material," set its Shader to Unlit/Texture, and drag ScreenTexture into the texture field.

Unlit is chosen so the room's brightness doesn't affect it. Left on Standard, the screen goes dark in a dark room.

Place the following in the scene.

NameHow to make it, and settings
Screen3D Object → Quad. (0, 2, 4), Scale (3.2, 1.8, 1). Material ScreenMaterial
VideoRootCreate Empty. (0, 0, 0)
ScreenAudioCreate Empty with an Audio Source. (0, 2, 4), Spatial Blend 0
PlayButtonCube. (0, 1, 2), Scale (0.4, 0.4, 0.4)
Arrangement of the screen, audio source, and play button

The Quad's 3.2 × 1.8 is a 16:9 ratio. Get it wrong and the picture stretches.

Add VRC Unity Video Player to VideoRoot via "Add Component" and set the following.

FieldWhat to set
Target TextureScreenTexture
Target Audio SourceScreenAudio
Auto PlayOff (we want it to start on press)
Assigning where it draws and where it plays, with Auto Play off

Spatial Blend is 0 because we're picturing a cinema — the same volume wherever you stand. If you'd rather have "audible as you approach," like a club's monitor, set it to 1.

2. Write the code

Create SharedScreen in Assets/Scripts.

using UdonSharp;
using UnityEngine;
using VRC.SDK3.Components.Video;
using VRC.SDK3.Video.Components.Base;
using VRC.SDKBase;

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class SharedScreen : UdonSharpBehaviour
{
    [SerializeField] private BaseVRCVideoPlayer videoPlayer;
    [SerializeField] private VRCUrl videoUrl;

    [UdonSynced] private bool isPlaying;
    [UdonSynced] private int startServerTime;   // The server time of the zero-second point

    // Called from the play button
    public void RequestPlay()
    {
        if (!Networking.IsOwner(gameObject))
        {
            Networking.SetOwner(Networking.LocalPlayer, gameObject);
        }
        if (!Networking.IsOwner(gameObject)) return;

        isPlaying = true;
        startServerTime = Networking.GetServerTimeInMilliseconds();
        RequestSerialization();

        StartLoading();
    }

    // Receivers go through the same routine
    public override void OnDeserialization()
    {
        StartLoading();
    }

    private void StartLoading()
    {
        if (!isPlaying || videoPlayer == null) return;

        // The position can't be aligned yet
        videoPlayer.LoadURL(videoUrl);
    }

    // Only once loading finishes can the position be moved
    public override void OnVideoReady()
    {
        int elapsedMs = Networking.GetServerTimeInMilliseconds() - startServerTime;
        float seconds = elapsedMs / 1000f;
        if (seconds < 0f) seconds = 0f;

        videoPlayer.SetTime(seconds);
        videoPlayer.Play();

        Debug.Log("[SharedScreen] playing from " + seconds + "s");
    }

    public override void OnVideoError(VideoError videoError)
    {
        Debug.LogWarning("[SharedScreen] can't play: " + videoError);
    }
}

The structure matches networking basics. The presser takes ownership, changes values, and sends. Receivers go through the same routine.

The only difference is that the post-receive work splits into two stages. Start loading, and align the position once it's ready. Video takes time, hence the split.

Typing the field as BaseVRCVideoPlayer is deliberate too. Swapping to AVPro later requires no code rewrite.

3. Wire it up

Add a Udon Behaviour to VideoRoot and set SharedScreen.

FieldWhat to set
Video PlayerVideoRoot itself (VRC Unity Video Player)
Video UrlA direct MP4 link

Put a direct link ending in .mp4 into Video Url — not a video site's watch page URL. For testing, a publicly available sample video URL is fine.

Put a short relay script on PlayButton.

using UdonSharp;

[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class ScreenPlayButton : UdonSharpBehaviour
{
    [SerializeField] private SharedScreen screen;

    public override void Interact()
    {
        if (screen != null) screen.RequestPlay();
    }
}

4. Check with two people

Launch Build & Test with Number of Clients set to 2.

OrderActionExpected result
1A presses the play buttonAfter a short wait, the video starts on both screens
2Listen to the soundIt's the same volume wherever you stand
3B leaves and re-enters 30 seconds laterB's screen starts around the 30-second mark
4Look at the ConsoleB's side shows "playing from 30.xs"

Row 3 is the most important check in this article. B, who never pressed play, sees the same moment as the person who did.

The seconds not matching exactly is normal. It drifts slightly by however long loading took.

Sponsored

Common Pitfalls

  • The screen is pitch black → Check that the Render Texture is in the material and the Video Player's Target Texture is correct
  • The screen is dark → The material's Shader is still Standard. Use Unlit
  • No sound → Check that an Audio Source is assigned to Target Audio Source
  • Only late joiners start from the beginning → You're calling SetTime() outside OnVideoReady()
  • Nothing plays → Check the URL isn't a watch page, and that it's a direct .mp4 link. Look at the OnVideoError log too
  • The picture stretches → The Quad's aspect doesn't match the video. For 16:9, use a ratio like 3.2 × 1.8
  • It only fails to show in the Unity editor → AVPro doesn't play in the editor. Switch to Unity Video to test

Bonus: Good to Know Up Front

  • Live streams can't be position-aligned: A live broadcast has no "20 minutes in" point, so SetTime() does nothing. You just watch it as it comes
  • URL permission has conditions: Playback requires the viewer to have Untrusted URLs allowed in their settings. In public instances, the world also needs its allowed domains registered. Test in an instance of your own first
  • Drift correction can wait: Watch long enough and it drifts gradually. There are designs that re-measure the position periodically, and just aligning the first time changes the experience enormously
  • Adding pause means one more value: Hold the paused seconds in a synced variable and recompute from there on resume. Same mechanism
  • It's useful all over: Cinema worlds, event venue broadcasts, work introduction videos, tutorial playback. All the same shape

Summary

Sharing video means sharing a meeting arrangement, not the picture.

  • You sync three things: whether it's playing, the URL, and the server time it started
  • Each person works out what second to be at with subtraction
  • Align the position after OnVideoReady(). SetTime() before loading does nothing
  • URLs can't be built at runtime. Pass them from the Inspector or an input field

The question to ask while building is: "When is this video considered to have started?" Share that and everyone can catch up on their own.

To bring in external text, go to updating a board from external text. To display external images, go to swapping posters with external images.

VRChat Notes in this section63