Can't get seamless transition between two video clips

Hey all,

I’ve been building out the UI for a game using the UI Toolkit. The current screen I’m creating has a video background. To achieve this, I use a video player component to record a video clip (.webm) to a render texture, and assign that to the BackgroundImage of the visual element. This is working fine.

For this “caught” screen, I have two videos. When the player gets caught, I play the first video. That video is supposed to blend seamlessly into the second video, which will run in a loop until the player presses continue. But I can’t figure out how to seamlessly transition from video A to video B. I have tried many approaches throughout the forums and documentation, but I can’t get rid of a single-frame flicker that happens between the two videos.

My general approach is to assign the two video clips to variables in the script. I assign video A to the VP and create a blank RT on awake, then assign this to the VP’s target texture. Using VP.loopPointReached, I’ve tried switching between the two a few ways:

  • calling targetTexture.Release(), assigning video B to VP.Clip, and calling VP.Prepare() (keeping the same render texture)
  • creating a second RT on Awake and calling Prepare, then reassigning the VP.targetTexture on loopPointReached to the second RT so it’s already “prepared” in advance;
  • calling targetTexture.Release(), creating a new RT, and reassigning the VP’s clip and targetTexture to video 2 and RT 2, respectively;
  • creating a second VP and RT for video B (both at runtime and in the editor), disabling VP 1 and enabling VP 2;
  • various combinations of the above, with a bunch of random attempts from forums I’ve read online, putting some pieces in coroutines, etc.

I recreated the issue in a dummy project to share here. My main code is a bit of a mess from trying so many different things so the repro project doesn’t necessarily represent all of my efforts, but it shows the issue, and it’s as simple as I could make it.

I would be super grateful for some guidance on the “proper” way to implement this, avoiding the one (or more) frame flicker that happens between the two videos.

Tested using 2023 Macbook Pro M2 running Sonoma 14.4.1, as well as a Linux running the latest PopOS. The attached project was written on the macbook.
Unity version 2022.3.4f1 with latest versions of all relevant packages

The zip is a bit too large to upload (presumably because of the webms) so here’s a dropbox link: Dropbox

Here’s the relevant code (reminder this is just a dummy project I threw together in 10 mins to repro the issue, don’t judge my lazy code :slight_smile: (unless it’s actually causing the issue)):

public class UI : MonoBehaviour
{
    // visual element variables...
    private VisualElement _container;

    public VideoPlayer videoPlayer;
    public RenderTexture renderTexture => videoPlayer.targetTexture;
    public VideoClip initialClip;
    public VideoClip loopingClip;

    private void Awake()
    {
        // set up root visual elements
        PlayVideo();
    }

    private void PlayVideo()
    {
        videoPlayer.clip = initialClip;
        videoPlayer.targetTexture =
            new RenderTexture(1920, 1080, GraphicsFormat.R8G8B8A8_UNorm, GraphicsFormat.D32_SFloat_S8_UInt);
        videoPlayer.targetTexture.Create();
        videoPlayer.loopPointReached += InitiateLoop;
        _container.style.backgroundImage = new StyleBackground(Background.FromRenderTexture(renderTexture));
    }

    private void InitiateLoop(VideoPlayer vp)
    {
        videoPlayer.targetTexture.Release();
        videoPlayer.clip = loopingClip;
        videoPlayer.Play();
        videoPlayer.loopPointReached -= InitiateLoop;
    }
}

Made some interesting progress, it’s got me pretty stumped.

I was actually able to get the transition to happen seamlessly in my dummy project by instantiating and preparing two video players on Awake and using this method for the loopPointReached event:

    private void TransitionToLoop(VideoPlayer vp)
    {
        Destroy(vp.targetTexture);
        vp.Stop();
        _container.style.backgroundImage =
            new StyleBackground(Background.FromRenderTexture(loopRenderTexture));
        videoPlayerLoop.Play();
        vp.loopPointReached -= TransitionToLoop;
    }

The only drawback is that I lose a couple frames off the tail end of Video A. It still looks okay because it’s a fast zoom-out, but obviously it would be better not to lose any frames. Most importantly, there is no flicker or frozen frames of any kind.

so I started over in the full project and reimplemented it this way, which I was sure I tried previously. I still have the same issue in my project. But I noticed that the “one frame flicker” is actually showing me a frozen frame from Video A. And it’s not the last frame of the video, it’s frame from a couple seconds before the last frame. So Video A plays till the end, displays a frame from ~3 seconds ago for about 1 second, and then begins playing the looping video.

Another interesting piece – I have two different versions of these videos. They are identical, but the transition times are slightly different. In the set I’m using here, Video A ends at about 0:16 and transitions to Video B. But in an older set I have, Video A ends around 0:19 and transitions to Video B. The weird thing is that if I use the older video set, where the transition happens a couple seconds later, it works perfectly. I’ve triple checked and even slowed the video down to make sure no frames are lost/repeated.

This tells me that it’s probably not something to do with my project setup that isn’t present in the dummy project, otherwise I should be seeing the freeze frame with the older video set as well. It also tells me that it’s probably not an issue with the videos, since in the dummy project the transition in both is seamless, and in the real project, only the older video set transitions seamlessly.

I’m at least not as blocked anymore since I can just use the older video set, but the transition time is less convenient. So I’d still like to use the new videos. But I really have no idea how to proceed, I’m not sure what to make of this. Any wisdom?

https://www.dropbox.com/scl/fo/64ih4nbnmeceh5j2l0h6x/APTWlr6UKY1kk1eeg2RnOE8?rlkey=5c25djhgzpeftr8e945fxzkka&st=abfihjjj&dl=0

Here are 3 relevant videos. Two videos on the new project: one shows the old video set transitioning seamlessly, and another shows the new video set with the freeze frame and stutter.

With the old video set, the transition happens when the UI text appears on screen towards the end. With the old video set, you can see the transition happening, right at the end of the zoom-out.

The third video shows the new video set working “seamlessly” in the dummy project, albeit losing the last few frames of Video A (you’ll notice the zoom-out is slightly more abrupt, but there is no flicker).

Ok, last note. I implemented it using the old video set in the meantime, and it looks good, but I am noticing an inconsistent flicker of a single black frame when the transition happens. Presumably the same issue… it happens much more often in full screen mode with a free aspect or 16:9. It’s less frequent when I play at 1920*1080 (the resolution of the videos), but it does still happen. May also be because I’m running it on an M2 macbook. I’ll test on my linux and in a build later on.

Surely there is a way to play two videos back-to-back without any frame loss or flicker…

As a workaround, I was able to more or less get rid of the black frame flicker by using a coroutine to swap out the VideoPlayers 0.1 seconds before the end of the first video. Hacky but it’s good enough for now

Just so you know, you’re not alone in this issue. I’m building a game with constant video and the flicker at the start is horrendous and really hard to solve. I’ll check your solutions to see if I find anything. I’ll report back if I get any good news.

All right, this may not be the most elegant, but here you go, future googler:

Make a gameobject with:
2 videoplayers, each with a separate texture. Let’s call them VP and cache
1 Raw Image

Whenever you know a video is coming (definitely NOT frames before, has to be ahead of time), give that video to the cache, and Prepare. Listen to prepared. OnPrepared, .Play() that video, and listen to frameReady. Now, OnFrameReady, pause the video.

Swap the texture to the RawImage when you need the video, and switch the references (so Cache is now VP, and VP is now cache). Hit play on the new VP. This allows you to play the video on the same VP that cached the video.

If you do this properly, no more black frames will appear :slight_smile: