【スマメイト】OBSだけでレート更新を完全に自動化する方法【完】
これまで「OBSだけで」にこだわってレートを自動更新する方法を考えたり作ったり書いたりしてきましたが、ようやく「OBSだけで配信中にスマメイトのレートを(((完全に)))自動更新するやつ」を実現できたので、記事を更新することにしました。
【前回の記事】
【スマメイト】OBSだけで配信中のレート・戦績の更新をラクにする方法【Ver.3】
https://note.com/nekofuton/n/n19c84da88730
前書き
所要時間はかかっても3分!
今回はLuaというスクリプトを用意して、OBSに読み込ませます。
スクリプトなんて聞くと難しそうですが、ここで公開しているので一瞬です。
Luaの読み込みはOBSのデフォルトの機能で使用することができますが、OBSで直接編集することはできず、スクリプトをどこかに保存しなければなりません。
つまり、前回は「自動」という部分に課題が残ってましたが、今回は「OBSだけで」という部分にハテナが付いてしまうことになります…
まあ、OBSに搭載されてる機能だし、一度設定したらそのまま使えるし、お許しください(^-^;)
(でも他にソフト起動したりとか準備とかせずにOBS単独でできるから使い勝手はとてもいいとおもう)
手順
1.OBSのソースに、ブラウザを追加
「+」のアイコンをクリック、ブラウザを選択します。
分かりやすい名前を入力して新規作成します。

2.プロパティ画面
URLにスマメイトのマイページのURLを入力します。
幅と高さはとりあえずこのままで、後ほど調整してください。
カスタムCSSは何も変更しなくてOK。

スマメイトのユーザーページが表示されます。

3.Lua(スクリプト)を準備する
レートを自動で更新するためのスクリプトを準備します。
下記にLuaファイルをアップロードしたので、ダウンロードすると楽です。
スクリプトは一応、記事の最下部に記述しようと思います。
このLuaファイルは後から場所の移動や削除などを行うと再度読み込みが必要になるため、あまり触らない、でもなくさない、程よい場所に保存することをおすすめします。
僕はOBSの設定フォルダ内に保存しています。

4.LuaをOBSに追加する
ダウンロードが終わったら、上部のメニューの「ツール」から「スクリプト」をクリックします。

開いた画面で左下の「+」を押して、準備したLuaファイルを選びます。

もしLuaファイルを僕と同じOBSの設定フォルダ内に移動している場合、開いた画面のアドレスバーに下記をコピペするとすぐに見つかります。
%AppData%\obs-studio
5.レート自動更新の設定
Luaの読み込みに成功すると、右側に様々な設定項目が表示されます。
必ず最初に「更新するソース」から「先ほど作成したブラウザソース」を選択してください。
先ほどのスマメイトのユーザーページがレート表示に変われば成功です。


