Friday, May 22, 2020

Swift iPhone: Bluetooth communication in the sleep state

A self-made  watch app I've posted in the past found that it doesn't work when the instruction-mediating iPhone is in a sleep state.

It's possible to always run the iPhone app on the front side when the camera is connected via Bluetooth, but that comes at the cost of battery consumption.

Then, I found a way to send recording command by bluetooth to the camera even if the iPhone app is in sleep mode.

Select Signing & Capabilities and press "+Capability" on the left. Next,Select Background Modes from the list that appears.
Choose Use Bluetooth LE accessories and Acts as a Bluetooth LE accessory from the added Modes.
After selecting the above option, run the build. So even if you put your iPhone in sleep mode after connecting to the camera, you can still record remotely from your apple watch. 

Reference information 

Sunday, May 17, 2020

Using swift to run blackmagic camera SDK in iOS Part3

This post is a continuation of the previous one.

This post will describe how to accomplish the following
  • Create an application that can send commands from apple watch to iPhone.
  • Send the recording command from apple watch to the Blackmagic camera via iPhone
Chapter 4: Creating an app for the apple watch.

 The environment is as follows
  • MacOS 10.15.4
  • Xcode 11.3.1
  • ios13.4.1(iPhone6S)
  • watchos 6.2(watch 5th generation)

This time, I made an app based on the following article.


The above article is a mutual communication between iPhone and apple watch, but in this post reduced the function to send from apple watch to Iphone.

First, select a project type as shown below to create an app that can work with iPhone and apple watch. In the old xcode, when you select the right icon, the app will be created automatically between iPhone and apple watch, but in the latest xcode, you can select the left icon to create an app for the iPhone and apple watch, and the right icon to create an app for the apple watch alone.


The following is the code of the ViewController on the iPhone side. It is almost the same as the code of the reference source as contents, and the function of the send button to the apple watch side is changed to the clear of the label only to be different.

ViewController.swift
import UIKit
import WatchConnectivity

class ViewController: UIViewController {

    @IBOutlet var label: UILabel!
    var session: WCSession?
   
    override func viewDidLoad() {
        super.viewDidLoad()
        self.configureWatchKitSesstion()
        // Do any additional setup after loading the view.
    }

    func configureWatchKitSesstion() {
     
      if WCSession.isSupported() {
        session = WCSession.default
        session?.delegate = self
        session?.activate()
      }
    }
   
    @IBAction func clear(_ sender: Any) {
        self.label.text = "Clear"
    }
}

extension ViewController: WCSessionDelegate {

  func sessionDidBecomeInactive(_ session: WCSession) {
  }

  func sessionDidDeactivate(_ session: WCSession) {
  }

  func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  }

  func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
    print("received message: \(message)")
    DispatchQueue.main.async {
      if let value = message["watch"] as? String {
        self.label.text = value
      }
    }
  }
}

I won't write the UI creation, but it's a simple structure with one button and one label.

Next, the code of InterfaceController of apple watch is shown below. This is also almost identical to the Swift Development Center code we're referring to here. The only difference is that I've removed the part that receives the command from the iPhone

InterfaceController.swift
import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController {

    let session = WCSession.default
   
    override func awake(withContext context: Any?) {
        super.awake(withContext: context)
       
        session.delegate = self
        session.activate()
        // Configure interface objects here.
    }
   
    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }
   
    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

    @IBAction func start() {
        let data: [String: Any] = ["watch": "data from watch" as Any]
           session.sendMessage(data, replyHandler: nil, errorHandler: nil)
    }
}

extension InterfaceController: WCSessionDelegate {

  func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  }

  func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
    }
}

I won't write the UI creation, but it's a simple structure with one button.

You can select the installation target when testing the app on the actual device, but please select the iPhone app side. Then the watch side will be automatically installed.

When you start the app with iPhone and watch and push the button on the watch side, the label letter on the iPhone side changes. And after clearing the letter of the label that pushes the button of iPhone, the label letter of iPhone should change automatically if you push the button of watch again!


Chapter 5. Built-in bluetooth communication and recording button

