#pragma mark DisplayLink - RTP PTS timestamp-based frame pacing (experimental)

// TODO: handle presentationTimeUs rollover every 13 hours

// This frame pacing method attempts to use timestamps from Sunshine to determine when
// frames should be displayed. Sunshine v2025.600+ required.
- (void)framePacingUsingTimestamps:(CADisplayLink *)link {
    // All times are in seconds
    static CFTimeInterval anchorLocal = 0.0f;
    static CMTime anchorHost;
    static CFTimeInterval lastTargetLocal = 0.0f;

    CFTimeInterval start = link.timestamp;
    CFTimeInterval deadline = link.targetTimestamp;
    _displayRefreshRate = 1.0f / (deadline - start);

    [self checkDisplayLayer];

    Frame *frame = nil;

    CFTimeInterval dl0 = CACurrentMediaTime();
    static CFTimeInterval avgOverhead = 0.004f; // averaged each callback
    CFTimeInterval waitFor = deadline - dl0 - avgOverhead;
    if (waitFor < 0.001f) {
        waitFor = 0.001f;
    }

    // Process the next frame for display
    frame = [_frameQueue dequeueWithTimeout:waitFor];
    if (frame) {
        CFTimeInterval now = CACurrentMediaTime();

        // during stream startup, wait and let the first frames through untouched
        if (!_seenFrameOne) {
            Log(LOG_I, @"Frame pacing (pts mode): target %f Hz with %d FPS stream", _displayRefreshRate, self->frameRate);
            _seenFrameOne = YES;

            // Just let frame 1 through, we don't want to anchor to it
            Log(LOG_I, @"[%.3f] rendering frame %d / %.3f, queue size %d",
                deadline, frame.frameNumber, frame.pts, [_frameQueue count]);
            [self renderFrame:frame atTime:frame.pts90];
            return;
        }

        if (!CMTIME_IS_VALID(anchorHost)) {
            // Use this frame as the anchor point. This is normally frame 2.
            anchorHost = frame.pts90;
            anchorLocal = now;
            Log(LOG_I, @"Setting anchor point: anchorHost=%.3f == anchorLocal=%.3f",
                CMTimeGetSeconds(anchorHost), anchorLocal);
        }

        //CMTime targetTime = CMTimeSubtract(frame.pts90, frameQueue.ptsCorrection); // total of all dropped frames
        CMTime targetTime = frame.pts90;
        CFTimeInterval targetLocal = CMTimeGetSeconds(targetTime);

#ifdef DISPLAYLINK_VERBOSE
        // iOS double-buffering renders the frame we queue here during the interval +2 from now
        CMTime hostDelta = CMTimeSubtract(frame.pts90, anchorHost);
        CFTimeInterval hostDeltaF = CMTimeGetSeconds(hostDelta);
        CMTime drift = CMTimeSubtract(hostDelta, CMTimeMakeWithSeconds(now - anchorLocal, NSEC_PER_SEC));
        Log(LOG_I, @"[%.3f] rendering frame %d / %.3f @ %.3f, drift %.3f, queue size %d",
            deadline - anchorLocal, frame.frameNumber, hostDeltaF, targetLocal,
            CMTimeGetSeconds(drift), [_frameQueue count]);
#endif

        // Schedule the frame at its original timestamp, adjusted by a correction factor (total duration of all dropped frames)
        [self renderFrame:frame atTime:targetTime];

        // Update metrics
        if (lastTargetLocal != 0) {
            [self->_callbacks observeFloat:PLOT_FRAMETIME value:(targetLocal - lastTargetLocal) * 1000.0];
        }
        lastTargetLocal = targetLocal;

        // weighted moving average of how much time displayLink needs after dequeuing a frame.
        // This is used to avoid overshooting a vsync by waiting too long.
        const double alpha = 0.1f;
        avgOverhead = ((CACurrentMediaTime() - dl0) * alpha) + (avgOverhead * (1.0 - alpha));
    }

    // are we returning too late?
    CFTimeInterval dl2 = CACurrentMediaTime();
    if (dl2 > deadline) {
        Log(LOG_W, @"*** returned past deadline by %.3f ms (%f > %f)", (dl2 - deadline) * 1000.0, dl2, deadline);
    }
}

