Extrapolated clips and getting normalized time

There were talk in the past about pushing Clip from CreatePlayable on, so you can access proper clip start time and duration in ProcessFrame. Now CreatePlayable was reworked and I can't find a way to access those values.

Was this simplified now? Or is it removed?

The issue is with Extrapolated clips which returns playable.GetDuration() as Infinity and Start as 0;

It was? In what way? I don’t think this has changed since the public release of timeline, but I could be wrong.

We haven’t made changes to push the clip down to the playable asset however. The workaround of assigning clips to the clip playable by overriding TrackAsset.CreateTrackMixer should still work, and in the case of extrapolation, you can use the track duration as a replacement for the end value of the last clip where the extrapolation gives you +infinity.

I have been struggling with the same thing myself :
I have found two different approaches - both feel like work-arounds , and I am quite new to the Timeline system, so I don’t know if I’m doing unnecessary workarounds, or if this is the way you have to do what I am trying to do.

In short - what I want to do :
While playHead is inside a clip - I want a value t in range 0 to 1 , based on clip start and clip end.

var myVal = behaviour.animCurve.Evaluate(t)*behaviour.myData;

If playHead is past the clip end, I want to grab the value as given by

var myVal = behaviour.animCurve.Evaluate(1.0)*behaviour.myData;

Here is a video with both workarounds

That far cleanest solution I have found - when it comes to MixerBehaviourCode is having access to the clip start and clip end : which I had to do by using the work-around described here :

The mixer behaviour code becomes like this :

public void ProcessFrameClipPassthroughAndPlayHeadWorkAround(Playable playable, FrameData info, object playerData)
    {
        ScoreTallyMaster trackBinding = playerData as ScoreTallyMaster;

        if (!trackBinding)
            return;

        int inputCount = playable.GetInputCount ();
        double speed = 1.0;
        float totalEndScore = 0f;
        float currentAccumulatedScore = 0f;
      
        for (int i = 0; i < inputCount; i++)
        {
          
            ScriptPlayable<ScoreTallyBehaviour> inputPlayable = (ScriptPlayable<ScoreTallyBehaviour>)playable.GetInput(i);
            ScoreTallyBehaviour input = inputPlayable.GetBehaviour ();
          
            totalEndScore += input.GetScore();
      
            double playHeadTime = playable.GetGraph().GetRootPlayable(0).GetTime();
            double duration = input.Clip.end - input.Clip.start;
            double playHeadRelativeToClip = playHeadTime - input.Clip.start;
            double tForClip = playHeadRelativeToClip / duration;
            float floatingScore = input.GetScore() * input.easing.Evaluate((float)tForClip);

            if (playHeadTime > input.Clip.end)
            {
                input.ScoreTallyPart.myScore.text = $"{Mathf.Floor(input.GetScore())}";
                currentAccumulatedScore += (input.GetScore());
            }
            else if (playHeadTime < input.Clip.start)
            {
                input.ScoreTallyPart.myScore.text = $"0";
            }
            else
            {
                speed = SpeedFromScore(input.EditorPreviewScore);
                currentAccumulatedScore += (floatingScore);
                input.ScoreTallyPart.myScore.text = $"{Mathf.Floor(floatingScore)}";
            }

        }
      
        if (trackBinding.Total!=null)
        {
            trackBinding.PublishScore(currentAccumulatedScore,totalEndScore);
        }
        playable.GetGraph().GetRootPlayable(0).SetSpeed(speed);
    }

The other solution I found to get this working foregoes the clip start/end workaround, but uses a
“workAroundForHold” boolean in the behaviour - this is turned on on clips that has to fill the gaps between the real clips.
The mixerBehaviour for this method is weirder by magnitudes…

