Airplay and secondary UIScreen latency

My question is: how to decrease the lag on the secodary screen when using Airplay mirroring?


To illustrate what I am doing, here is some code. To use it, create a new iOS project (single view, Swift). Then copy-paste the following code in ViewController.swift, replacing all that is there by default.

Make sure you have an Apple TV to mirror to, and enable mirroring to it on your iPhone / iPad.

Tap the i-Device's screen to stop the framecounter and observe the latency between iOS device and television. I was filming an iPhone 5 with the television in the background using the slo-mo function of the iPhone 6 Plus to get a good indication of the absolute latency.


The thing is, playing Real Racing 2 on the same device, same network, same Apple TV, same TV, works very very well.

Could there be less lag when using OpenGL / Metal?


The endgoal here is:

I have an array of bytes and I want to draw these on the secondary screen. As this is going to be a game, low-latency in response to touches is required.


Here is the code:


import UIKit


class ViewController: UIViewController, MyViewDelegate
{
    var myView = MyView(frame: CGRectZero) // allow responding to touches directly
    var pause = false // when MyView is touched, this is flipped
    var totalTime = 0 // incremented each frame, displayed on both screens
    var secondScreenLabel = UILabel(frame: CGRectZero)
    var primaryScreenLabel = UILabel(frame: CGRectZero)
    var secondaryWindow = UIWindow(frame: CGRectZero)
    var _secondaryScreen : UIScreen?


    func secondaryDisplayLinkFired(displaylink : CADisplayLink)
    {
        // if not paused, the counter gets incremented and the value
        // is displayed on the labels in both screens
        if (!self.pause)
        {
            self.totalTime++


            self.secondScreenLabel.text = "\(self.totalTime)"
            self.secondScreenLabel.sizeToFit()
            self.secondScreenLabel.center = self.secondaryWindow.center


            self.primaryScreenLabel.text = "\(self.totalTime)"
            self.primaryScreenLabel.sizeToFit()
            self.primaryScreenLabel.center = self.view.center
        }
    }


    func myViewDidBeginTouches(window: MyView, touches: Set<NSObject>)
    {
        // textColors get changed for extra visibility
        self.primaryScreenLabel.textColor = UIColor.redColor()
        self.secondScreenLabel.textColor = UIColor.redColor()


        // this flip will cause the labels to not change anymore
        self.pause = true


    }
    func myViewDidEndTouches(window: MyView, touches: Set<NSObject>)
    {
        // reset the colors
        self.primaryScreenLabel.textColor = UIColor.lightGrayColor()
        self.secondScreenLabel.textColor = UIColor.lightGrayColor()


        // this flip will cause the labels to change their values again
        self.pause = false
    }


    func secondaryScreen () -> UIScreen?
    {
        // find the first screen that is not the mainscreen.
        // We hold on to it, so that calling self.secondaryScreen
        // many times should not be too slow
        if (nil == _secondaryScreen)
        {
            let screens = UIScreen.screens()
            // no more then one screen means there is no
            // secondary screen
            if (screens.count > 1)
            {
                for aScreen in screens
                {
                    let screen = UIScreen.mainScreen() as UIScreen
                    if (aScreen as! UIScreen != screen)
                    {
                        _secondaryScreen = aScreen as? UIScreen
                        break
                    }
                }
            }
        }
        return  _secondaryScreen
    }


    override func viewDidLoad()
    {
        super.viewDidLoad()


        // setup the secondary screen if it is already available
        if (nil != self.secondaryScreen())
        {
            self.screenDidConnect(nil)
        }
        else
        {
            self.screenDidDisconnect(nil)
        }


        // in a real application, self needs to be removed as observer
        // in e.g. deinit
        NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("screenDidConnect:"), name: UIScreenDidConnectNotification, object: nil)
        NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("screenDidDisconnect:"), name: UIScreenDidDisconnectNotification, object: nil)
    }


    override func viewDidAppear(animated: Bool)
    {
        super.viewDidAppear(animated)


        // use myView to capture touches
        myView.frame = self.view.bounds
        myView.delegate = self
        myView.backgroundColor = UIColor.blackColor()
        self.view.addSubview(myView)


        // a label to display the frame count
        self.primaryScreenLabel.text = "\(totalTime)"
        self.primaryScreenLabel.userInteractionEnabled = false
        self.primaryScreenLabel.textColor = UIColor.lightGrayColor()
        self.primaryScreenLabel.font = UIFont.systemFontOfSize(100)
        self.primaryScreenLabel.sizeToFit()
        self.primaryScreenLabel.center = self.view.center
        self.view.addSubview(self.primaryScreenLabel)
    }


    func screenDidConnect(notification : NSNotification?)
    {
        if let secondScreen = self.secondaryScreen()
        {
            // see MainViewController.m in sample code at:
            // https://developer.apple.com/library/ios/samplecode/GLAirplay/Introduction/Intro.html
            self.secondaryWindow.frame = secondScreen.bounds
            self.secondaryWindow.backgroundColor = UIColor.blackColor()


            self.secondaryWindow.makeKeyAndVisible()
            self.secondaryWindow.screen = secondScreen


            self.secondaryWindow .addSubview(self.secondScreenLabel)
            self.secondScreenLabel.text = "\(totalTime)"
            self.secondScreenLabel.textColor = UIColor.lightGrayColor()
            self.secondScreenLabel.font = UIFont.systemFontOfSize(400)
            self.secondScreenLabel.sizeToFit()
            self.secondScreenLabel.center = self.secondaryWindow.center


            if let secondaryScreenDisplayLink = secondScreen.displayLinkWithTarget(self, selector: Selector("secondaryDisplayLinkFired:"))
            {
                secondaryScreenDisplayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
            }
            else
            {
                print("WARNING: could not get a displaylink")
            }
        }
        else
        {
            print("PROGRAMMER ERROR: screenDidConnect: called but there is no second screen")
        }
    }
    func screenDidDisconnect(notification : NSNotification?)
    {
        // in a real application, this should be implemented properly
    }
}


protocol MyViewDelegate
{
    func myViewDidBeginTouches(view : MyView,  touches : Set<NSObject>)
    func myViewDidEndTouches(view : MyView, touches : Set<NSObject>)
}


class MyView : UIView
{
    var delegate : MyViewDelegate?
    override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent)
    {
        self.delegate?.myViewDidBeginTouches(self, touches: touches)
    }
    override func touchesMoved(touches: Set<NSObject>, withEvent event: UIEvent)
    {
    }
    override func touchesCancelled(touches: Set<NSObject>!, withEvent event: UIEvent!)
    {
        self.delegate?.myViewDidEndTouches(self, touches: touches)
    }
    override func touchesEnded(touches: Set<NSObject>, withEvent event: UIEvent)
    {
        self.delegate?.myViewDidEndTouches(self, touches: touches)
    }
}
Airplay and secondary UIScreen latency
 
 
Q