Giter VIP home page Giter VIP logo

swift-atem's Introduction

Swift 5.1 Platforms: macOS & Linux

Atem network protocol implementation

Implementation of BlackMagicDesign's ATEM communication protocol in Swift. It is written on top of Apple's networking library NIO and implements both sides of the protocol: the control panel and the switcher side. This means that you can not only use it to control atem switchers but also to connect to your control panels without the need for a switcher. Opening a whole new world of applications for the Atem control panels. An example can be found at Atem-Simulator

Starting from version 1.0.0 this package uses Swift 5 and NIO2.

Tested platforms

  • macOS 10.14.6 on a MacBook Pro retina 15" late 2013
  • macOS 10.15.3 on a MacBook Pro retina 15" late 2013
  • Raspbian GNU/Linux 9 stretch on a Raspberry Pi model 3 B
  • Raspbian GNU/Linux 10 Buster on a Raspberry Pi 4 model B Rev 1.2

Installation

When starting a new project: create a Swift package via SPM

# Shell
> swift package init # --type empty|library|executable|system-module

Then add this library to the package description's dependencies

.package(url: "https://github.com/Dev1an/Swift-Atem", from: "1.0.0")

And resolve this new dependency

# Shell
> swift package resolve

Finally import the Atem module in your code

import Atem

You are now ready to create atem controllers and switchers ๐Ÿ˜Ž !

Usage

After looking at the following examples, study the API reference for more details.

Controller

This example shows how to create a controller that connects to a swicther at ip address 10.1.0.67 and print a message whenever the preview bus changes.

try Controller(ipAddress: "10.1.0.67") { connection in
  connection.when{ (change: PreviewBusChanged) in
    print(change) // prints: 'Preview bus changed to input(x)'
  }
}

Sending messages

To send a message to the switcher use the send(...) method like this:

controller.send(message: ChangeTransitionPosition(to: 5000))

Switcher

The following example shows how to emulate the basic functionality of an atem switcher. It will forward incoming messages containing transition and preview & program bus changes to all connected controllers.

This snippet is also included in a seperate SPM target "Simulator" (./Sources/Simulator) and can be run by simply executing swift run Simulator in the terminal.

let switcher = Switcher { controllers in
  controllers.when { (change: ChangePreviewBus, _) in
    controllers.send(
      PreviewBusChanged(
        to: change.previewBus,
        mixEffect: change.mixEffect
      )
    )
  }
  controllers.when{ (change: ChangeProgramBus, _) in
    controllers.send(
      ProgramBusChanged(
        to: change.programBus,
        mixEffect: change.mixEffect
      )
    )
  }
  controllers.when { (change: ChangeTransitionPosition, _) in
    controllers.send(
      TransitionPositionChanged(
        to: change.position,
        remainingFrames: 250 - UInt8(change.position/40),
        mixEffect: change.mixEffect
      )
    )
  }
  controllers.when { (change: ChangeAuxiliaryOutput, _) in
    controllers.send(
      AuxiliaryOutputChanged(
        source: change.source,
        output: change.output
      )
    )
  }
}

swift-atem's People

Contributors

cattivole avatar dev1an avatar maxbaeumle avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

swift-atem's Issues

Error while trying to connect and send message to switcher

I'm having a problem trying to get the controller connected properly so I can send messages:

if let controller = try? Controller(ipAddress: "10.0.1.50") {
	print("Before sending message")
	controller.send(message: ChangeTransitionPosition(to: 5000))
}

This error occurs right after calling print in the EventLoopGroup extension of NIO.

