Ruby:slimにdiscordrbをインストールできた

Rubyの公式dockerイメージの一つruby:slimにDiscord botライブラリのdiscordrbをインストールしたかった。

解決

  • makeとg++パッケージをインストールする
    • C++のライブラリが足りてないっぽい
# apt update
# apt install g++ make
# gem install discordrb
  • build-essential にg++もmakeも含まれるので、build-essentialでも可
    • g++とmake (151MB)
    • build-essential (221MB)

エラー

$ docker run -it --rm ruby:slim bash
# gem install discordrb

Building native extensions. This could take a while...
ERROR: Error installing discordrb:
ERROR: Failed to build gem native extension.

current directory: /usr/local/bundle/gems/unf_ext-0.0.7.7/ext/unf_ext
/usr/local/bin/ruby -I /usr/local/lib/ruby/2.7.0 -r ./siteconf20200406-597-121o8d0.rb extconf.rb
checking for -lstdc++... extconf.rb failed
Could not create Makefile due to some reason, probably lack of necessary
libraries and/or headers. Check the mkmf.log file for more details. You may
need configuration options.
Provided configuration options:
--with-opt-dir
--without-opt-dir
--with-opt-include
--without-opt-include=${opt-dir}/include
--with-opt-lib
--without-opt-lib=${opt-dir}/lib
--with-make-prog
--without-make-prog
--srcdir=.
--curdir
--ruby=/usr/local/bin/$(RUBY_BASE_NAME)
--with-static-libstdc++
--without-static-libstdc++
--with-stdc++-dir
--without-stdc++-dir
--with-stdc++-include
--without-stdc++-include=${stdc++-dir}/include
--with-stdc++-lib
--without-stdc++-lib=${stdc++-dir}/lib
--with-stdc++lib
--without-stdc++lib
/usr/local/lib/ruby/2.7.0/mkmf.rb:471:in try_do': The compiler failed to generate an executable file. (RuntimeError)<br> <br> You have to install development tools first.<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:564:intry_link0'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:582:in try_link'<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:801:intry_func'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:1029:in block in have_library'<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:971:inblock in checking_for'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:361:in block (2 levels) in postpone'<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:331:inopen'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:361:in block in postpone'<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:331:inopen'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:357:in postpone'<br> from /usr/local/lib/ruby/2.7.0/mkmf.rb:970:inchecking_for'
from /usr/local/lib/ruby/2.7.0/mkmf.rb:1024:in have_library'<br> from extconf.rb:6:in

'

To see why this extension failed to compile, please check the mkmf.log which can be found here:

/usr/local/bundle/extensions/x86_64-linux/2.7.0/unf_ext-0.0.7.7/mkmf.log

extconf failed, exit code 1

em files will remain installed in /usr/local/bundle/gems/unf_ext-0.0.7.7 for inspection.
Results logged to /usr/local/bundle/extensions/x86_64-linux/2.7.0/unf_ext-0.0.7.7/gem_make.out

logファイル

"gcc -o conftest -I/usr/local/include/ruby-2.7.0/x86_64-linux -I/usr/local/include/ruby-2.7.0/ruby/backward -I/usr/local/include/ruby-2.7.0 -I.    -O3 -ggdb3 -Wall -Wextra -Wdeprecated-declarations -Wduplicated-cond -Wimplicit-function-declaration -Wimplicit-int -Wmisleading-indentation -Wpointer-arith -Wwrite-strings -Wimplicit-fallthrough=0 -Wmissing-noreturn -Wno-cast-function-type -Wno-constant-logical-operand -Wno-long-long -Wno-missing-field-initializers -Wno-overlength-strings -Wno-packed-bitfield-compat -Wno-parentheses-equality -Wno-self-assign -Wno-tautological-compare -Wno-unused-parameter -Wno-unused-value -Wsuggest-attribute=format -Wsuggest-attribute=noreturn -Wunused-variable  -fPIC conftest.c  -L. -L/usr/local/lib -Wl,-rpath,/usr/local/lib -L. -fstack-protector-strong -rdynamic -Wl,-export-dynamic     -Wl,-rpath,/usr/local/lib -L/usr/local/lib -lruby  -lm   -lc"
 checked program was:
/* begin */
1: #include "ruby.h"
2:
3: int main(int argc, char **argv)
4: {
5:   return !!argv[argc];
6: }
/* end */

Azure SignalR Service の REST API では URL区切り文字を含んだユーザーIDがあて先のメッセージを送れない

Azure SignalR Service は serverless モードにして Azure Functions と使うことができる。その場合には Azure Functions から REST API1 で Azure SignalR Service へ メッセージを発行する。 現時点(2019/12/29)の Azure SignalR Service では URL区切り文字を含んだユーザーIDが有効である。しかし、REST API から URL区切り文字を含んだユーザーIDをあて先としたメッセージ送信ができない。 URL区切り文字を含んだユーザーIDとは例えば、"user/1"のような文字列。

原因は、Azure SignalR Service が REST API のパラメーターとして送られた URLエンコードされたユーザーIDをデコードせずにあて先ユーザーIDとするから。

