OBS配信で使用しているコード

誰も見てないこちらのtwitch配信(LoL)で使ってるコードを取り敢えず置いておきます。

前提

下記コードはMITライセンスとします。

日時表示用HTML

HTMLファイルをローカルに保存し、ブラウザソースにfile:でURLを設定して使用します。Windows用しか用意しておらずサイズ感なども固定ですが、時刻表示ではプロポーショナルフォントでも固定長の文字として表示されるよう細工しています。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <style>
        body {
            font-family: 'Impact', 'メイリオ';
            width: 130px;
            height: 60px;
            margin: 0;
            padding: 0;
            color: white;
        }
        div {
            text-align: center;
        }
        p {
            margin: 0;
            text-shadow: 
                 2px  2px 1px black,
                -2px  2px 1px black,
                 2px -2px 1px black,
                -2px -2px 1px black,
                 2px  0px 1px black,
                 0px  2px 1px black,
                -2px  0px 1px black,
                 0px -2px 1px black;
        }
        #hour-min-text {
            font-size: 30px;
        }
        .char {
            display: inline-block;
            width: 18px;
        }
    </style>
</head>
<body>
    <div class="content">
        <p id="hour-min-text"><span id="hour" class="boxes">00</span>:<span id="minute" class="boxes">00</span>:<span id="second" class="boxes">00</span>
        </p>
        <p>
            <span id="date-text" >0000/00/00</span>
            <span id="weekday-text"></span>
        </p>
    </div>

    <script>
        function wrap_letters(element) {
            element.childNodes.forEach(child=>{
                if (child.nodeType === Node.TEXT_NODE) {
                    const newE = document.createDocumentFragment();
                    Array.prototype.forEach.call(child.nodeValue, ch=>{
                        const span = document.createElement('span');
                        span.className = 'char';
                        span.textContent = ch;
                        newE.appendChild(span);
                    });
                    element.replaceChild(newE, child);
                }
            });
        }
        const update_clock = () => {
            const now = new Date();
            const year = ("0000" + (now.getFullYear())).slice(-4);
            const month = ("00" + (now.getMonth() + 1)).slice(-2);
            const day = ("00" + now.getDate()).slice(-2);
            const hour = ("00" + now.getHours()).slice(-2);
            const min = ("00" + now.getMinutes()).slice(-2);
            const sec = ("00" + now.getSeconds()).slice(-2);
            const week = ["日", "月", "火", "水", "木", "金", "土"][now.getDay()];

            document.getElementById("hour").textContent = `${hour}`;
            document.getElementById("minute").textContent = `${min}`;
            document.getElementById("second").textContent = `${sec}`;
            document.getElementById("date-text").textContent = `${year}/${month}/${day}`;
            document.getElementById("weekday-text").textContent = `${week}`;
            
            document.querySelectorAll('.boxes').forEach(e=>wrap_letters(e));
        };
        setInterval(update_clock, 1000);
    </script>
</body>
</html>

録画と過去配信再生pythonスクリプト

OBSはpythonluaスクリプトが使えます(本家リファレンス)。メニューからスクリプトpython環境を設定し、以下のスクリプトを読み込むと設定画面が出るので、そこで録画するキャプチャソース(ゲーム画面)と、再生用のメディアソースを指定すると、配信したときに自動録画と非録画時にメディアを再生します。

import obspython as S
from contextlib import contextmanager
from datetime import datetime

game_name = ""
media_source_name = ""

@contextmanager
def source_auto_release(source_name):
    source = S.obs_get_source_by_name(source_name)
    try:
        yield source
    finally:
        S.obs_source_release(source)

@contextmanager
def enum_sources_auto_release():
    sources = S.obs_enum_sources()
    try:
        yield sources
    finally:
        S.source_list_release(sources)

def logging(message, level=S.LOG_INFO):
    now = datetime.now().isoformat()
    S.script_log(level, f'{now}: {message}')

def add_logging(func):
    def wrapper(*args, **kwargs):
        unified_args_str = ','.join([str(x) for x in args] + [f'{str(t[0])}={str(t[1])}' for t in kwargs.items()])
        logging(f'[entry]{func.__name__}({unified_args_str})')
        return func(*args, **kwargs)
    return wrapper

