動画配信ソフト「OBS」をPythonで操るぞ~

はじめに

こんにちは~。 会社のみんなから「ポン太郎」と呼ばれている技術部新卒の井上宗汰です!(同期に井上がもう一人いる)

「なんだか全然仕事できなさそ~」って感じのあだ名ですが、毎日を全力で生きているので何卒。

最近はイベント関係で使うシステムの構築をやったりしてるxR系のUnityエンジニアとしてお仕事してます。

さてさて、今回は、案件の関係でOBS(という動画配信でよく使われているソフト)をWebsocket経由でAPI叩いて操作したので、その知見の共有をしたいと思います。

これで外部アプリと連携させて、不具合が起きた時にそれをトリガーにして「しばらくお待ちください」みたいな放送事故っぽいアレを自動で出したりできますね! (動画配信していた頃の、灰色の高校時代を思い出しながら書いたので、なにやら心臓の調子が悪い気がした)

f:id:xabutoon:20181221230608p:plain
OBS(OpenBroadCasterSoftware)

環境

  • Windows10(x64)
  • OBS
  • Python3.5.4
  • obs-websocket(こやつでwebsocketでやりとりできるようになる)

github.com

  • obs-ws-rc(Python3.5+でobs-websocketを操れるようになる。うれし~) github.com

Python3.5+でなくても、Python2/3・JavaScript・C#を使えるから、好きなやつを選んでみてね

構築

とにもかくにも、まずは環境構築ですね。 OBSとPython3.5+を入れる。 (ここら辺は割愛) 次にobs-websocket! これはインストーラーが用意されているので、それを使いましょう。

github.com

こんな感じで、ツール以下に「Websocketサーバーの設定」という項目が現れたらインストールは成功です!うれしー!(パンパンパン!)

f:id:xabutoon:20181221220405p:plain

最後にobs-ws-rc。以下のコマンド一発でオーケー。

pip install obs-ws-rc

これで環境構築は終わり!

ドキュメント

まず、ドキュメントを覗いてみましょー

https://obs-ws-rc.readthedocs.io/en/latest/

リクエストの実行サンプル。

"""Example shows how to send requests and get responses."""

import asyncio

from obswsrc import OBSWS
from obswsrc.requests import ResponseStatus, StartStreamingRequest
from obswsrc.types import Stream, StreamSettings


async def main():

    async with OBSWS('localhost', 4444, "password") as obsws:

        # We can send an empty StartStreaming request (in that case the plugin
        # will use OBS configuration), but let's provide some settings as well
        stream_settings = StreamSettings(
            server="rtmp://example.org/my_application",
            key="secret_stream_key",
            use_auth=False
        )
        stream = Stream(
            settings=stream_settings,
            type="rtmp_custom",
        )

        # Now let's actually perform a request
        response = await obsws.require(StartStreamingRequest(stream=stream))

        # Check if everything is OK
        if response.status == ResponseStatus.OK:
            print("Streaming has started")
        else:
            print("Couldn't start the stream! Reason:", response.error)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

どうやら別で用意したRTMPのストリームをOBSに持ってくるサンプル?のようですね。 脳死でこれ実行しても、OBSとのコネクションはうまくいきますが、stream_settingsをちゃんとやらんと当然エラーになってしまうので、テックブログ記事のサンプルとしては、何だかしょぼい感じになってしまいます。

それはとても嫌な事なので『「海藻に張り付くダンゴウオのイラスト」の画像を配信していたが、トラブルが発生したのでコマンドから「お辞儀をしている猫のイラスト」に差し替える』というシチュエーションを想定して解説していくことにします。 (いらすとやさん、いつもありがとう)

お辞儀をしている猫のイラスト | かわいいフリー素材集 いらすとや

海藻に張り付くダンゴウオのイラスト | かわいいフリー素材集 いらすとや

実践 ~OBSの準備~

まずはOBS側の準備。 「ソース」の項目から「画像」を選択。

f:id:xabutoon:20181221230825p:plain

名前を「海藻に張り付くダンゴウオのイラスト」にして作成。

f:id:xabutoon:20181221231117p:plain

「参照」から画像を選択。

f:id:xabutoon:20181221231648p:plain

「お辞儀をしている猫のイラスト」も同じ要領で作成。とりあえず真ん中に配置しておきましょう。

......あぁ!なんと、ダンゴウオの上に猫が重なってしまいました!これでは既に放送事故ですッ!!!

f:id:xabutoon:20181221232813p:plain

猫は非表示にしておきましょう。 なんか目っぽいやつをクリック。

f:id:xabutoon:20181221233009p:plain

最後に先ほど登場した「Websocketサーバー設定」をクリック。 基本デフォルト設定ですが、今回は認証ありでいきます。パスワードは「dangouo」にしました。 「Enable System Tray Alert」をTrueにすれば、OBSとの接続・切断時にWindowsの通知が来るので、必要に応じて切り替えると良いでしょう。

f:id:xabutoon:20181221234257p:plain