ユーザーIDが user/1 の場合、REST API へ URL {prefix}/user%2F1 としてリクエストするとメッセージがユーザーIDが user%2F1 のクライアントへメッセージが送信される。 URLエンコードをせずに REST API へ URL {prefix}/user/1 としてリクエストすると、URL が API のルートに一致せずに 404 Not Found レスポンスが返る。

なお、この REST API では リクエスト URL は ヘッダーで送信する JWT の aud claim と一致する必要がある。ユーザーIDを URLエンコードした場合であっても、aud claim はエンコード前の文字列と一致して有効な API リクエストとして扱われる。

Azure SignalR Service へは、REST API 以外に websocket で接続してメッセージ送信することができる。その場合にはユーザーID に / スラッシュなどの URL区切り文字が含まれていてもメッセージのあて先にできる。

接続したクライアントのユーザーID は Hub の接続イベントで確認できる。Azure SignalR Service では EventGrid を通して Hub への connected イベントが購読できる。

ためしたこと

Azure SignalR Service SDK のコードを変更して Azure SignalR Service へ接続して試してみた。Microsoft.Azure.SignalR.Management.ServiceTransportType.Transient を指定した ServiceManager が REST API を使用する。API への HTTP リクエスト URL を組み立てるコードを変更して、ユーザーID を URLエンコードして実行した。

public RestApiEndpoint GetSendToUserEndpoint(string userId, TimeSpan? lifetime = null)
{
    var path_token = $"/users/{userId}";

    // user/1 -> user%2F1
    var path_url = $"/users/{Uri.EscapeDataString(userId)}";
    
    var token = _restApiAccessTokenGenerator.Generate($"{_audiencePrefix}{path_token}", lifetime);
    return new RestApiEndpoint($"{_requestPrefix}{path_url}", token);
}

https://github.com/Azure/azure-signalr/blob/c0875fcb8c90befb8dc07d69a826cb66219127de/src/Microsoft.Azure.SignalR.Management/RestApiProvider.cs#L44-L47

  • "user/1"だとする
  • Connected イベントでは、"user/1"
  • access_token.id では、 "{prefix}/user/1"
  • rest api
    • url = {prefix}/user/1 , token.aud = "{prefix}/user/1"
    • url = {prefix}/user/1 , token.aud = "{prefix}/user%2F1"
    • url = {prefix}/user%2F1 , token.aud = "{prefix}/user%2F1"
      • client "user%2F1" でメッセージを受信した
    • url = {prefix}/user%2F1 , token.aud = "{prefix}/user/1"
      • client "user%2F1" でメッセージを受信した

OBS Studio の browser source に スマホのカメラ映像を出す

OBSのブラウザソースは、Chromium Embedded Framework (CEF)で出来ているそうだ。ローカルファイルのHTMLをロードすることもできる。それを使ってAndroidスマホのカメラからの映像を表示しようと思った。WebRTCだ。

OBSのブラウザソースで表示したHTMLは、ボタンをクリックしたりテキスト入力したりできるがやりにくい。ページが読み込まれたら自動でWebRTCの接続をするようにした。

github.com

Pythonでこの用途限定のシグナリングサーバーを書いた。

  • GET /offer スマホ側がofferポーリングする
  • POST /offer OBS側がofferをポストする
  • GET /answer スマホ側がanswerをポーリングする
  • POST /answer OBS側がanswerをポストする

スマホ側もOBS側もローカルファイルからHTMLを読み込むので origin が "null"になる。CORSヘッダーで許可する必要がある。"null"の場合は、"*"ワイルドカードでの許可はできない。

from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    offer = None
    answer = None

    def send_corsHeaders(self):
        self.send_header("Access-Control-Allow-Origin", "null")
        self.send_header("Access-Control-Allow-Method", "POST, GET")
        self.send_header("Access-Control-Allow-Headers", "*")

    def do_OPTIONS(self):
        self.send_response(200)
        self.send_corsHeaders()
        self.end_headers()

    def do_GET(self):

        if self.path == '/offer' and self.offer:
            size, data = self.offer
        elif self.path == '/answer' and self.answer:
            size, data = self.answer
        else:
            self.send_response(204)
            self.send_corsHeaders()
            self.end_headers()
            return

        self.send_response(200)
        self.send_corsHeaders()
        self.send_header("Content-Type","application/json")
        self.send_header("Content-Length", size)
        self.end_headers()

        self.wfile.write(data)

    def do_POST(self):
        
        size = int(self.headers["content-length"])
        data = self.rfile.read(size)

        if self.path == '/offer':
            self.__class__.offer = [size, data]
        elif self.path == '/answer':
            self.__class__.answer = [size, data]
        else:
            self.send_response(204)
            self.send_corsHeaders()
            self.end_headers()
            return
            
        self.send_response(200)
        self.send_corsHeaders()
        self.end_headers()


HTTPServer.allow_reuse_address = True
HTTPServer.timeout = None
with HTTPServer(("0.0.0.0", 8080), Handler) as server:
    server.serve_forever()

