エンジニアの大場です。

2021年2月に行ったEarth & Humanで映像を切り替えるためにAtem Production Studio 4KというBlackmagic Design製のスイッチャーを使いました。
ATEMライブプロダクションスイッチャーはC++やC#向けのSDKが用意されています。
C#でSDKが用意されていたのでUnityに移植した話です。

ソースコードはGitHubにあげています。

1. 環境

当時の環境です。

  • ATEM Switchers 8.5.3 SDK
  • ATEMスイッチャー8.5.3
  • Unity 2019.4系
  • Windows10

2. 移植

Step1

こちらからソフトウェアとSDKをダウンロードします。

今回はSDKの「Windows\Samples\SimpleSwitcherExampleCSharp」を参考にしました。

Step2

SimpleSwitcherExampleCSharp.slnを開きます。

Program.csはBMDSwitcherAPI という名前空間を参照しています。
その参照はBMDSwitcherAPISimpleSwitcherExampleCSharp\obj\Debug\Interop.BMDSwitcherAPI.dllにあることがわかります。

このdllファイルをコピーしてUnityにインポートします。

Step3

必要な部分のソースコードだけをUnityに移植します。
メインロジックはMonoBehaviourを継承するとUnityで楽に使えます。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Linq;
using UnityEngine;
using UniRx;

namespace BMDSwitcherAPI
{
    public class BMDSwitcherController : MonoBehaviour
    {
        [SerializeField] private string _ipAddress = "192.168.10.240";
        [SerializeField] private string _switcherName = "SwitcherName";
        [SerializeField] private string _programInput = "ProgramInput";
        [SerializeField] private string _previewInput = "PreviewInput";
        [SerializeField, Range(0, 1)] private float _transition = 0;

        private IBMDSwitcherMixEffectBlock _mixEffectBlock = null;
        private Dictionary<string, long> _inputDictionary = new Dictionary<string, long>();
        private long _programId;
        private long _previewId;
        private bool _isConnecting;

        public string IpAddress
        {
            set { _ipAddress = value; }
            get { return _ipAddress; }
        }

        public string SwitcherName
        {
            private set { _switcherName = value; }
            get { return _switcherName; }
        }

        public string ProgramInput
        {
            set { _programInput = value; }
            get { return _programInput; }
        }

        public string PreviewInput
        {
            set { _previewInput = value; }
            get { return _previewInput; }
        }

        public float Transition
        {
            set { _transition = value; }
            get { return _transition; }
        }

        public bool IsConnecting
        {
            private set { _isConnecting = value; }
            get { return _isConnecting; }
        }

        public Dictionary<string, long> InputDictionary
        {
            private set { _inputDictionary = value; }
            get { return _inputDictionary; }
        }

        public async Task<bool> Connect()
        {
            try
            {
                _mixEffectBlock = await Task.Run(() =>
                {
                    return Setup();
                });

                _mixEffectBlock.GetProgramInput(out _programId);
                _mixEffectBlock.GetPreviewInput(out _previewId);

                this.ObserveEveryValueChanged(_ => _._previewId).Subscribe(id => _previewInput = _inputDictionary.FirstOrDefault(_ => _.Value == id).Key).AddTo(gameObject);
                this.ObserveEveryValueChanged(_ => _._programId).Subscribe(id => _programInput = _inputDictionary.FirstOrDefault(_ => _.Value == id).Key).AddTo(gameObject);
                this.ObserveEveryValueChanged(_ => _._transition).Subscribe(value => _mixEffectBlock.SetTransitionPosition(value)).AddTo(gameObject);
                this.ObserveEveryValueChanged(_ => _._programInput).Subscribe(value =>
                {
                    long inputId = 0;
                    _inputDictionary.TryGetValue(value, out inputId);
                    _mixEffectBlock.SetProgramInput(inputId);
                }).AddTo(gameObject);
                this.ObserveEveryValueChanged(_ => _._previewInput).Subscribe(value =>
                {
                    long inputId = 0;
                    _inputDictionary.TryGetValue(value, out inputId);
                    _mixEffectBlock.SetPreviewInput(inputId);
                }).AddTo(gameObject);

                Debug.Log("Connect " + _ipAddress);

                _isConnecting = true;

                return true;
            }
            catch (System.Runtime.InteropServices.ExternalException e)
            {
                Debug.LogError("Not Connect " + _ipAddress);
                Debug.LogException(e);
                return false;
            }
        }

