【obs-websocket v5版】rekordbox+OBSでトラックタイトルの表示を自動切り替えする@Python

投稿者: | 2023年4月20日

結構前にこのようなものを作ったが、obs-websocketのプロトコルがv5になり色々と変わったため以前のままだと動かなくなった。というわけでv5に対応させる。

目次


環境構築

折角なので環境から作り直す。今回入れたのは以下の通り。

  • Python 3.11.2
  • obs-websocket-py 1.0
  • opencv-python 4.7.0.72

なお、Anaconda Navigatorからopencvを探したりすると4.6.0が見つかるが、

[ WARN:0] global C:\ci_311_rebuilds\opencv-suite_1679001454889\work\modules\videoio\src\cap_gstreamer.cpp (862) cv::GStreamerCapture::isPipelinePlaying OpenCV | GStreamer warning: GStreamer: pipeline have not been created

みたいなWarningに数時間を潰されたくなければpipで入れたほうがいい。
ちなみに上記のWarningが出ても普通に動いてはいた。でも出したくはないのでpipを使おう。

pip install obs-websocket-py opencv-python

前回からの変更点

基本的な部分は大きく変わらないのでAPI名と書き方の修正だけでいけるかと思いきや、ソースアイテムの指定が名前ではなく sceneItemId になったのでこのIDを調べる必要がある。名前が被っててもIDは違うのでこっちのほうが安心!
ただOBSからGUIで見る方法は無さげなのでこれもAPIを叩いて引っ張ってくるしかない。

前提知識等

ws.call() でリクエストを投げると以下のようなdict型でレスポンスが返ってくる。
'datain' に欲しいデータが入っている。

{
  'name': <API名>,
  'datain': <APIから返された値>,
  'dataout': <APIに送ったパラメータ>,
  'status': <実行結果>
}

今回は GetSceneItemListGetGroupSceneItemList を使う。
これらは共に以下のようなデータを返す。
'sceneItems'しかないdictの中にArrayがあり、各要素がdictになっている。微妙にめんどくさい。
必要なデータは 'sourceName', 'sceneItemId', 'isGroup' の3種。

{
  'sceneItems': 
  [
    {
      'inputKind': <str>,
      'isGroup': <NoneType>,
      'sceneItemBlendMode': <str>,
      'sceneItemEnabled': <bool>,
      'sceneItemId': <int>,
      'sceneItemIndex': <int>,
      'sceneItemLocked': <bool>,
      'sceneItemTransform': 
      {
        'alignment': <int>,
        'boundsAlignment': <int>,
        'boundsHeight': <float>,
        'boundsType': <str>,
        'boundsWidth': <float>,
        'cropBottom': <int>,
        'cropLeft': <int>,
        'cropRight': <int>,
        'cropTop': <int>,
        'height': <float>,
        'positionX': <float>,
        'positionY': <float>,
        'rotation': <float>,
        'scaleX': <float>,
        'scaleY': <float>,
        'sourceHeight': <float>,
        'sourceWidth': <float>,
        'width': <float>
      },
      'sourceName': <str>,
      'sourceType': <str>
    },
    {
      ...
    },
    ...
  ]
}

IDを調べるスクリプト

GetSceneItemId を使えば各ソースごとに sceneItemId を引っ張ってこれるらしいが、折角なのでソースに登録してある全てのアイテム名とIDを出してみることにする。これ名前被ってたらどういう動きするんだろうとかは気になるが一旦置いておく。

GetSceneItemList を使うことで上述した形式でデータを得られるので、うまいこと取り出して整形する。
ただしグループにまとめてあるとグループ内のアイテムは取れないので、'isGroup'=True の時だけ別途 GetGroupSceneItemList を用いてグループの中身を取り出す。グループは内部的にぶっ壊れてるから使うなみたいなことが書かれてあるがこれについては後述する。

何故グループだけ別で取り出す必要があるのかに関しては GetGroupList の項にそれっぽいことが書かれており、グループは内部的にはシーンとのこと。そうなんだ……
そりゃ別シーンだと別で取り出す必要あるわな。

以上を踏まえて、実際に書くとこう。異論は認める。

from obswebsocket import obsws, requests as rq

# websocket接続先設定
host = "localhost" 
port = 4455
password = "xxxxxxxxxxxxxxxx"
# ----------

scene = input("シーン名 > ")  #ID取得対象のシーン名を入力させる

# OBSに接続
ws = obsws(host, port, password)
ws.connect()

items = ws.call(rq.GetSceneItemList(sceneName=scene)).datain["sceneItems"]  # アイテム取得

items.reverse() # 逆順

export_list = []  # 出力用リスト初期化