スマホ側のJavaScriptコード。OBS側もほぼ一緒。Androidの保存場所はダウンロードフォルダ(Download)にした、他の場所は許可のこととかわからんかったから。Chromeからは file:///sdcard/Download/camera.htmlでアクセスできる。file:///はスラッシュ3つ。開発中はパソコンにUSBで繋いでChromeのdevtoolsのremote devicesから開いて操作した。

SDPのメッセージには改行が含まれているので、JSON.stringifyしてもJSON.parseでエラーになってしまう。stringify、parseメソッドの第2引数にreplacer, reviver関数を渡して改行を処理した。

LAN内で接続するだけなので、RTCPeerConnectionにTURNサーバーを設定しない。それでもiceGatheringのために関数を抜ける必要があるので、awaitでiceGatheringStateがcompleteになるのを待つ。

const pc = new RTCPeerConnection();

const mediaConstraints = {
  video: { facingMode: "user", width: 200, height: 200 }
};

const url_offer = "http://192.168.1.2:8080/offer";
const url_answer = "http://192.168.1.2:8080/answer";

const sleep = ms => new Promise(r => setTimeout(r, ms));

const replacer = (_, v) => {
  if (typeof v == "string") {
    return v.replace(/\r\n/g, "\n");
  }

  return v;
};

const reviver = (_, v) => {
  if (typeof v == "string") {
    return v.replace(/\n/g, "\r\n");
  }

  return v;
};

/* main */
(async () => {
  // attach stream
  const ms = await navigator.mediaDevices.getUserMedia(mediaConstraints);
  pc.addTrack(ms.getTracks()[0], ms);

  // build SDP
  pc.setLocalDescription(await pc.createOffer());

  const msg = await new Promise(rslv => {
    const f = () => {
      if (pc.iceGatheringState == "complete") {
        pc.removeEventListener("icegatheringstatechange", f);
        rslv(pc.localDescription);
      }
    };

    pc.addEventListener("icegatheringstatechange", f);
  });

  // send SDP
  let res = await fetch(url_offer, {
    mode: "cors",
    method: "POST",
    body: JSON.stringify(msg, replacer),
    headers: {
      "Content-Type": "application/json"
    }
  });

  // polling answer
  let ans;
  while (true) {
    const res = await fetch(url_answer, {
      mode: "cors",
      method: "GET"
    });

    if (res.status === 200) {
      ans = await res.text().then(txt => JSON.parse(txt, reviver));
      break;
    }

    await sleep(1000);
  }

  pc.setRemoteDescription(ans);
})();

その他

Chromeのビデオの自動再生は、mutedの場合だけ有効。1

fetch api の response.json()に reviverがない理由はBody.jsonがネイティブで実装されているから2

Pythonでwindows apiを呼ぶ

呼ぶ = call

ウィンドウサイズをどうかしたかった。

Jupyter と Pythonwindows api 触れたら楽だね。pywin32と足りない分は自分でctypes使えばできる。

miniconda をインストールしたらもうpywin32も入ってた。

ウィンドウのサイズはスナップしたときとそうでないときで違うらしい、よくわからん。

  • DwmGetWindowAttribute(DWMWA_EXTENDED_FRAME_BOUNDS )
  • GetWindowRect()
  • GetClientRect()

どれもそのままではMoveWindowで使える値は取れないみたいだった。

gist.github.com

ウィンドウにしるしをつけようと思ったけどできなかった

OBS Studioのウィンドウキャプチャがウィンドウを見つける方法は、

  • タイトル
  • 実行ファイル名
  • なるべく一致 (??)

ということで、複数のChromeウィンドウがあったときに配信用と定めたものにキャプチャを限定できないか考えてみた。

タイトルは、ウィンドウタイトルのことだと思われる。windows api の setWindowTextで書いてしまえばいいと思った。しかしChromeの場合、アクティブなタブが変わる度にそのタブのdocument.Title + "- Google Chrome"になってしまうのでダメだった。

実行ファイル名は、ハードリンクで別名のexeを作ってそれから起動したらいけるんじゃないかと思ったけどchrome.exeのままだった。ダメだった。どういう仕組みかはわからなかった。

ということであきらめた。

https://www.reddit.com/r/PowerShell/comments/6lg1tm/possible_to_override_application_window_title/

twitchでスクリーンキャストストリーミングをはじめた

OBS Studio を使ってる。

配信用に設定したChromeを用意した。「ハードウェアアクセラレーションが使用可能な場合は使用する」がONになっているとOBSでウィンドウキャプチャができなかった。ユーザーディレクトリごと別に分けてしまって、"設定"画面で、ハードウェアアクセラレーションをOFFにした。それと配信に映ってしまわないように、google アカウントで同期するものを絞っておいた。--user-data-dir=pathのオプションを追加したショートカットファイルを作った。Windowsのタスクバーに追加した。ショートカットファイルのオプション違いはスタートメニューやタスクバーでまとめられてしまうけど、Chromeの --user-data-dir や、--profile 違いのショートカットキーはまとめられない。

Twitchのコミュニティガイドラインに目を通した。

Twitchユーザーの多様な年齢層や国際的なコミュニティの文化的背景を考慮

外出時またはショッピングモールおよびレストランに出かける時のような服装の着用が適切です

御意。