6.完成
お疲れさまでした!
これだけでOBSだけでレートを自動更新するシステムの完成です!
あとは、レートの更新間隔やフォントのデザイン設定などをお好みで調整してください。
更新間隔は、あまり短いと良くないと思い300秒以下にはできないようにしています。
テスト用ですが、スクリプト画面の「更新」を押すと手動でレートを更新できます。
【ご注意】
OBSのみでレートを自動更新する都合上、「自動更新用」という新しいソースを作成します。すみませんが、削除しないでください!
後書き
使用の報告や配信ページへの記載などは不要ですが、使っていただいている人がいることが分かれば、またスマメイトさんのページの構造が変わった時などに対応するモチベになるので歓迎です。
前回の記事にも書いていますが、CSSの知識はあるけどプログラミングの知識に乏しくて「自動更新」ができなかったところ、AIを使ってプログラミングのお勉強を少しずつ初めて実現できました。でもほとんどAIがやってくれたような… 本当に、今までの努力は何だったのかってくらい簡単に学習ができて、簡単に実装ができて、良い時代なのか、そう言い切るのは難しい時代なのか…
それはそうと、OBSでこんなスクリプトを動かせることを今更ながら知って、こんなん何でもできるやん…と感動したりウズウズしたりしています。
なので、もしかしたらもうどなたかがとっくに作ってるかもしれない…
また不具合などが出てきたらコメントを送ったりXでリプを送ったりこの記事のURLと一緒にポストしていただけると見つけたときに直したいです。
Luaファイルの中身
Luaファイルのダウンロードがうまくいかなかった場合は
メモ帳を開いて
下記のコードをコピーして貼り付けて
「名前を付けて保存」を押して
ファイルの種類を「全てのファイル」にして
ファイル名の一番右の末尾に「.lua」(ドットLUA) と書いて
保存してください。
obs = obslua
source_name = ""
interval_sec = 300
load_wait_ms = 3000
helper_name = "__自動更新用__"
helper_source = nil
helper_item = nil
showing_helper = false
busy = false
font_face = "Arial"
font_size = 80
font_color = 0xFFFFFF
font_bold = true
outline_px = 0
outline_color = 0x000000
local function get_browser_sources()
local result = {}
local sources = obs.obs_enum_sources()
if sources ~= nil then
for _, src in ipairs(sources) do
local id = obs.obs_source_get_id(src)
local name = obs.obs_source_get_name(src)
if id ~= nil and name ~= nil and string.find(id, "browser_source", 1, true) ~= nil and name ~= helper_name then
table.insert(result, {name = name})
end
end
obs.source_list_release(sources)
end
table.sort(result, function(a, b) return a.name < b.name end)
return result
end
local function css_escape_string(s)
if s == nil or s == "" then return "Arial" end
s = string.gsub(s, "\\", "\\\\")
s = string.gsub(s, '"', '\\"')
return s
end
local function obs_color_to_hex(color)
color = tonumber(color) or 0
local r = color % 256
local g = math.floor(color / 256) % 256
local b = math.floor(color / 65536) % 256
return string.format("#%02X%02X%02X", r, g, b)
end
local function build_outer_text_shadow(stroke, color)
stroke = math.max(0, math.floor(tonumber(stroke) or 0))
if stroke <= 0 then
return "none"
end
local shadows = {}
local radius = stroke + 0.45
local radius_sq = radius * radius
for y = -stroke, stroke do
for x = -stroke, stroke do
if not (x == 0 and y == 0) and (x * x + y * y) <= radius_sq then
table.insert(shadows, string.format("%dpx %dpx 0 %s", x, y, color))
end
end
end
return table.concat(shadows, ",\n ")
end
local function build_custom_css()
local face = css_escape_string(font_face)
local text_hex = obs_color_to_hex(font_color)
local outline_hex = obs_color_to_hex(outline_color)
local weight = font_bold and "700" or "400"
local stroke = math.max(0, math.floor(tonumber(outline_px) or 0))
local outer_shadow = build_outer_text_shadow(stroke, outline_hex)
local edge_space = stroke + 2
local box_height = math.max(20, font_size + edge_space * 2 + 8)
return string.format([[
html,body {margin: 0 !important;padding: 0 !important;background: transparent !important;overflow: hidden !important;}body * {visibility: hidden !important;}.side div:nth-of-type(1) .rate_text {visibility: visible !important;position: fixed !important;top: 0 !important;left: 0 !important;margin: 0 !important;padding: %dpx !important;box-sizing: border-box !important;color: %s !important;font-family: "%s", sans-serif !important;font-size: %dpx !important;font-weight: %s !important;font-style: normal !important;line-height: %dpx !important;-webkit-text-stroke: 0 !important;text-shadow:%s !important;width: 700px !important;height: %dpx !important;overflow: hidden !important;background: transparent !important;}.side div:nth-of-type(1) .rate_text::after {content: "" !important;display: block !important;visibility: visible !important;position: fixed !important;top: %dpx !important;left: 0 !important;width: 100vw !important;height: 100vh !important;background: transparent !important;
}
]],
edge_space,
text_hex,
face,
font_size,
weight,
font_size + 20,
outer_shadow,
box_height,
box_height
)
end
local function set_source_css(source, css)
if source == nil then return false end
local settings = obs.obs_source_get_settings(source)
if settings == nil then return false end
obs.obs_data_set_string(settings, "css", css)
obs.obs_source_update(source, settings)
obs.obs_data_release(settings)
return true
end
local function apply_style_to_sources()
if source_name == nil or source_name == "" then return end
local css = build_custom_css()
local target_source = obs.obs_get_source_by_name(source_name)
if target_source ~= nil then
set_source_css(target_source, css)
obs.obs_source_release(target_source)
end
if helper_source ~= nil then
set_source_css(helper_source, css)
end
end
function script_properties()
local props = obs.obs_properties_create()
local p = obs.obs_properties_add_list(props, "source_name", "更新するソース", obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING)
obs.obs_property_list_add_string(p, "(レートを表示しているソース)", "")
for _, s in ipairs(get_browser_sources()) do
obs.obs_property_list_add_string(p, s.name, s.name)
end
obs.obs_properties_add_int(props, "interval_sec", "更新間隔(秒)", 300, 3600, 1)
obs.obs_properties_add_font(props, "rate_font", "フォント")
obs.obs_properties_add_int(props, "font_size", "フォントサイズ", 10, 300, 1)
obs.obs_properties_add_color(props, "font_color", "文字色")
obs.obs_properties_add_bool(props, "font_bold", "太字")
obs.obs_properties_add_int(props, "outline_px", "境界線", 0, 30, 1)
obs.obs_properties_add_color(props, "outline_color", "境界線の色")
obs.obs_properties_add_button(props, "refresh_now", "更新", refresh_now_clicked)
return props
end
function script_defaults(settings)
obs.obs_data_set_default_int(settings, "interval_sec", 300)
obs.obs_data_set_default_int(settings, "font_size", 80)
obs.obs_data_set_default_int(settings, "font_color", 0xFFFFFF)
obs.obs_data_set_default_bool(settings, "font_bold", true)
obs.obs_data_set_default_int(settings, "outline_px", 0)
obs.obs_data_set_default_int(settings, "outline_color", 0x000000)
end
local function ensure_target_visible()
if source_name == nil or source_name == "" then return end
local scene_src = obs.obs_frontend_get_current_scene()
if scene_src == nil then return end
local scene = obs.obs_scene_from_source(scene_src)
if scene ~= nil then
local target_item = obs.obs_scene_find_source_recursive(scene, source_name)
if target_item ~= nil then
obs.obs_sceneitem_set_visible(target_item, true)
end
local h_item = obs.obs_scene_find_source_recursive(scene, helper_name)
if h_item ~= nil then
obs.obs_sceneitem_set_visible(h_item, false)
end
showing_helper = false
end
obs.obs_source_release(scene_src)
end
local function release_helper()
ensure_target_visible()
if helper_item ~= nil then
obs.obs_sceneitem_remove(helper_item)
helper_item = nil
end
if helper_source ~= nil then
obs.obs_source_remove(helper_source)
obs.obs_source_release(helper_source)
helper_source = nil
end
showing_helper = false
end
local function get_current_scene_and_items()
local scene_src = obs.obs_frontend_get_current_scene()
if scene_src == nil then return nil, nil, nil, nil end
local scene = obs.obs_scene_from_source(scene_src)
if scene == nil then
obs.obs_source_release(scene_src)
return nil, nil, nil, nil
end
local target_item = obs.obs_scene_find_source_recursive(scene, source_name)
local h_item = obs.obs_scene_find_source_recursive(scene, helper_name)
return scene_src, scene, target_item, h_item
end
local function copy_item_transform(from_item, to_item)
if from_item == nil or to_item == nil then return end
local pos = obs.vec2()
local scale = obs.vec2()
local bounds = obs.vec2()
local crop = obs.obs_sceneitem_crop()
obs.obs_sceneitem_get_pos(from_item, pos)
obs.obs_sceneitem_set_pos(to_item, pos)
obs.obs_sceneitem_get_scale(from_item, scale)
obs.obs_sceneitem_set_scale(to_item, scale)
obs.obs_sceneitem_set_rot(to_item, obs.obs_sceneitem_get_rot(from_item))
obs.obs_sceneitem_set_alignment(to_item, obs.obs_sceneitem_get_alignment(from_item))
obs.obs_sceneitem_set_bounds_type(to_item, obs.obs_sceneitem_get_bounds_type(from_item))
obs.obs_sceneitem_set_bounds_alignment(to_item, obs.obs_sceneitem_get_bounds_alignment(from_item))
obs.obs_sceneitem_get_bounds(from_item, bounds)
obs.obs_sceneitem_set_bounds(to_item, bounds)
obs.obs_sceneitem_get_crop(from_item, crop)
obs.obs_sceneitem_set_crop(to_item, crop)
obs.obs_sceneitem_set_scale_filter(to_item, obs.obs_sceneitem_get_scale_filter(from_item))
obs.obs_sceneitem_set_blending_method(to_item, obs.obs_sceneitem_get_blending_method(from_item))
obs.obs_sceneitem_set_blending_mode(to_item, obs.obs_sceneitem_get_blending_mode(from_item))
obs.obs_sceneitem_set_locked(to_item, true)
end
local function strip_refresh_param(url)
if url == nil then return "" end
url = string.gsub(url, "([?&])obs_refresh_ts=[^&#]*&?", function(sep)
if sep == "?" then return "?" else return "" end
end)
url = string.gsub(url, "%?$", "")
url = string.gsub(url, "?&", "?")
return url
end
local function with_refresh_param(url)
local clean = strip_refresh_param(url)
local sep = string.find(clean, "?", 1, true) and "&" or "?"
return clean .. sep .. "obs_refresh_ts=" .. tostring(os.time()) .. tostring(math.random(1000, 9999))
end
local function create_helper_if_needed(target_source, target_item, scene)
if helper_source ~= nil then return true end
local settings = obs.obs_source_get_settings(target_source)
if settings == nil then return false end
obs.obs_data_set_bool(settings, "shutdown", false)
obs.obs_data_set_bool(settings, "restart_when_active", false)
obs.obs_data_set_string(settings, "css", build_custom_css())
helper_source = obs.obs_source_create("browser_source", helper_name, settings, nil)
obs.obs_data_release(settings)
if helper_source == nil then
return false
end
obs.obs_source_set_hidden(helper_source, true)
helper_item = obs.obs_scene_add(scene, helper_source)
if helper_item == nil then
obs.obs_source_release(helper_source)
helper_source = nil
return false
end
copy_item_transform(target_item, helper_item)
obs.obs_sceneitem_set_visible(helper_item, false)
return true
end
local function update_source_from_canonical(dest_source, canonical_source)
local canonical = obs.obs_source_get_settings(canonical_source)
if canonical == nil then return false end
local is_local = obs.obs_data_get_bool(canonical, "is_local_file")
if is_local then
obs.obs_data_release(canonical)
return false
end
local url = obs.obs_data_get_string(canonical, "url")
if url == nil or url == "" then
obs.obs_data_release(canonical)
return false
end
obs.obs_data_set_bool(canonical, "shutdown", false)
obs.obs_data_set_bool(canonical, "restart_when_active", false)
obs.obs_data_set_string(canonical, "url", with_refresh_param(url))
obs.obs_data_set_string(canonical, "css", build_custom_css())
obs.obs_source_update(dest_source, canonical)
obs.obs_data_release(canonical)
return true
end
local function finish_swap()
obs.timer_remove(finish_swap)
local scene_src, scene, target_item, h_item = get_current_scene_and_items()
if scene_src == nil or target_item == nil or h_item == nil then
if scene_src ~= nil then obs.obs_source_release(scene_src) end
busy = false
return
end
copy_item_transform(target_item, h_item)
if showing_helper then
obs.obs_sceneitem_set_visible(target_item, true)
obs.obs_sceneitem_set_visible(h_item, false)
showing_helper = false
else
obs.obs_sceneitem_set_visible(h_item, true)
obs.obs_sceneitem_set_visible(target_item, false)
showing_helper = true
end
obs.obs_source_release(scene_src)
busy = false
end
function perform_refresh()
if busy or source_name == nil or source_name == "" then return end
local target_source = obs.obs_get_source_by_name(source_name)
if target_source == nil then return end
local id = obs.obs_source_get_id(target_source)
if id == nil or string.find(id, "browser_source", 1, true) == nil then
obs.obs_source_release(target_source)
return
end
local scene_src, scene, target_item, h_item = get_current_scene_and_items()
if scene_src == nil or target_item == nil then
if scene_src ~= nil then obs.obs_source_release(scene_src) end
obs.obs_source_release(target_source)
return
end
if not create_helper_if_needed(target_source, target_item, scene) then
obs.obs_source_release(scene_src)
obs.obs_source_release(target_source)
return
end
h_item = obs.obs_scene_find_source_recursive(scene, helper_name)
if h_item == nil then
obs.obs_source_release(scene_src)
obs.obs_source_release(target_source)
return
end
copy_item_transform(target_item, h_item)
local hidden_source
if showing_helper then
hidden_source = target_source
else
hidden_source = helper_source
end
busy = true
local ok = update_source_from_canonical(hidden_source, target_source)
obs.obs_source_release(scene_src)
obs.obs_source_release(target_source)
if not ok then
busy = false
return
end
obs.timer_remove(finish_swap)
obs.timer_add(finish_swap, load_wait_ms)
end
function refresh_now_clicked(props, prop)
perform_refresh()
return false
end
local function restart_main_timer()
obs.timer_remove(perform_refresh)
if source_name ~= nil and source_name ~= "" then
obs.timer_add(perform_refresh, math.max(300, interval_sec) * 1000)
end
end
local function read_font_face(settings)
local font_obj = obs.obs_data_get_obj(settings, "rate_font")
if font_obj == nil then
return font_face
end
local face = obs.obs_data_get_string(font_obj, "face")
obs.obs_data_release(font_obj)
if face == nil or face == "" then
return font_face
end
return face
end
function script_update(settings)
local new_source = obs.obs_data_get_string(settings, "source_name")
local new_interval = math.max(300, obs.obs_data_get_int(settings, "interval_sec"))
obs.obs_data_set_int(settings, "interval_sec", new_interval)
local source_changed = new_source ~= source_name
if source_changed then
release_helper()
source_name = new_source
end
ensure_target_visible()
interval_sec = new_interval
font_face = read_font_face(settings)
font_size = obs.obs_data_get_int(settings, "font_size")
font_color = obs.obs_data_get_int(settings, "font_color")
font_bold = obs.obs_data_get_bool(settings, "font_bold")
outline_px = obs.obs_data_get_int(settings, "outline_px")
outline_color = obs.obs_data_get_int(settings, "outline_color")
apply_style_to_sources()
restart_main_timer()
end
function script_load(settings)
math.randomseed(os.time())
end
function script_unload()
obs.timer_remove(perform_refresh)
obs.timer_remove(finish_swap)
release_helper()
end
もし、もし配信ページなどに書いてくださる場合は、
「スマメイトのレートをOSBだけで自動更新するやつ」
とか、
「レート表示@nekofuton」
とか、そんな感じで…
こういうのって、たいしたものでもないのにおこがましいって感じるし、書いてくださいって言ってるみたいであんまり好きじゃないけど、書いてもらえるとうれしいっていう、どうにもならない板挟みなんですよね…
