Code Example : How To Detect The End Of The Playable Clip

Hi,

This is my first post here, so, sorry if something is weird.

Anyway, I'm about to share my little discovery to detect if the clip is ended. But if there's a solution out there already that same or even better than me, let me know.

Straight to the code:

    public override void OnBehaviourPause (Playable playable , FrameData info)
    {
        var duration = playable.GetDuration ();
        //var delay = playable.GetDelay (); // probably used in some cases, but for now, just let it be
        var time = playable.GetTime ();
        var delta = info.deltaTime;

        if (info.evaluationType == FrameData.EvaluationType.Playback)
        {
            var count = time + delta;

            if (count >= duration)
            {
                Debug.Log ($@"OnClipEnd");
            }
        }
    }

So the Idea here is we know if the Clip is stop playing for getting function call on OnBehaviourPause()

And for polishing the check, we use the info.evaluationType to check if it's ended while the timeline getting played (it's work, but when you pause in the middle of the clip, it'll got caught too)

and if you dig into both parameter, you actually get the required data

First, is the playable.GetDuration(), the crucial one, it'll give you the, well, duration of your clip
Second, the playable.GetTime(), this is the local, clip played time, between 0 - your duration
And lastly, info.deltaTime, the amount of time passes from the previous frame, with this we can make the logic here more promising

The first thing here if you just printed out the playable.GetTime() after the check of info.evaluationType, it'll print out a number that close to your clip's duration, but even that, we can't sure if its because pausing in the middle of the clip or not

On the other hand tho, the info.deltaTime variable give us the answer, it'll return 0 if it's paused and from seek, but when played it correctly, it actually give us the delta time,

So when you add the playable.GetTime() and the info.deltaTime you'll get the value >= the playable.GetDuration()

So from there we can check if it's actually ended or not.

And also there's one function that may be useful, its the playable.GetPreviousTime() it gives us the time before the current frame time, but if you use that instead, the result will be less than the playable.GetDuration()

2 Likes

Great post! Thank you!

We identified this as an issue a while back. The method gets called when the playable behaviour becomes inactive due to the time OR when the graph itself gets stopped (i.e. paused). The latter was a mistake, but one that removing would break a lot of code.

So instead we added https://docs.unity3d.com/ScriptReference/Playables.FrameData-effectivePlayState.html

If that is set to playing, it means the graph was paused and you can ignore it. If it's paused it means the clip became inactive (or a parent did, but that doesn't happen on custom tracks).

But your solution looks great...and backwards compatible! :)

4 Likes

I have one question here: neither the original method with checking deltaTime+GetTime >= Duration nor just checking if the effectivePlayState is Paused work when the clip is the last clip on the timeline.

OnBehaviourPause is indeed called, but both of those conditionals will fail. DeltaTime will be 0, and the effectivePlayState will return "Playing".

The only way I found to reliably handle both cases (a clip in the middle of the timeline, and a clip at the very end), was to check for Mathf.Approximately((float)time, (float)duration). If I checked the values they were always equal right at the end, but for some reason the conditional time == duration would never be true, but Approximately worked out well.

Edit: actually, info.effectivePlayState == PlayState.Paused will trigger to true at the very beginning of the PlayMode! So you can use either the original approach + Approximately, or a hybrid of seant's and Approximately, as you will still need to check count > duration if effectivePlayState == Paused.

Final code with Hybrid approach:

    public override void OnBehaviourPause(Playable playable, FrameData info)
    {
        if (!Application.isPlaying)
        {
            return;
        }

        var duration = playable.GetDuration();
        var time = playable.GetTime();
        var count = time + info.deltaTime;

        if ((info.effectivePlayState == PlayState.Paused && count > duration) || Mathf.Approximately((float)time, (float)duration))
        {
            // Execute your finishing logic here:
            Debug.Log("Clip done!");
        }
    }
1 Like

You are right, that's a special case. The graph is stopped because the timeline is completed, so the clip is never actually paused on its own, or has it's time updated accurately.

I think you can more accurately check for that condition by playable.GetGraph().GetRootPlayable(0).IsDone();

It is basically checking if the timeline playable has been flagged as past it's duration. That's only valid on the timeline playable, and only happens if the wrap mode isn't loop or hold (which is when the graph gets stopped automatically).

1 Like

[quote=“seant_unity”, post:2, topic: 739066]
Great post! Thank you!

We identified this as an issue a while back. The method gets called when the playable behaviour becomes inactive due to the time OR when the graph itself gets stopped (i.e. paused). The latter was a mistake, but one that removing would break a lot of code.

So instead we added https://docs.unity3d.com/ScriptReference/Playables.FrameData-effectivePlayState.html

If that is set to playing, it means the graph was paused and you can ignore it. If it’s paused it means the clip became inactive (or a parent did, but that doesn’t happen on custom tracks).

But your solution looks great…and backwards compatible! :slight_smile:
[/quote]
Glad you guys provide the more cleaner look or even shortest way to check the state.

[quote=“f0ff886f”, post:3, topic: 739066]
I have one question here: neither the original method with checking deltaTime+GetTime >= Duration nor just checking if the effectivePlayState is Paused work when the clip is the last clip on the timeline.

OnBehaviourPause is indeed called, but both of those conditionals will fail. DeltaTime will be 0, and the effectivePlayState will return “Playing”.

The only way I found to reliably handle both cases (a clip in the middle of the timeline, and a clip at the very end), was to check for Mathf.Approximately((float)time, (float)duration). If I checked the values they were always equal right at the end, but for some reason the conditional time == duration would never be true, but Approximately worked out well.

Edit: actually, info.effectivePlayState == PlayState.Paused will trigger to true at the very beginning of the PlayMode! So you can use either the original approach + Approximately, or a hybrid of seant’s and Approximately, as you will still need to check count > duration if effectivePlayState == Paused.

Final code with Hybrid approach:

    public override void OnBehaviourPause(Playable playable, FrameData info)
    {
        if (!Application.isPlaying)
        {
            return;
        }

        var duration = playable.GetDuration();
        var time = playable.GetTime();
        var count = time + info.deltaTime;

        if ((info.effectivePlayState == PlayState.Paused && count > duration) || Mathf.Approximately((float)time, (float)duration))
        {
            // Execute your finishing logic here:
            Debug.Log("Clip done!");
        }
    }

[/quote]
Yeah, but it is somehow more often happen if the Playable Director Wrap Mode is on Hold.

Nice workaround, tho.

This is EXACTLY what I needed, because I built a custom clip that jumps the timeline backwards to a loop point while it waits for a button press. I was checking in ProcessFrame to see if we were at the end of the clip, but if the device has a lower frame rate, that check was failing. This "end of clip" check lets me catch this situation and rewind the timeline if you have not clicked the button.

    public override void OnBehaviourPause(Playable playable, FrameData info)
    {
        var duration = playable.GetDuration();
        var count = playable.GetTime() + info.deltaTime;

        if ((info.effectivePlayState == PlayState.Paused && count > duration) || playable.GetGraph().GetRootPlayable(0).IsDone())
        {
            // Execute your finishing logic here:
            Debug.Log("Clip done!");
        }
    }
8 Likes

Awesome post. I recently encountered a problem with spine's timeline integration and I'm pretty sure this solves it.

taking 10 minutes instead of days to solve a bug like that is very much appreciated.

taking 10 minutes instead of days to solve a bug like that is very much appreciated.

Updating to the latest Spine Timeline UPM package which already resolved the problem would also have helped ;).