NIO-ELT-1-#0 (10): Fatal error: BUG DETECTED: syncShutdownGracefully() must not be called when on an EventLoop.
Calling syncShutdownGracefully() on any EventLoop can lead to deadlocks.
Current eventLoop: SelectableEventLoop { selector = Selector { descriptor = 3 }, thread = NIOThread(name = NIO-ELT-1-#0) }

Do you have a sample app that uses the Swift-Atem package to send one or two commands to a switcher?

public name and model in Config:ProductInfo

I am using the latest master and would like to obtain how many hardware input (ie: HDMI in) for ATEM mini (for example, ATEM mini has 4 whilst ATEM mini extreme has 8).

I can't find any useful information except ProductInfo with name and model enum so that I can obtain it by myself.

Is it possible to public those variables in the master in the future?

public struct ProductInfo: SerializableMessage {
public static let title = Message.Title(string: "_pin")
static let namePosition = 0..<40
static let tooLongNameCount = namePosition.count + 1
static let truncationDots = Array("...".utf8)
static let modelPosition = 40
/// The name of the product
let name: String
/// The model of the product
let model: Model

Getting current Preview / Program Bus index

Hi all, I'm currently developing iOS application to monitor program/preview bus.
I've noticed change: ChangePreviewBus and change: ChangeProgramBus can monitor the program/preview bus changes action.

However, may I ask is there anyway to obtain current preview or program bus index so that the UI can response with which bus is currently selecting? To be precise, I am planning to develop a tally iOS application

One more thing, I've noticed there is a while true action in sample PreviewSwitcher. Since iOS can't have such a infinite loop, may I how is there any workaround to retain the controller instance so that it can receive connection.when(change: ChangeXXXBus) action?

Many Thanks

Connected to Switcher

Hi,
I've been looking through your library and I can't seem to figure it out, but is there a way to tell if the controller is connected to a switch? Or if it disconnects from a switch?

Thanks-

Swift 5 iOS incompatibility

When compiling a test project with dependency .package(name: "Atem", url: "https://github.com/Dev1an/Swift-Atem", from: "1.0.0"), and target iOS I get the following compile error:

.../SourcePackages/checkouts/Swift-Atem/Sources/Atem/Connection.swift:101:33: error: 'random()' is unavailable in Swift: Use arc4random instead.
                let randomNumber = UInt32(Int(random()) % Int(UInt16.max))
                                              ^~~~~~
Darwin.random:2:13: note: 'random()' has been explicitly marked unavailable here
public func random() -> Int
            ^

Replacing random() with arc4random() fixes this problem.

Xcode: 11.6
Apple Swift version 5.2.4

`ConnectionInitiationEnd` message should be public

When I initialize the controller, I would like to know once the init status is OK.
The documentation says :

When this connection initiation process is finished the ConnectionInitiationEnd message will be sent. From that moment on you know that a connection is succesfully established.

But ConnectionInitiationEnd is not declared public, so the following code does not compile.

self.controller = try Controller(ipAddress: "192.168.0.48") { connection in
      connection.when{ (message: ConnectionInitiationEnd) in
          print(message)
      }
}

Solution: add the public visibility to ConnectionInitiationEnd struct.

New Release from Master

Fantastic work on this repo, thanks!

I see there's a few new features that are not yet in a release (e.g. being able to listen to disconnects) Will these new changes be put in a formal release, soon?

Uploading images on iOS

I'm trying to upload a PNG image to the Media Pool on iOS but am getting stuck with what I believe to be the correct value to set for the uncompressedSize parameter to uploadStill. See my code below:

The resizeImage function just resizes the image to fit within a 1920x1080 box. The same problem occurs if I try to send the original UIImage.

Looking at your TitleGenerator example, you're setting the size of the uncompressedData to be 1920x1080x4. When I do this, or use the actual byte count from the PNG data, the ATEM gets stuck and unresponsive to future uploads.

Any thoughts on what might be happening?

func uploadStill(at index: Int?, image: UIImage) {
	guard let slot = index,
		let resizedImage = self.resizeImage(image: image, targetSize: CGSize(width: 1920, height: 1080)),
		let pngData = resizedImage.pngData()
	else { return }

	let imageSize: CGSize = resizedImage.size
	let width = imageSize.width
	let height = imageSize.height
		
	let numBytes = UInt32(pngData.count)
		
	let yuvData = Media.encodeRunLength(rgbData: pngData)
	let numYuvDataBytes = yuvData.count
				
	let size: UInt32 = UInt32(width * height * 4 * resizedImage.scale)

	// controller should be defined outside of this method. Neither of these two methods work.

	controller.uploadStill(slot: UInt16(slot), data: yuvData, uncompressedSize: numBytes)

//	controller.uploadStill(slot: UInt16(slot), data: yuvData, uncompressedSize: size)
}

Does not work with newer versions of Atem Software Control

I got everything up and running but the Atem Software Control prompts saying "Your Switcher requires a software update" and launches the setup where it cant do anything.
image

I would just use older versions of Atem Software Control but they dont run on newer versions of macos.

Package XYZ already read

Sometimes when I connect to the ATEM (mostly with the simulator), the switcher continually sends the connection the "package XYZ already read" message to the console (they never stop).

Other times, the controller connects to the switcher without any problem and the console just outputs messages normally.

Is this something to be concerned about?

Missing public initializer for `Message.Title`

The public struct MessageTitle (renamed Message.Title on master) does not expose its init method publicly, preventing the developer to create new Atem Messages.

For example, here's the implementation of a VideoOutput command, driving the Atems video output.

public struct VideoOutput: Serializable {
    public enum Output {
        case in1
        case in2
        case in3
        case in4
        case multiview
        case program
        case preview
    }
    let output: Output
    
    public static var title = MessageTitle(string: "CAuS")
        
    public init(with bytes: ArraySlice<UInt8>) throws {
        throw AtemMessageError.notImplemented
    }
    
    public init(output: Output) {
        self.output = output
    }

    public var dataBytes: [UInt8] {
        return switch output {
        case .in1: [1, 0, 0, 1]
        case .in2: [1, 0, 0, 2]
        case .in3: [1, 0, 0, 3]
        case .in4: [1, 0, 0, 4]
        case .multiview: [1, 0, 35, 41]
        case .program: [1, 0, 39, 26]
        case .preview: [1, 0, 39, 27]
        }
    }
    
    public var debugDescription: String {
        return "Video output set to \(output)"
    }
}

Without the public MessageTitle init, the compiler produces the following error:

'MessageTitle' initializer is inaccessible due to 'internal' protection level

Adding a public MessageTitle initializer would make this possible.

Xcode 14 Beta and fatal error when initializing FixedWidthIntegers

Running the Xcode 14 beta, I'm seeing fatal errors when initializing variables from messages sent from the ATEM. Seems like the code here is triggering the error:

init(from slice: ArraySlice<UInt8>) {
	self.init(bigEndian: slice.withUnsafeBufferPointer {
		$0.baseAddress!.withMemoryRebound(to: Self.self, capacity: 1) {$0.pointee}
	})
}

Building the same code using Xcode 13.4.1 does not result in an error when running.

This particular message causes an error to happen is AEBP which returns information when a Fairlight Audio Mixer Source Equalizer Band has changed:

extension Message.Did {
	/// Informs a controller that a source's equalizer settings have changed
	public struct ChangeFairlightMixerSourceEqualizerBand: SerializableMessage {
		public static let title = Message.Title(string: "AEBP")

		public let index: AudioSource
		public let sourceId: Int64
		public let band: UInt8
		public let bandEnabled: Bool
		public let supportedShapes: FairlightEqualizerBandShape
		public let shape: FairlightEqualizerBandShape
		public let supportedFrequencyRanges: FairlightEqualizerFrequencyRange
		public let frequencyRange: FairlightEqualizerFrequencyRange
		public let frequency: UInt32
		public let gain: Int32
		public let qFactor: Int16

		public init(with bytes: ArraySlice<UInt8>) throws {
			self.index = AudioSource(rawValue: UInt16(from: bytes[relative: Position.index]))
			self.sourceId = Int64(from: bytes[relative: Position.sourceId])
			self.band = bytes[relative: Position.band]
			self.bandEnabled = bytes[relative: Position.bandEnabled].firstBit
			self.supportedShapes = FairlightEqualizerBandShape(rawValue: bytes[relative: Position.supportedShapes])
			self.shape = FairlightEqualizerBandShape(rawValue: bytes[relative: Position.shape])
			self.supportedFrequencyRanges = FairlightEqualizerFrequencyRange(rawValue: bytes[relative: Position.supportedFrequencyRanges])
			self.frequencyRange = FairlightEqualizerFrequencyRange(rawValue: bytes[relative: Position.frequencyRange])
			self.frequency = UInt32(from: bytes[relative: Position.frequency])
			self.gain = Int32(from: bytes[relative: Position.gain])
			self.qFactor = Int16(from: bytes[relative: Position.qFactor])
		}

		public init(index: AudioSource, sourceId: Int64, band: UInt8, bandEnabled: Bool, supportedShapes: FairlightEqualizerBandShape, shape: FairlightEqualizerBandShape, supportedFrequencyRanges: FairlightEqualizerFrequencyRange, frequencyRange: FairlightEqualizerFrequencyRange, frequency: UInt32, gain: Int32, qFactor: Int16) {
			self.index = index
			self.sourceId = sourceId
			self.band = band
			self.bandEnabled = bandEnabled
			self.supportedShapes = supportedShapes
			self.shape = shape
			self.supportedFrequencyRanges = supportedFrequencyRanges
			self.frequencyRange = frequencyRange
			self.frequency = frequency
			self.gain = gain
			self.qFactor = qFactor
		}

		public var dataBytes: [UInt8] {
			.init(unsafeUninitializedCapacity: 36) { (buffer, count) in
				buffer.write(index.rawValue.bigEndian, at: Position.index.lowerBound)
				buffer.write(sourceId.bigEndian, at: Position.sourceId.lowerBound)
				buffer[Position.band] = band
				buffer[Position.bandEnabled] = bandEnabled ? 1 : 0
				buffer[Position.supportedShapes] = supportedShapes.rawValue
				buffer[Position.shape] = shape.rawValue
				buffer[Position.supportedFrequencyRanges] = supportedFrequencyRanges.rawValue
				buffer[Position.frequencyRange] = frequencyRange.rawValue
				buffer.write(frequency.bigEndian, at: Position.frequency.lowerBound)
				buffer.write(gain.bigEndian, at: Position.gain.lowerBound)
				buffer.write(qFactor.bigEndian, at: Position.qFactor.lowerBound)
				count = 36
			}
		}

		public var debugDescription: String { return """
Did.ChangeFairlightMixerSourceEqualizerBand {
	index: \(index),
	sourceId: \(sourceId),
	band: \(band),
	bandEnabled: \(bandEnabled),
	supportedShapes: \(supportedShapes),
	shape: \(shape),
	supportedFrequencyRanges: \(supportedFrequencyRanges),
	frequencyRange: \(frequencyRange),
	frequency: \(frequency),
	gain: \(gain),
	qFactor: \(qFactor),
}
"""
		}
		
		enum Position {
			static let index = 0..<2
			static let sourceId = 8..<16
			static let band = 16
			static let bandEnabled = 17
			static let supportedShapes = 18
			static let shape = 19
			static let supportedFrequencyRanges = 20
			static let frequencyRange = 21
			static let frequency = 24..<28
			static let gain = 28..<32
			static let qFactor = 32..<34
		}
	}
}

and the code to call it out:

import Foundation
import Atem

var enabled: Bool = false

let address: String
if CommandLine.arguments.count > 1 {
	address = CommandLine.arguments[1]
} else {
	print("Enter switcher IP address: ", terminator: "")
	address = readLine() ?? "10.1.0.210"
}
print("Trying to connect to switcher with IP address", address)

let controller = try Controller(ipAddress: address) { connection in
	connection.when { (change: Did.ChangeFairlightMixerSourceEqualizerBand) in
		print(change)
	}

	connection.when { (connected: Config.InitiationComplete) in
		print(connected)
		print("Type 0 or 1 and <enter> to change the Audio Follow Video setting.")
	}
	
	connection.whenDisconnected = {
		print("Disconnected")
	}
}

while true {
	_ = readLine() ?? "1"
}

Downloading Images from ATEM

Now that I've gotten stills to upload properly to the ATEM, what's the procedure for downloading them, given an index?

Is there a DownloadManager class that's yet to be written that would take advantage of the decodeRunLength method you've implemented in the Media enum?

ATEM Looses connection

I have created a controller to manage the camera settings on a ATEM 2 M/E Production Studion 4k.
The controller application is running on MacOS 10.15.7.
I have tried ATEM Software versions 8.1, 8.2, 8.2.1, 8.5 and 8.6
I have also used Swift-ATEM versions 1.1.1 and the master from 3/4/2021
The only difference is the Swift-NIO was upgraded from 2.18.0 to 2.26.0.

The application interfaces with a camera shader via TCP connections to adjust camera GAIN using ATEM CameraControl Commands. I added my own extensions for CameraControl. Just connecting to the ATEM (no connection from the shader yet), the app runs for about 2 minutes and then gets:

  • ATEM Disconnected (this is my message on connection.whenDisconnected)
    lost connection due to too many unacknowbedged packets

After the disconnect, the real console is unresponsive until I stop my controller app.

Initial Connection debug logs, before the disconnect:
Trying to connect to ATEM: 172.18.0.240...
channel active
reconnecting
connected, now retreiving initial state
connected using id [129, 4]
Version: 2.30
InCm [1, 0, 0, 0]

  • ATEM: Connected

Not able to perform auto transition.

I tried to switch camera using Auto mode but not able to perform in Switcher, anyone can help me to perform auto switch for specific camera(1 to 8) using 1.5 second transition?

What am I doing wrong?

Hi Dev1an,

Your Swift-Atem package look amazing but I just cannot get it working. I must be doing something wrong as the control software will connect to my Atem Mini and so will your NodeJS Atem package.

My code is very simple:

        let controller = try? Controller(ipAddress: "192.168.68.240") { connection in
            connection.when { (connected: Config.InitiationComplete) in
                print(connected)
                print("Type a number and press <enter> to change the current preview")
            }
            connection.whenDisconnected = {
                print("Disconnected")
            }
        }
        
        controller?.send(message: Do.ChangeProgramBus(to:VideoSource.input(2)))

I keep getting the following in the output:

channel active
๐Ÿ‘น Not sending because connectionstate is nil
๐Ÿ›‘ Shutting down connection [IPv4]192.168.68.240:9910
channel inactive
Disconnected
lost connection due to channel inactive

I guess the issues here is the fact that the logging isn't really telling me anything. Is there a way to enable full logging that might give me a clue why this isn't working?

Thanks,
Simon

Embed inside macOS iOS app

Hi, I was trying this out and so far have been unable to connect to an ATEM. The only reason I can think of is that it's on a different subnet (10.10.0.x) and I've only been able to try it from another subnet (10.20.0.x). Traffic is routed between the two, and the native ATEM control software can connect just fine.

Do you know of any reason that shouldn't work? Has this project been used with a real ATEM recently? I wouldn't think it would matter in terms of just being able to connect, but the ATEM in question is on firmware 7.4, a little behind current.

By "not connect" I mean it executes the the handler code but it never receives any events, e.g. InitiationComplete or PreviewBusChanged, nor does it respond when I call send(message:) on the controller instance.

I feel like I must be missing something obvious since the example code is quite straightforward. I'll paste what I have below for reference.

I definitely appreciate any insight you might have!

import UIKit
import Atem

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        do {
            let controller = try Controller(ipAddress: "10.10.0.20") { handler in
                print("in handler") // this prints, nothing else below does

                handler.when { (version: ProtocolVersion) in
                    print(version)
                }

                handler.when { (change: InitiationComplete) in
                    print("Initiation complete")
                    print(change)
                }

                handler.when { (change: SourceTallies) in
                    print(change)
                }
                handler.when{ (change: PreviewBusChanged) in
                    print(change) // prints: 'Preview bus changed to input(x)'
                }
            }

            controller.send(message: ChangePreviewBus(to: .black))

        } catch let error {
            debugPrint(error)
        }
    }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.