Finally, incorporate the following that you did in the previous chapter into this iPhone app and watch app

  • Add  blackmagic sdk API for iPhone(Embed the API which resolved some errors in the previous chapter for iOS)
  • Implement Bluetooth-related code to connect to the camera
  • add Bluetooth-related UI(tableview & connect button)
  • edit Bluetooth-related info.plist for app
  • The function of the button of the watch app is changed for the recording button.
First, appdelegate adds the same content as the previous chapter. The details are as follows

AppDelegate.swift
import UIKit
import CoreBluetooth

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    let cameraControlInterface = CameraControlInterface()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }

}


Next,Embed bluetooth related code in ViewController as the previous chapter. And add a recording command on the last line that corresponds to the command from apple watch.

ViewController.swift
import UIKit
import WatchConnectivity
import CoreBluetooth

class ViewController: UIViewController,UITableViewDelegate, UITableViewDataSource, InitialConnectionToUIDelegate, IncomingRecordControlToUIDelegate{

    @IBOutlet var label: UILabel!
    var session: WCSession?
    @IBOutlet var cameralist: UITableView!
   
    weak var m_initialConnectionInterfaceDelegate: InitialConnectionFromUIDelegate?
    weak var m_outgoingRecordControlDelegate: OutgoingRecordControlFromUIDelegate?
    var m_displayedCameras = [DiscoveredPeripheral]()
    var m_selectedCameraIdentifier: UUID?
   
    override func viewDidLoad() {
        super.viewDidLoad()
        self.configureWatchKitSesstion()
        // Do any additional setup after loading the view.
       
        cameralist.delegate = self
        cameralist.dataSource = self
    }
   
    override func viewWillAppear(_ animated: Bool) {
        // Register as a InitialConnectionToUIDelegate with the CameraControlInterface,
        // and assign CameraControlInterface as our InitialConnectionFromUIDelegate.

        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        let cameraControlInterface: CameraControlInterface = appDelegate.cameraControlInterface
        cameraControlInterface.m_initialConnectionToUIDelegate = self
        cameraControlInterface.m_recordControlToUIDelegate = self
        m_initialConnectionInterfaceDelegate = cameraControlInterface
        m_outgoingRecordControlDelegate = cameraControlInterface

        // Disconnect any current connections, and start scanning for Bluetooth peripherals.
        m_initialConnectionInterfaceDelegate?.disconnect()
        m_initialConnectionInterfaceDelegate?.refreshScan()

        // Clear cached cameras
        m_displayedCameras.removeAll()
    }

    func updateDiscoveredPeripheralList(_ discoveredPeripheralList: [DiscoveredPeripheral]) {
        // Remove all cached cameras.
        m_displayedCameras.removeAll()

        // Add all cameras from the updated discoveredPeripheralList.
        for peripheral in discoveredPeripheralList {
            m_displayedCameras.append(peripheral)
        }
        // Update UI to display our discovered cameras.
        cameralist.reloadData()
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return m_displayedCameras.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel!.text = m_displayedCameras[indexPath.row].name
        //return cell
        return cell
    }

    func configureWatchKitSesstion() {
     
      if WCSession.isSupported() {
        session = WCSession.default
        session?.delegate = self
        session?.activate()
      }
    }
   
    @IBAction func clear(_ sender: Any) {
        self.label.text = "Clear"
    }
   
    @IBAction func connect(_ sender: Any) {
        let selectedIndex = 0
               if selectedIndex >= 0 {
                   let uuid: UUID = m_displayedCameras[selectedIndex].peripheral.getPeripheralIdentifier()
                   m_initialConnectionInterfaceDelegate?.attemptConnection(to: uuid)
               }
    }
   
}

extension ViewController: WCSessionDelegate {

  func sessionDidBecomeInactive(_ session: WCSession) {
  }

  func sessionDidDeactivate(_ session: WCSession) {
  }

  func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
  }

  func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
    DispatchQueue.main.async {
      if let value = message["watch"] as? String {
        self.label.text = value
      }
        self.m_outgoingRecordControlDelegate?.onRecordPressed()
    }
  }
}

Then add NSBluetoothAlwaysUsageDescription and NSLocationWhenInUseUsageDescription to info.pilist for app permission setting.
    <key>UILaunchStoryboardName</key>
    <string>LaunchScreen</string>
    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>for camera control</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>I will need your location when app in use</string>

    <key>UIMainStoryboardFile</key>
    <string>Main</string>
    <key>UIRequiredDeviceCapabilities</key>