# リストの処理
for i in items:
  # グループなら
  if i["isGroup"] == True:
    export_list.append("ID: {: >3,d} | [G]{}".format(i["sceneItemId"],i["sourceName"])) # グループを出力用リストに追加

    group_items = ws.call(rq.GetGroupSceneItemList(sceneName=i["sourceName"])).datain["sceneItems"] # グループ内アイテム取得
    group_list = [] #グループ用リスト初期化

    # グループ内のアイテム処理
    for j in group_items:
      group_list.append("ID: {: >3,d} |  + {}".format(j["sceneItemId"],j["sourceName"]))  # グループ用リストに追加(グループ内アイテム)

    group_list.reverse()  # 逆順

    #グループ内アイテムを出力用リストに追加
    for j in group_list:
      export_list.append(j)

  # グループではないなら
  else:
    export_list.append("ID: {: >3,d} | {}".format(i["sceneItemId"],i["sourceName"]))  # 出力用リストに追加

# 出力
for i in export_list:
  print(i)

ws.disconnect() # OBSから切断

OBSと表示順を合わせたうえでいい感じIDとソース名を得られた。ちゃんとグループ内のアイテムも取得できている。

前回と同じく再生中のデッキに合わせて表示を切り替えたいので、必要なIDをメモっておく。
これでやっとメインのスクリプトを修正できる。

出力例

メインスクリプトの修正

修正に入る前に、opencvがキャプチャするデバイスのIDを調べておく必要がある。これについては0から順番に気合で調べるくらいしか方法がないので詳しくは書かない。
映像をプレビューする方法は以下のページが参考になる。

OpenCV 接続したカメラから動画を取得しよう (Python)

というわけで修正したものがこちら。

import os
import cv2
import datetime

from obswebsocket import obsws, requests as rq

# ======================================

# OBS接続設定
host = "localhost" 
port = 4455
password = "xxxxxxxxxxxxxxxx"

# シーン名
scene_name = "DJ"

# アイテムID
# ID_Analyzerで調べたやつ
deck1_id = 17
deck2_id = 18
track2_id = 19

# キャプチャデバイスID
device_id = 3

# 色のしきい値
threshold = 20

# キャプチャ解像度
capture_width = 1920
capture_height = 1080

# クロップエリア
deck_top = 353
deck_bottom = 370
deck1_left = 902
deck1_right = 958
deck2_left = 1862
deck2_right = 1918

# ======================================

# ソースの表示を切り替える
def setVisible(item_id, visible):
	ws.call(rq.SetSceneItemEnabled(sceneName=scene_name, sceneItemId=item_id, sceneItemEnabled=visible))

# VideoCapture オブジェクトを取得
capture = cv2.VideoCapture(device_id)
print(capture.isOpened())
capture.set(cv2.CAP_PROP_FRAME_WIDTH, capture_width)
capture.set(cv2.CAP_PROP_FRAME_HEIGHT, capture_height)

# OBSに接続
ws = obsws(host, port, password)
ws.connect()

# 処理フラグ
switch = 0

try:
	while True:
		# 映像を取得
		ret, frame = capture.read()
		
		# クロップ
		deck1_crop = frame[ deck_top : deck_bottom , deck1_left : deck1_right]
		deck2_crop = frame[ deck_top : deck_bottom , deck2_left : deck2_right]
		
		# 色情報を取得
		red_deck1 = deck1_crop.T[2].flatten().mean()
		red_deck2 = deck2_crop.T[2].flatten().mean()
		
		# DECK1がMASTERのとき
		if red_deck1 > threshold and (switch == 2 or switch == 0):
			os.system('cls')
			print(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
			print("\nNOW PLAYING: DECK1\n")
			setVisible(deck2_id, False)
			setVisible(deck1_id, True)
			setVisible(track2_id, False)
			switch = 1
		
		# DECK2がMASTERのとき
		elif red_deck2 > threshold and (switch == 1 or switch == 0):
			os.system('cls')
			print(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
			print("\nNOW PLAYING: DECK2\n")
			setVisible(deck1_id, False)
			setVisible(deck2_id, True)
			setVisible(track2_id, True)
			switch = 2
		
		# よくわからんとき
		elif (red_deck1 < threshold and red_deck2 < threshold) or (red_deck1 > threshold and red_deck2 > threshold):
			os.system('cls')
			print(datetime.datetime.now().strftime('%Y/%m/%d %H:%M:%S'))
			print("\nUndefined\n")
			setVisible(deck1_id, False)
			setVisible(deck2_id, False)
			setVisible(track2_id, False)
			switch = 0
		
		# なにもしない
		else:
			pass

# Ctrl+Cが押されたら終了
except KeyboardInterrupt:
	capture.release()
	ws.disconnect()

備考など

スクリプト上で変わったのはsetVisible関数くらいなもの。あとはシーン名を明確に指定して制御するようになったので、シーンを切り替えると当然ながら動かない……というか指定したシーンしか制御しないので別シーンでは意味がない。以前は表示中シーンに対して動いたので、誤動作防止ともいえる。

ただし問題がひとつあり、折角グループ内アイテムのIDまで調べたがグループ内にあるアイテムの制御ができないGetSceneItemEnabled などで状態を取得することすらできず、falseが返されてしまう。
仕方ないので制御対象のソースをグループから出した。レイヤーの上下関係があるからちょっと散らかっちゃうが仕方ない。片付けたいならグループじゃなくシーンのネストをしろとのお達しである。
グループがぶっ壊れてるのが悪い。なんとかならんか……?ならんか……

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です