[#VCI][#Vキャス]それでもボクはルームでコメントを受け取りたい...
はじめに
バーチャルキャストはVRChatやClusterのようなVRSNSの一種ですが、その最大の魅力は何といってもワールドを超えてアイテムを持ち歩けるVCIだと思っています。まさにメタバースの重要キーワードの相互運用(Interoperability) を体現した特徴ですね。
さて、今年はルームでニコニコ動画のコメントを受け取る方法についてまとめたいと思います。
私は普段は配信をメインにVキャスを使ってるのでコメントをVR内で確認するのは非常に重要な事なんですが残念ながらルームでは未だサポートされていません。一応非公式のコメントビューア連携コメビュなどを使えば実現出来るのですが、その仕組みにも興味があったし自分でも作ってみる事にしました。
なお、今回紹介する方法はデバックモードを利用したややハック的な方法となっています。公式が推奨するセキュアな方法では無いので、その点は注意をし限定的 / 実験的な自己責任の利用に留めてください。
ルームとコメント連携機能
VCIにはニコニコ動画のコメントを取得できる機能があります。ちょうど@sirohigeさんが7日目の記事でも書かれていますね。
しかし、残念ながら2022/12/12現在、このコメントの取得機能はスタジオでしか動作しません。そのためルームではコメントビューアなどが動作しません。。。 公式の対応もいずれはされると思うのですが、現状は出来ないので代替手段として自分でVCIでコメントの取得から実装したくなりますよね? ただ、そこでVCIのセキュリティという大きな壁が立ちはだかります。
VCIのセキュリティとジェイルブレイク
VCIは非常にセキュリティを意識して作られた仕様です。Luaによる拡張機能を備えながらもサンドボックスとして実装されており例えば以下のような事は出来ません。
- PC内のローカルファイルへのI/O
- インターネットを含むネットワーク通信
- PC内の別ソフトウェアの起動や連携
機能としては備えているらしく、一部のミクランド等に使われる公式VCIでは限定的に使われる事もあるようですが、現在は安全性を考慮しながら必要に応じて解放していくそうです。VCIにマルウェア等が仕込まれるリスクを考えるとこれは非常にありがたい運用なのですが、今回に限れば何らかの方法で上記3つのいずれかを実現する必要があります。
実はこの点に関しては先駆者が居て、デバック機能を利用してローカルのLuaスクリプトを書き換える事でPC側のアプリケーションとやり取りをすることが出来ます。
VCIではデバックモードを利用する事で直接ローカルのスクリプトを読み込む事が出来ます。C:/Users/{USER}/AppData/LocalLow/infiniteloopCo,Ltd/VirtualCast/EmbeddedScriptWorkspace/{VCI名}/_xxx.lua
というファイルをxxx.lua
という形に修正すると作業ファイルとしてVCIに埋め込まれたLuaよりも優先して読み込まれます。これはVCIのスクリプトを開発する上で非常に便利な必須機能なのですが、これを間違った利用をするのが今回のやり方。
つまり外部プログラムからこのLuaスクリプトを書き換えてやればVCIとアプリケーション間の疑似的な通信が実現出来ます。かなり力業ですが綺麗な方法は必要な機能の公式サポートを待ちましょう><
ルーム向けコメントビューアのアーキテクチャ
ルーム向けのコメントビューアは以下のようなデザインにします。
コメントの取得は自前で作ると保守が大変なのでマルチコメントビューア(MCV)を利用させていただきました。MCVで 「取得したコメントをファイル(data.lua)に書き出す」 というシンプルなプラグインをC#で作成します。続いてdata.lua
を含むデバックモードで動作するSender.vci
からViewer.vci
にコメント情報をメッセージで送信し、Viewer.vci
で実際にメッセージの表示等を行います。
Sender.vci
とViewer.vci
を分離するのには2つ理由があります。ひとつは送信用とビュアーで分離して開発出来るので一つの送信VCIで複数のUIをサポートする事が出来ること 、もう一つは状態管理を簡単にするためです。デバックモードでローカルの作業ファイルを書き換えてしまうと該当のVCI全体がリロードされます。そのためコメントが来る度にdata.luaを更新してしまうと1コメントずつしか表示されません。MCVプラグイン側でそうした過去のコメント履歴も管理しても良いのですが、ちょっと扱いづらくなるので、シンプルにVCIを分けることにしました。これで状態はViwer.vci
に持つ事で問題を回避できます。
またdata.lua
は以下のようなJSON風フォーマットで書き出す事でVCIスクリプト側での扱いが簡単になります。
return {
{
name = "紅月",
comment = "コメントテスト",
cmntSource = "MCV"
},
{
name = "koduki",
comment = "Hello World",
cmntSource = "MCV"
}
}
MCVプラグインの実装
まずはMCVのプラグインを作ります。残念ながら公式のプラグインドキュメントを見つけれなかったのですが、ありがたい事に以下の記事にやり方が載ってたのでそちらを参考にしています。
Visual Studioのインストールとプロジェクトの作成
まずは何はともあれVSのインストールです。既存で持っていなければ無償で利用できるCommunity版を以下からダウンロードして入れましょう。最新のバージョンは2022です。
インストールが完了したらプロジェクトの作成を行います。利用するテンプレートは .NET/.NET Standard です。似た名前のクラスライブラリ(.NET Framework) でも問題無いですが、最新とも互換のある.NET Standad 2.0を使うのが良い気がしています。
ファイル名は任意のプラグイン名ですが 「..Plugin.dll」 という名前で最終的に出力する必要があるのでプラグインとしておくのが良いでしょう。クラスライブラリは.NET Frameworkと互換のある .NET Standard 2.0 を選びます。
必要なパッケージのインストールとライブラリの参照
NCVのプラグイン作成にはSystem.ComponentModel.Composition
が必要なのでNuGetを使ってインストールします。
続いて参照という機能を使ってMCVのDLLを読み込みます。依存関係を右クリック -> プロジェクト参照の追加 -> 参照でいけます。参考にしたサイトとVSのバージョンが変わってUIが違ったので少し戸惑いました。
追加するDLLはMCV配下にある以下です。
- MultiCommentViewer\dll\SitePlugin.dll
- MultiCommentViewer\dll\Plugin.dll
- MultiCommentViewer\dll\NicoSitePlugin2.dll
- MultiCommentViewer\dll\NicoLiveIF.dll
上2つはMCVプラグインとして必須、下2つはニコ生向けです。YouTubeなどに対応させるには適宜参照を足す必要がありそうです。
スケルトンの作成とデプロイ
using System;
using Plugin;
using SitePlugin;
using System.ComponentModel.Composition;
namespace VCIConnectPlugin
{
[Export(typeof(IPlugin))]
public class Class1 : IPlugin, IDisposable
{
public string Name { get { return "プラグイン名"; } }
public string Description { get { return "プラグイン説明"; } }
public IPluginHost Host { get; set; }
public virtual void OnLoaded() { }
public void OnClosing() { }
public void OnMessageReceived(ISiteMessage message, IMessageMetadata messageMetadata) { }
public void OnTopmostChanged(bool isTopmost) { }
public void ShowSettingView() { }
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing) { }
}
}
こちらをビルドします。
作成されたVCIConnectPlugin.dll
をMultiCommentViewer\plugins\{任意のフォルダ名}
に配置してMCVを起動します。
先ほど作ったプラグインがメニューに表れているのが分かります。
今回は全てスケルトンなので何も起こりませんが、イベントとして特に重要なのがコメントの取得時に実行されるOnMessageReceived
です。また、今回は実装しませんが設定画面等をGUIで作りたいときはメニュー選択時に実行されるShowSettingView
にFormの起動をする処理を書けばよいと思います。
コメント連携機能の実装
続いて連携機能を実装していきましょう。MtoVのコードを参考にさせてもらいました。また、Class1.csのままというのも味気ないのでPlugin.csという名前に変更してあります。
全文はこちらに置いています。OnMessageReceivedでコメントを受け取りそれをパースしてLua形式で作業ファイルのパスにファイル出力しています。今回はハードコーディングしてますが、実際にはFormなどで設定が出来るようにしておくと汎用性が高く出来そうです。また、今回は受けっとった端から逐次出力していますが、コメント量が多いケースを考えるとバッファリングをして定期的に書きだす方が良いかもしれません。
public void OnMessageReceived(ISiteMessage message, IMessageMetadata messageMetadata)
{
var msg = parseMesage(message);
var list = new List<string>();
list.Add(" return {");
list.Add("{");
list.Add($"name = \"{msg.name}\",");
list.Add($"comment = \"{msg.comment}\",");
list.Add($"cmntSource = \"{msg.cmntSource}\"");
list.Add("}");
list.Add("}");
var filePath = @"C:\Users\koduki\AppData\LocalLow\infiniteloop Co,Ltd\VirtualCast\EmbeddedScriptWorkspace\Comment Sender via MCV\";
var fileName = "data.lua";
using (var sw = new StreamWriter(filePath + fileName, false, Encoding.UTF8))
{
var contents = string.Join(Environment.NewLine, list);
sw.WriteLine(contents);
}
}
parseMesage
ではコメントをパースしています。ここでは運営コメントなどメッセージの種別ごとの変換を行ってINicoMessageからシンプルな文字列型に変換しています。
private (string name, string comment, string cmntSource) parseMesage(ISiteMessage message)
{
var name = "(名前なし)";
var comment = "(本文なし)";
var cmntSource = "Unknown";
if (message is INicoMessage NicoMessage)
{
name = "(運営)";
cmntSource = "MCV";
switch (NicoMessage.NicoMessageType)
{
case NicoMessageType.Connected:
comment = (NicoMessage as INicoConnected).Text;
break;
case NicoMessageType.Disconnected:
comment = (NicoMessage as INicoDisconnected).Text;
break;
case NicoMessageType.Item:
comment = (NicoMessage as INicoGift).Text;
break;
case NicoMessageType.Ad:
comment = (NicoMessage as INicoAd).Text;
break;
case NicoMessageType.Spi:
comment = (NicoMessage as INicoSpi).Text;
break;
case NicoMessageType.Info:
comment = (NicoMessage as INicoInfo).Text;
break;
case NicoMessageType.Emotion:
comment = "/emotion " + (NicoMessage as INicoEmotion).Content;
break;
case NicoMessageType.Comment:
if ((NicoMessage as INicoComment).Is184 == true) { name = ""; }
else{ name = (NicoMessage as INicoComment).UserName; }
comment = (NicoMessage as INicoComment).Text;
cmntSource = "Nicolive";
break;
}
}
return (name, comment, cmntSource);
}
これでMCVプラグインの作成は出来ました。つづいてこちらを読み込むVCIを作成していきましょう。
Sender VCIの作成
プロジェクトの準備
それでは本命のVCIの方を作っていきます。まずはUnityとUniVCIの準備ですが 6日目でしろひげさんと@sheeta_mmさんが書かれている記事と、ついでに私が以前書いた記事が参考になると思います。
CommentViwerExという名前のプロジェクトとSenderというシーンを作りました。VCI Objectを作成して適当にパラメータを入れておきます。
送信用のVCI Itemの準備
送信用のVCI Itemを作りますがこちらは特にこだわる部分はないので適当にスフィアあたりを作っておきます。このあたりを凝ったデザインにするのも良いですよね。
スクリプトの作成
それではスクリプトの作成を行います。まずは以下のようにVCI Objectでmain.lua
とdata.lua
を作ります。スクリプトのデバックを有効にするのを忘れないでください。
data.lua
は以下のように記載します。とりあえず空を返す感じですね。このファイルが先ほど作成したVCIConnectPluingで毎回ファイルごと作り直されます。
return {}
つづいてそれを取得して外部メッセージとして連携するmain.lua
は以下のように書きます。
local comments = require "data"
for i, c in ipairs(comments) do
print(c.name .. ": " .. c.comment)
vci.message.Emit("koduki.vcix.comment", c)
end
とりあえず取得したデータ構造をそのまま送信する単純なものですね。
Viewer VCIの作成
受信用VCI Itemの作成
それではコメントビューアの本体となる受信用VCIを作成します。作りとしてはこちらもいたってシンプルでInkscapeで作成した透過pngをCubeのテクスチャにして文字表示用などいくつかのTextをくっつけてるだけの状態になります。
この辺にいかにギミックを仕込んだりカッコいいものにするかはセンスと技量が問われるのですが、私にはこれが限界でした><
スクリプトの作成
受信用のスクリプトも書いていきましょう。ちなみに送信用と違い受信用は無理にデバッグオプションを有効にしなくても大丈夫です。開発中は付けた方が便利でしょうけど。
コメントビューアとしてメインの処理を行っているmain.lua
は以下のようになります。
local vcix = require "vcix"
local MAX_LINE = 11
local comments = {}
local function onComment(sender, name, message)
table.insert(comments, 1, { name = sender["name"], type = sender["type"], comment = message })
local text = ""
for i, c in ipairs(comments) do
text = c.comment .. "\n" .. text
if i == MAX_LINE then
break
end
end
vci.assets.SetText("TxtComments", text)
end
vcix.message.On("comment", onComment)
基本的には通常のvci.message.On("comment",...)
とI/Fを同じにしてあるので既存のコードがあればほぼそのまま使えると思います。違いはvci
をvcix
にするだけですね。vcixの中身は以下のようになります。
local callbackOnVCIComment = nil
local function onVCIXComment(sender, name, message)
_sender = {
name = message.name,
type = "comment",
cmntSource = message.cmntSource
}
_name = "comment"
_message = message.comment
callbackOnVCIComment(_sender, _name, _message)
end
local function On(type, callback)
if type == "comment" then
callbackOnVCIComment = callback
end
end
vci.message.On("koduki.vcix.comment", onVCIXComment)
local export = {}
export.message = {}
export.message.On = On
return export
先ほどの送信用VCIからメッセージを取得して、公式のCommentAPIと同じフォーマットに整えています。同じような変換を2度してるのでやや無駄にも感じてしまいますが、このあたりはAPI互換を優先した方が良い気がしています。
最終的に出来たものはこんな感じです。後ろに見える巨大な雪玉みたいなのが送信用VCIです。もう少し小さくした方が良いですね><
これでMCVを経由して無事にルームで使えるVCIコメントビューアを作る事が出来ました。
まとめ
今回はルームから使えるコメントビューアを作るためにVCIのセキュリティ境界をはみ出してみる方法を紹介しました。セキュリティはVCIやVキャスの良い部分だと思いますが、PoCとか実験をする分にはこういう方法もあるよ、という感じです。
ちなみに去年は「VCI入門】スライドを変更するレーザポインタを作る」という事で記事でPDFからVCIに変換する仕組みや「Vキャス内で撮った写真を自動でVCIアイテムに変換するチェキ機能」を作ってたのですが、どちらも公式から同等以上の機能がサポートされたので、非公式な方法でも作ってたら公式がちゃんとしたの作ってくれるというジンクスに期待もして、そろそろ欲しいルーム対応コメントビューアを作成しておきました。
それではHappy Hacking!
Discussion