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

No comments:

Post a Comment