        private async Task<IBMDSwitcherMixEffectBlock> Setup()
        {
            return await Task.Run(() =>
            {
                IBMDSwitcherDiscovery discovery;
                IBMDSwitcher switcher;
                AtemSwitcher atem;
                _BMDSwitcherConnectToFailure failureReason;

                discovery = new CBMDSwitcherDiscovery();
                discovery.ConnectTo(_ipAddress, out switcher, out failureReason);

                atem = new AtemSwitcher(switcher);

                switcher.GetProductName(out _switcherName);

                // // Get reference to various objects
                var mixEffectBlock = atem.MixEffectBlocks.First();

                // Get an input iterator.
                var inputs = atem.SwitcherInputs;

                foreach (var input in inputs)
                {
                    string inputName;
                    long inputId;

                    input.GetInputId(out inputId);
                    input.GetLongName(out inputName);

                    // Add items to list:
                    _inputDictionary.Add(inputName, inputId);
                }

                return mixEffectBlock;
            });
        }

        private void OnDestroy()
        {

        }

        private void Update()
        {
            if (_mixEffectBlock == null) return;

            try
            {
                _mixEffectBlock.GetProgramInput(out _programId);
                _mixEffectBlock.GetPreviewInput(out _previewId);
                _mixEffectBlock.SetFadeToBlackRate(30); // コネクトしているか確認のため適当なセット関数を呼んでいる
            }
            catch
            {
                if (_isConnecting) Disconnected();
            }
        }

        public void PlayAutoMix(uint frame)
        {
            var mix = _mixEffectBlock as IBMDSwitcherTransitionMixParameters;
            mix.SetRate(frame);
            _mixEffectBlock.PerformAutoTransition();
        }

        private void Disconnected()
        {
            _isConnecting = false;
            _inputDictionary.Clear();
            _mixEffectBlock = null;
        }
    }
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace BMDSwitcherAPI
{
    public class AtemSwitcher
    {
        private IBMDSwitcher _switcher;

        public AtemSwitcher(IBMDSwitcher switcher)
        {
            _switcher = switcher;
        }

        public IEnumerable<IBMDSwitcherMixEffectBlock> MixEffectBlocks
        {
            get
            {
                // Create a mix effect block iterator
                IntPtr meIteratorPtr;
                _switcher.CreateIterator(typeof(IBMDSwitcherMixEffectBlockIterator).GUID, out meIteratorPtr);
                IBMDSwitcherMixEffectBlockIterator meIterator = Marshal.GetObjectForIUnknown(meIteratorPtr) as IBMDSwitcherMixEffectBlockIterator;
                if (meIterator == null)
                    yield break;

                // Iterate through all mix effect blocks
                while (true)
                {
                    IBMDSwitcherMixEffectBlock me;
                    meIterator.Next(out me);

                    if (me != null)
                        yield return me;
                    else
                        yield break;
                }
            }
        }

        public IEnumerable<IBMDSwitcherInput> SwitcherInputs
        {
            get
            {
                // Create an input iterator
                IntPtr inputIteratorPtr;
                _switcher.CreateIterator(typeof(IBMDSwitcherInputIterator).GUID, out inputIteratorPtr);
                IBMDSwitcherInputIterator inputIterator = Marshal.GetObjectForIUnknown(inputIteratorPtr) as IBMDSwitcherInputIterator;
                if (inputIterator == null)
                    yield break;

                // Scan through all inputs
                while (true)
                {
                    IBMDSwitcherInput input;
                    inputIterator.Next(out input);

                    if (input != null)
                        yield return input;
                    else
                        yield break;
                }
            }
        }
    }
}

Step4

BMDSwitcherControllerのInputDictionaryでスイッチャー固有のインプット情報を見ることができます。ProgramInputとPreviewInputにスイッチャーのインプットと対応した情報をInputDictionaryから選びます。

またTransitionを0~1で変更することでスイッチャーの映像出力を ProgramInputとPreviewInputで切り替えることができます。

あくまでもミキサーではなくスイッチャーなのでTransitionが1になるとProgramInputとPreviewInputが逆に入れ替わります。

4. うまくいかなかったところ

今回は全部の機能を移植したわけではなく、案件で使う映像スイッチングのみを移植しました。
もろもろ参考にしていけばSDKで用意されている機能は移植できると思います。

またスイッチャーとの接続状態の確認手段がなかっため、Updateメソッドでスイッチャー側の適当な関数を呼び確認してます。
接続が切れると関数を叩いたタイミングでエラーが出るので、TryCatch文でエラーハンドリングを行いDisconnectにしています。

もっといい方法を探し実装したかったですが、本番まで期間がなく他にも実装するものがあったため今回は妥協しました。