Here is the code for the InterfaceController.shift of watch, and  I added and edited the following red text so that the color of the recording button changes alternately between black and red each time the button is pressed.

InterfaceController.shift
import WatchKit
import Foundation
import WatchConnectivity

class InterfaceController: WKInterfaceController {

    let session = WCSession.default
    var buttonstaus = 1
    @IBOutlet var recbutton: WKInterfaceButton!

    override func awake(withContext context: Any?) {
        super.awake(withContext: context)

        session.delegate = self
        session.activate()
        // Configure interface objects here.
    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        super.willActivate()
    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

    @IBAction func start() {
        let data: [String: Any] = ["watch": "data from watch" as Any]
        session.sendMessage(data, replyHandler: nil, errorHandler: nil)
        if buttonstaus == 1 {
            recbutton.setBackgroundColor(UIColor.red)
            buttonstaus = 0
        } else {
            recbutton.setBackgroundColor(UIColor.black)
            buttonstaus = 1

        }
    }
}

extension InterfaceController: WCSessionDelegate {

    func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
    }

    func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
    }
}

After the above implementation is complete, the following procedure is used on the actual machine

1.Build and run with xcode
2.Turn on the blackmagic camera and make sure the camera's bloooth is turned on.
3.Make sure you see blackmagic's camera on your Iphone screen
  If you don't see the blackmagic camera on your iPhone here, make sure your iPhone's bluetooth is turned on

4.Pairing with the camera by pressing the iPhone connection button (1)
If you have been paired with a blackmagic camera in the past, the pairing will be completed automatically and you will see the following
When pairing for the first time, enter the six-digit number displayed on the camera into the dialog displayed on the iPhone.
iPhone display when pairing
When the pairing is successful, the camera display switches to the iPhone connection display.
If you are ready to go this far, start the application you created this time on the apple watch side, press the button on the apple watch to start recording on the camera side, and press the button again to stop recording.


Remaining issues.
  1. I know that the following errors appear in the log when connecting with blackmagic camera, but I haven't solved them yet. 
     PacketReader.swift: 40 
     Failed to decode CCU packet: PacketValidationFailed      Failed to decode CCU packet: PayloadConversionFailed

     CCUDecodingFunctions.swift:93
     Payload expected count (1) not equal to converted count (2)

  2. I tried to connect apple watch and blackmagic camera directly, but the pairing fails with the following condition.
  • When the pairing request from apple watch, the  blackmagic camera's display only shows for a second "Connection failed Please try again" but the 6-digit number is not displayed this time.
  • There is no dialog to enter the pairing number on the apple watch side.
  •  xcode log : PeripheralInterface.swift: 203 CBPeripheralDelegate::didWriteValueFor Error Domain=CBATTErrorDomain Code=15
 
 


watch app Icons made by Freepik from www.flaticon.com

Friday, May 15, 2020

Using swift to run blackmagic camera SDK in iOS Part2

This post is a continuation of the previous one, this time using the previously modified SDK and implementing the modifications to work on iOS.

Chapter 3: Edit the blackmagic SDK for  iOS
  1. Create a new project for iOS.
  2. Add only the last modified API of blackmagic SDK to the project file.
  3. Make the necessary modifications to make it work in iOS

Create a new project for iOS 


Add the Apple folder shown below to the project in the last edited blackmagic SDK.


To add them, check "Copy itmes if needed" and "Create groups" as indicated by the red arrow above
Edit the API file in the Apple folder for iOS.

Next time, RUN, there are five errors.( or some time six error)



Enter import UIKit In the following files


  • Utility/StringFunctions.swift 
  • Camera/PeripheralInterface.swift 
Then the error is reduced to tow.

Quit XCODE once you have reached this point.
And again, open the project for editing.
And that leaves six errors.

Then press the Fix button to fix the 'NSFontAttributeName' has been renamed to 'NSAttributedString.Key.font' error that remains in "StringFunctions.swift "


Next time you run with 5 errors, the error will disappear and the build will complete.


Chapter 4: Create an app to connect your IPhone to blackmagic pocket cinema camera

The procedure is as follows


  1. Creating the UI
  2. Bluetooth related implementations
  3. Edit info.plist for app permissions