def is_playing(media_source):
    return S.obs_source_media_get_state(media_source) in (S.OBS_MEDIA_STATE_PLAYING, S.OBS_MEDIA_STATE_OPENING, S.OBS_MEDIA_STATE_BUFFERING)

def is_player_not_started(media_source):
    return S.obs_source_media_get_state(media_source) in (S.OBS_MEDIA_STATE_NONE, S.OBS_MEDIA_STATE_STOPPED, S.OBS_MEDIA_STATE_ENDED)

@add_logging
def play_pause_media_source(pause=False):
    with source_auto_release(media_source_name) as media_source:
        if is_player_not_started(media_source):
            S.obs_source_media_restart(media_source)
        else:
            S.obs_source_media_play_pause(media_source, pause)

@add_logging
def on_hooked_(data):
    play_pause_media_source(pause=True)
    S.obs_frontend_recording_start()

# decoratorが効かないので
def on_hooked(data):
    on_hooked_(data)

@add_logging
def on_unhooked_(data):
    S.obs_frontend_recording_stop()
    play_pause_media_source()

# decoratorが効かないので
def on_unhooked(data):
    on_unhooked_(data)

@add_logging
def prepare_source_callback():
    with source_auto_release(game_name) as game:
        sh = S.obs_source_get_signal_handler(game)
        S.signal_handler_connect(sh, "hooked", on_hooked)
        S.signal_handler_connect(sh, "unhooked", on_unhooked)

@add_logging
def release_source_callback():
    S.obs_frontend_recording_stop()
    with source_auto_release(game_name) as game:
        sh = S.obs_source_get_signal_handler(game)
        S.signal_handler_disconnect(sh, "hooked", on_hooked)
        S.signal_handler_disconnect(sh, "unhooked", on_unhooked)

@add_logging
def on_event(event):
    global pastgames_running
    if event == S.OBS_FRONTEND_EVENT_STREAMING_STARTING:
        play_pause_media_source()
        prepare_source_callback()
    elif event == S.OBS_FRONTEND_EVENT_STREAMING_STOPPING:
        play_pause_media_source(pause=True)
        release_source_callback()

def extract_from_sources_in(lst):
    with enum_sources_auto_release() as sources:
        return [S.obs_source_get_name(s) for s in sources if S.obs_source_get_unversioned_id(s) in lst]

@add_logging
def script_update(settings):
    global media_source_name
    global game_name
    
    game_name_updated = S.obs_data_get_string(settings, "game_name")
    if game_name != game_name_updated:
        logging(f'game_name: {game_name} -> {game_name_updated}')
        game_name = game_name_updated

    media_source_name_updated = S.obs_data_get_string(settings, "media_source_name")
    if media_source_name != media_source_name_updated:
        logging(f'game_name: {media_source_name} -> {media_source_name_updated}')
        must_play = False
        with source_auto_release(media_source_name) as media_source:
            must_play = is_playing(media_source)
        media_source_name = media_source_name_updated
        with source_auto_release(media_source_name) as media_source:
            playing_now = is_playing(media_source)
            if must_play != playing_now:
                S.obs_source_media_play_pause(media_source, pause=playing_now)

@add_logging
def script_description():
    return "配信開始時メディアを再生開始し、ゲームキャプチャ中はメディア再生を一時停止して録画し、ゲームキャプチャ終了時メディア再生を再開し、配信終了でメディア再生を一時停止する"

@add_logging
def script_properties():
    props = S.obs_properties_create()
    p = S.obs_properties_add_list(props, "game_name", "ゲーム画面のキャプチャ名", S.OBS_COMBO_TYPE_LIST, S.OBS_COMBO_FORMAT_STRING)
    for name in extract_from_sources_in(["window_capture", "game_capture"]):
        S.obs_property_list_add_string(p, name, name)
    p = S.obs_properties_add_list(props, "media_source_name", "再生するソース名", S.OBS_COMBO_TYPE_LIST, S.OBS_COMBO_FORMAT_STRING)
    for name in extract_from_sources_in(["ffmpeg_source", "vlc_source"]):
        S.obs_property_list_add_string(p, name, name)
    return props

@add_logging
def script_load(settings):
    S.obs_frontend_add_event_callback(on_event)