実践 ~スクリプト作成~

さっきのサンプルとドキュメントをヒントに実装していきます。

https://obs-ws-rc.readthedocs.io/en/latest/obswsrc.html

最終的なコード。

現在のシーンを取得して、そいつが持ってる映像ソース(ここではダンゴウオと猫)の表示・非表示を入れ替えてるだけ。

マジでそれだけ。

import asyncio

from obswsrc import OBSWS
from obswsrc.requests import ResponseStatus, GetCurrentSceneRequest,SetSourceRenderRequest


async def main():

    async with OBSWS('localhost',4444,"dangouo") as obsws:

        response = await obsws.require(GetCurrentSceneRequest())

        if response.status == ResponseStatus.OK:
            print('//////////////////////////////////////////////')
            print("Connected.")
            print('//////////////////////////////////////////////')
            print("CurrentScene`s name is "+str(response.name))
            print('//////////////////////////////////////////////')

            print("The scenes that CurrentScene has")
            print('//////////////////////')
            for src in response.sources:
                print(src)
                print('//////////////////////')
                if src['render'] == True:
                    setReq = await obsws.require(SetSourceRenderRequest(source = src['name'],render=False))
                else:
                    setReq = await obsws.require(SetSourceRenderRequest(source = src['name'],render=True))
        else:
            print("Couldn't connect!:", response.error)
        

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

帰ってきた辞書のrenderキーを参照して、ブーリアンのステータスを入れ替えています。 実行するとこんな感じ。辞書のrenderキー見ると、ちゃんと猫とダンゴウオが入れ替わってることがわかります。 放送事故の被害を最小限に抑えることができました。

$ python TestObsWebsocket.py
//////////////////////////////////////////////
Connected.
//////////////////////////////////////////////
CurrentScene`s name is ▒V▒[▒▒
//////////////////////////////////////////////
The scenes that CurrentScene has
//////////////////////
{'cy': 400.0, 'cx': 335.0, 'volume': 1.0, 'render': True, 'source_cx': 335, 'source_cy': 400, 'x': 780.0, 'name': '▒▒▒▒▒V▒▒▒▒▒Ă▒▒▒L▒̃C▒▒▒X▒g', 'y': 355.0, 'type': 'image_source'}
//////////////////////
{'cy': 609.0, 'cx': 609.0, 'volume': 1.0, 'render': False, 'source_cx': 609, 'source_cy': 609, 'x': 646.0, 'name': '▒C▒▒▒ɒ▒▒▒t▒▒▒_▒▒▒S▒E▒I▒̃C▒▒▒X▒g', 'y': 223.0, 'type': 'image_source'}

あ、あれ、、、

も、文字化けしてるぅぅ~~~!、。?。!

OBS側で、シーン名とソース名を日本語にしてるからですね。かっこわるー

f:id:xabutoon:20181222005229p:plain

「シーン」を「TestScene」、各ソース名を「neko」「dangouo」に変更。(右クリックして「名前を変更」)

$ python TestObsWebsocket.py
//////////////////////////////////////////////
Connected.
//////////////////////////////////////////////
CurrentScene`s name is TestScene
//////////////////////////////////////////////
The scenes that CurrentScene has
//////////////////////
{'type': 'image_source', 'render': False, 'source_cx': 335, 'name': 'neko', 'y': 355.0, 'volume': 1.0, 'cx': 335.0, 'source_cy': 400, 'x': 780.0, 'cy': 400.0}
//////////////////////
{'type': 'image_source', 'render': True, 'source_cx': 609, 'name': 'dangouo', 'y': 223.0, 'volume': 1.0, 'cx': 609.0, 'source_cy': 609, 'x': 646.0, 'cy': 609.0}
//////////////////////

当然、ちゃんと直りましたね。

あと、他の処理に応用させる上で気を付けてほしいのがこの一文。

from obswsrc.requests import ResponseStatus, GetCurrentSceneRequest,SetSourceRenderRequest

他の処理を使った実装を行う際は、protocol reference のrequest項目から探すことになると思いますが、GetCurrentSceneRequest、SetSourceRenderRequestのように、referenceに載ってる項目に「Request」をつけてモジュールをインポートしてあげてください。(どうすればよいのか30分くらい迷った。descriptionのリンク切れてたし、、)

https://obs-ws-rc.readthedocs.io/en/latest/protocol.html

RequesField、ResponseFieldの各要項を見ながら臨機応変に対応してもらえれば他の処理にも応用できると思います。

あとがき

ここだけの話、まともなPython実践歴2日ではありましたが、立派なpythonistaになれるように精進していこうと思えるいい機会でした。 拙い記事ではありましたが、最後まで読んでいただきありがとうございました!

ちなみにですが、カヤックでは体験型コンテンツの制作も行っており、xR・ハードウェア志向エンジニアを募集しております。

www.kayac.com

www.kayac.com

明日のアドベントカレンダーの担当は長堂さんです!この後の記事もぜひお楽しみに〜