Select "Main.storyboard" and place it in order of 1) Button 2) Table View 3) Table Cell. The table cells should be arranged so that they fit inside the table view.


Connecting Table Views and Buttons to ViewController

Select Inspectors from the View menu and select "Show Attributes Inspector.

Select the table view cell and edit the identifier to the cell

After editing, it looks like this

Next is the first part of editing ViewController.
In the following conditions, the error of the Tableview occurs, but it is not fixed yet.

import UIKit
import CoreBluetooth

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, InitialConnectionToUIDelegate,IncomingRecordControlToUIDelegate {

@IBOutlet var cameralist: UITableView!

weak var m_initialConnectionInterfaceDelegate: InitialConnectionFromUIDelegate?
weak var m_outgoingRecordControlDelegate: OutgoingRecordControlFromUIDelegate?
var m_displayedCameras = [DiscoveredPeripheral]()
var m_selectedCameraIdentifier: UUID?


override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.

cameralist.delegate = self
cameralist.dataSource = self

}
The red text is the part I added.
The parameter initialization in the middle is quoted from the following blackmagic SDK SelectViewController.swift



Here is the code for the bluetooth related initialization and finding the camera part It's almost the same here from the Blackmagic SDK SelectViewController.swift
override func viewWillAppear(_ animated: Bool) {

// Register as a InitialConnectionToUIDelegate with the CameraControlInterface,
// and assign CameraControlInterface as our InitialConnectionFromUIDelegate.
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let cameraControlInterface: CameraControlInterface = appDelegate.cameraControlInterface
cameraControlInterface.m_initialConnectionToUIDelegate = self
cameraControlInterface.m_recordControlToUIDelegate = self
m_initialConnectionInterfaceDelegate = cameraControlInterface
m_outgoingRecordControlDelegate = cameraControlInterface

// Disconnect any current connections, and start scanning for Bluetooth peripherals.
m_initialConnectionInterfaceDelegate?.disconnect()
m_initialConnectionInterfaceDelegate?.refreshScan()

// Clear cached cameras
m_displayedCameras.removeAll()
}

To fix the error displayed in cameraControlInterface, add a AppDelegate.swift as follows
import UIKit
import CoreBluetooth

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    let cameraControlInterface = CameraControlInterface()
The red text is the part I added.
If you run Clean Build Folder after editing the above, the ViewController.swift "Value of type 'AppDelegate' has no member 'cameraControlInterface'error" will be resolved.

Next, we'll add the update part of the peripheral information in Bluetooth. Here's another quote from SelectViewController.swift


func updateDiscoveredPeripheralList(_ discoveredPeripheralList: [DiscoveredPeripheral]) {
        // Remove all cached cameras.
        m_displayedCameras.removeAll()

        // Add all cameras from the updated discoveredPeripheralList.
        for peripheral in discoveredPeripheralList {
            m_displayedCameras.append(peripheral)
        }

        // Update UI to display our discovered cameras.
        cameralist.reloadData()
    }

Then add the following code to get the number of rows of tableview to display the list of cameras found in bluetooth and to specify the relationship with the display cell of tableview

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return m_displayedCameras.count
    }

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel!.text = m_displayedCameras[indexPath.row].name
     return cell
    }

Adding the above code will also resolve the ViewController error 'ViewController 'does not conform to protocol 'UITableViewDataSource'

Then, open the source code of info.plist add the key of the NSBroutothAlwaysUsageDescription as follows, and write the appropriate reason in <strings>



        <string>UIInterfaceOrientationPortrait</string>
        <string>UIInterfaceOrientationPortraitUpsideDown</string>
        <string>UIInterfaceOrientationLandscapeLeft</string>
        <string>UIInterfaceOrientationLandscapeRight</string>
    </array>
    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>for camera control</string>
</dict>
</plist>

If there are no errors left after all the above code edits, it can be executed. Connect the real iPhone and run the test.

Select OK because the following dialog is displayed when the code is correctly transferred to the actual device.


Next, turn on the power of Blackmagic Pocket cinema camera and check if Bluetooth is turned on from Setup menu.
After that, if the iPhone app is running correctly, the name of the camera will be displayed on the screen as follows

Here is the code for the button to pair with the camera