public void ProcessFrameHoldWorkAround(Playable playable, FrameData info, object playerData)
    {
        ScoreTallyMaster trackBinding = playerData as ScoreTallyMaster;

        if (!trackBinding)
            return;

        int inputCount = playable.GetInputCount ();
        float totalWeight = 0.0f;
        float floatingScore = 0f;
        double speed = 1.0;
        float totalEndScore = 0f;
        float currentAccumulatedScore = 0f;

        /*
         * We are counting the clips backwards for the reasons below..
         * if we encounter clip 4 with weight > 0 , we
         * want all clips prior to clip 4 to be at considered as weight 1
         * When we are inside a clip - we want the local time t in 0-1
         * to use with our own AnimationCurve to set easing for our clips.
         */
      
        int playHeadPastClipIndex = -1;
        for (int i = inputCount-1; i >= 0; i--)
        {
          
            ScriptPlayable<ScoreTallyBehaviour> inputPlayable = (ScriptPlayable<ScoreTallyBehaviour>)playable.GetInput(i);
            ScoreTallyBehaviour input = inputPlayable.GetBehaviour ();
          
            bool pastClip = false;
            float inputWeight = playable.GetInputWeight(i);
            if (inputWeight > Double.Epsilon)
            {
                playHeadPastClipIndex = i;
            }
            if (i < playHeadPastClipIndex)
            {
                pastClip = true;
            }

            /* I was unable to get previous values to hold and still get a t=0-1 range with GetTime and GetDuration
             * when there were gaps in the timeline track.
             *
             * I ended up with a pragmatic workaround where a "hold"-clip is inserted instead of gaps in the timeline
             * If the playhead is at a hold clip, just continue the loop
             */
            if (input.workAroundForHold)
            {
                continue;
            }
            if (inputWeight > Double.Epsilon)
            {
                speed = SpeedFromScore(input.EditorPreviewScore) * inputWeight;
            }
            totalEndScore += input.GetScore();
            totalWeight += inputWeight;
          
            double timeForInput = playable.GetInput(i).GetTime();
            double durationForInput = playable.GetInput(i).GetDuration();
            float t = (float) (timeForInput/durationForInput);
            floatingScore = input.GetScore() * input.easing.Evaluate(t);

            if (pastClip)
            {
                input.ScoreTallyPart.myScore.text = $"{Mathf.Floor(input.GetScore())}";
                currentAccumulatedScore += (input.GetScore());
            }
            else if (inputWeight > Double.Epsilon)
            {
                currentAccumulatedScore += (floatingScore);
                input.ScoreTallyPart.myScore.text = $"{Mathf.Floor(floatingScore)}";
            }
            else if (inputWeight < Double.Epsilon)
            {
                input.ScoreTallyPart.myScore.text = $"0";
            }

        }
      
        if (trackBinding.Total!=null)
        {
            if (totalWeight > Mathf.Epsilon || playHeadPastClipIndex!=-1)
            {
                trackBinding.PublishScore(currentAccumulatedScore,totalEndScore);
            }else if (totalWeight < Mathf.Epsilon)
            {
                trackBinding.PublishScore(0f,totalEndScore);
            }
        }
      
        playable.GetGraph().GetRootPlayable(0).SetSpeed(speed);
    }

I would love to know if there is a better way to do things?
If there isn’t a better way, I think I would go for the the Clip-passthrough technique, because that code is just so much easier to read and understand.

There are some built-in timeline features that may help here. Animated parameters and extrapolation,

Instead of storing an animation curve, if you use an animated parameter like in the script below, your animated value is automatically updated before ProcessFrame is called.

This will also expose the extrapolation options in the clip editor, which will allow the user to say how the gap gets filled (hold, loop, etc...) like the animation track.

using System;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

[Serializable]
public class MyPlayableAsset : PlayableAsset, ITimelineClipAsset
{
    [Serializable]
    public class MyBehaviour : PlayableBehaviour
    {
        public float AnimatedValue = 0;
    }

    // this allows the editor to animate any parametes in the behaviour
    public MyBehaviour template = new MyBehaviour();

    public override Playable CreatePlayable(PlayableGraph graph, GameObject go)
    {
        return ScriptPlayable<MyBehaviour>.Create(graph, template);
    }

    // this tells timeline to support extrapolation in gaps
    public ClipCaps clipCaps
    {
        get { return ClipCaps.Extrapolation; }
    }
}

5916110--631961--upload_2020-5-29_11-13-6.png

But once you get to the next clip after the extrapolation - you would no longer get the extrapolated values from the previous clip - or am I mistaken?

So I'm reviving this thread because the concrete answer to the question is still missing.
I would like to get the normalized time from the playable when using extrapolation, but for that I need the "actual duration". GetDuration() is already nicely adjusted accordingly, returning looping and wrapping values, so the "actual duration" has to exist somewhere. How can I get the normalized value?

I think your best bet is this workaround - https://forum.unity.com/threads/how-to-access-the-clip-timing-start-end-time-in-playablebehaviour-functions.494344/#post-3861688