@IBAction func connect(_ sender: Any) {
        let selectedIndex = 0
            if selectedIndex >= 0 {
                let uuid: UUID = m_displayedCameras[selectedIndex].peripheral.getPeripheralIdentifier()
                m_initialConnectionInterfaceDelegate?.attemptConnection(to: uuid)
            }

    }

Add the following red font code for the button code you connected in the beginning. This time, the code assumes that there is only one camera. If you have more than one camera and need to select one of them, you need to modify the second line, let selectedIndex = 0

After adding the code to connect the camera, build and run the app, make sure the camera's Bluetooth is turned on as before, make sure the camera name is displayed on the Iphone, and press the button on the iPhone.

When the pairing process has started properly, the camera will display the pair number below, and the pairing dialog will pop up on your IPhone.


If you enter the number displayed on the camera with your iPhone, pairing will be performed, and the following message will appear on the camera to show that the pairing was done correctly.


Finally, add a button for recording in the UI Builder, connect to the ViewController, and add the following code.

@IBAction func recStartstop(_ sender: Any) {
         m_outgoingRecordControlDelegate?.onRecordPressed()
    }

Once the app is running correctly and pairing with the camera has been performed, pressing the added button will alternate between Start/Stop recording.

The entire code is described below. Note that the code is only for normal systems, so abnormal system processing is not included.

ViewController.swift
import UIKit
import CoreBluetooth

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, InitialConnectionToUIDelegate, IncomingRecordControlToUIDelegate {

    @IBOutlet var cameralist: UITableView!

    weak var m_initialConnectionInterfaceDelegate: InitialConnectionFromUIDelegate?
    weak var m_outgoingRecordControlDelegate: OutgoingRecordControlFromUIDelegate?
    var m_displayedCameras = [DiscoveredPeripheral]()
    var m_selectedCameraIdentifier: UUID?

    override func viewDidLoad() {

        super.viewDidLoad()
        // Do any additional setup after loading the view.
        cameralist.delegate = self
        cameralist.dataSource = self
    }

    override func viewWillAppear(_ animated: Bool) {

        // Register as a InitialConnectionToUIDelegate with the CameraControlInterface,
        // and assign CameraControlInterface as our InitialConnectionFromUIDelegate.

        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        let cameraControlInterface: CameraControlInterface = appDelegate.cameraControlInterface

        cameraControlInterface.m_initialConnectionToUIDelegate = self
        cameraControlInterface.m_recordControlToUIDelegate = self

        m_initialConnectionInterfaceDelegate = cameraControlInterface
        m_outgoingRecordControlDelegate = cameraControlInterface

        // Disconnect any current connections, and start scanning for Bluetooth peripherals.
        m_initialConnectionInterfaceDelegate?.disconnect()
        m_initialConnectionInterfaceDelegate?.refreshScan()

        // Clear cached cameras
        m_displayedCameras.removeAll()

    }

    func updateDiscoveredPeripheralList(_ discoveredPeripheralList: [DiscoveredPeripheral]) {

        // Remove all cached cameras.
        m_displayedCameras.removeAll()

        // Add all cameras from the updated discoveredPeripheralList.
        for peripheral in discoveredPeripheralList {
            m_displayedCameras.append(peripheral)
        }

        // Update UI to display our discovered cameras.
        cameralist.reloadData()

    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return m_displayedCameras.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel!.text = m_displayedCameras[indexPath.row].name
        return cell
    }

    @IBAction func connect(_ sender: Any) {
        let selectedIndex = 0
        if selectedIndex >= 0 {
            let uuid: UUID = m_displayedCameras[selectedIndex].peripheral.getPeripheralIdentifier()
            m_initialConnectionInterfaceDelegate?.attemptConnection(to: uuid)
        }
    }

    @IBAction func recStartstop(_ sender: Any) {
        m_outgoingRecordControlDelegate?.onRecordPressed()
    }
}

AppDelegate.swift
import UIKit
import CoreBluetooth

@UIApplicationMain

class AppDelegate: UIResponder, UIApplicationDelegate {

    let cameraControlInterface = CameraControlInterface()

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
    }
}

Don't forget to add and edit the APIs provided by Blackmagic, as well as the Info.plist I wrote at the beginning!