- 投稿日:2019-12-23T21:10:46+09:00
ラズパイ4とGStreamerでストリーミングサーバーを作ろう
やること
ラズパイ4にカメラをつけてストリーミング配信の環境を作ります。 ラズパイに付けられるマイクがなかったので、音声なし映像のみです。
参考
- ラズパイを映像展示用デバイスにした話
- ラズパイにNodejsインストール
- [メモ]video.jsでHLS配信をやってみた
- Video.jsの使い方
- Raspberry Pi + Node.jsでSkyWayを動かしてみる
- GStreamerのインストール
構成図
カメラを付けたラズパイを、GStreamerとNodejsでストリーミングサーバにします。
更に、CORSを設定し別のWEBサーバでもストリーミングデータが利用できるようにします。
環境
- ラズパイ
- Raspberry Pi 4 Model B 4GB
$ lsb_release -a No LSB modules are available. Distributor ID: Raspbian Description: Raspbian GNU/Linux 10 (buster) Release: 10 Codename: buster
- カメラ
カメラがサポートするフォーマット
$ v4l2-ctl --list-formats-ext ioctl: VIDIOC_ENUM_FMT Type: Video Capture [0]: 'YU12' (Planar YUV 4:2:0) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [1]: 'YUYV' (YUYV 4:2:2) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [2]: 'RGB3' (24-bit RGB 8-8-8) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [3]: 'JPEG' (JFIF JPEG, compressed) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [4]: 'H264' (H.264, compressed) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [5]: 'MJPG' (Motion-JPEG, compressed) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [6]: 'YVYU' (YVYU 4:2:2) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [7]: 'VYUY' (VYUY 4:2:2) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [8]: 'UYVY' (UYVY 4:2:2) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [9]: 'NV12' (Y/CbCr 4:2:0) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [10]: 'BGR3' (24-bit BGR 8-8-8) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [11]: 'YV12' (Planar YVU 4:2:0) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [12]: 'NV21' (Y/CrCb 4:2:0) Size: Stepwise 32x32 - 2592x1944 with step 2/2 [13]: 'BGR4' (32-bit BGRA/X 8-8-8-8) Size: Stepwise 32x32 - 2592x1944 with step 2/2このようにラズパイに接続します。(写真はイメージです。基板はラズパイ4ではありません)
GStreamerをインストールしよう
ストリーミング配信に必要なアプリGStreamerをインストールします。以下2つのコマンドを実行すればインストール完了です。(ということがわかるまで苦労しました)
$ sudo apt install autoconf automake libtool $ sudo apt install gstreamer1.0-tools gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly libgstreamer1.0-dev libgstreamer-plugins-base1.0-devGStreamerの動作確認しよう
早速ストリーミングデータを作成してみます。
$ mkdir test $ cd test $ sudo gst-launch-1.0 -v -e v4l2src device=/dev/video0 ! video/x-h264,width=640,height=480,framerate=15/1 ! h264parse ! mpegtsmux ! hlssink max-files=8 target-duration=5 location=./segment%05d.ts playlist-location=./output.m3u8 playlist-root=/実行時はこんな感じです。
実行すると、このように
.m3u8
,.ts
ファイルが作成されます。$ ls output.m3u8 segment00000.ts segment00001.ts segment00002.tsGStreamerの出力ファイルを確認しよう
.m3u8
の中は、.ts
ファイルとの相対パスが書かれてました。実際の相対パスとずれていないか確認します。(これに気が付かず苦労しました)$ cat output.m3u8 #EXTM3U #EXT-X-VERSION:3 #EXT-X-ALLOW-CACHE:NO #EXT-X-MEDIA-SEQUENCE:0 #EXT-X-TARGETDURATION:9 #EXTINF:8.7807645797729492, /segment00000.ts #EXTINF:7.9985880851745605, /segment00001.ts #EXTINF:6.8206477165222168, /segment00002.ts
.ts
ファイルを確認しよう
.ts
ファイルをWindowsにコピーしてダブルクリックで実行します。カメラ映像が再生されれば成功です。(.ts
ファイルは動画データなんですね、へぇー)Nodejsをインストールしよう
GStreamerでストリーミングデータが作れるようになりました。そのデータをWebブラウザで開くことができるように、NodejsをインストールしてラズパイをWebサーバにします。
# npmを更新 sudo npm install npm@latest -g # npmのバージョン確認 $ npm -v # -> 6.13.4 $ sudo apt update $ sudo apt install curl $ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash - $ sudo apt install nodejs -y # nodejsのバージョン確認 $ node -v # -> v12.14.0Nodejsがインストールできたので、プロジェクトを作成します。
$ mkdir nodejs $ cd nodejs $ npm init $ npm install --save express $ touch app.js $ mkdir wwwroot $ touch wwwroot/test.html
app.js
ファイルを以下のようにします。app.jsvar express = require('express'); var app = express(); app.use(express.static('wwwroot')); var port = 3000; app.listen(port,function(){ console.log("サーバがポート%dで起動しました。モード:%s",port,app.settings.env) });
test.html
ファイルを以下のようにします。wwwroot/test.html<!DOCTYPE html> <html><head></head><body> TEST </body></html>Webサーバとして動作するか試運転します。
$ pwd /home/pi/nodejs $ node app.js サーバがポート3000で起動しました。モード:developmentブラウザで
http://<ラズパイのIPアドレス>:3000/test.html
を開き、Webサイトが開けば成功です。ストリーミングサーバにしよう!
ストリーミングデータの作成とWebサーバを用意できました。これら2つを使ってストリーミングサーバを作ります。
具体的には、ストリーミングデータをhtmlの
<video> </video>
タグで表示できるようにします。そのためには、video.js
というモジュールを使うことでChromeでも表示できるようになります。必要なファイル(
.m3u8
,.ts
以外)は、githubに置きました。これをwwwroot
配下に格納します。
https://github.com/zgw426/GStreamerSample/tree/master/sample01ストリーミングデータ含め、各ファイルがこのように配置されるようにします。
$ pwd /home/pi/nodejs/wwwroot $ tree . ├── output.m3u8 ├── segment00056.ts ├── segment00057.ts ├── segment00058.ts ├── segment00059.ts ├── segment00060.ts ├── segment00061.ts ├── segment00062.ts ├── segment00063.ts ├── static │ ├── css │ │ └── video-js.min.css │ └── js │ ├── video.min.js │ ├── videojs-contrib-hls.min.js │ └── videojs-contrib-media-sources.min.js └── streamtest.htmlストリーミングサイトを開いてみよう
http://<ラズパイのIPアドレス>:3000/streamtest.html
を開くとこのような画面が表示されます。動画を再生して、ラズパイに付けたカメラ映像が動画として表示されれば成功です。
これで、ストリーミングサーバが完成しました。
CORSを有効にしよう
冒頭に紹介した2つ目の構成にします。
別にWebサーバーを用意して、そちらのサイトでストリーミング配信ができるようにします。これには、CORS(オリジン間リソース共有)を有効にする必要があります。
CORSが有効でないと、
Access to XMLHttpRequest at 'http://xxx' from origin 'http://yyy' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
というエラーが発生し再生できません。NodejsにCORSモジュールをインストールしよう
ラズパイのNodejsにCORSを有効にするため、モジュールをインストールします。
$ npm install cors
app.js
を以下のようにします。これで、CORS設定が完了です。※注意※
この設定では、どのサーバからもリソースが利用できる状態で、セキュリティ的には危険な状態です。本来なら、特定のサーバからのみアクセス許可するなど、許可する範囲を最小にします。app.jsvar express = require('express'); var cors = require('cors'); var app = express(); app.use(cors()); app.use(express.static('wwwroot')); var port = 3000; app.listen(port,function(){ console.log("サーバがポート%dで起動しました。モード:%s",port,app.settings.env) });CORSを有効にすると、このように別PCのWebサーバでもストリーミング配信ができるようになります。
おまけ:遅延テスト
ストリーミング配信にはいくらか遅延があります。配信時間が長くなった場合、遅延がどうなるか検証してみました。16時間連続稼働でもそれほど遅くならず、個人的には満足な値でした。
結果
遅延 開始時 18秒 1.5時間後 42秒 16時間後 27秒
- 投稿日:2019-12-23T21:01:06+09:00
StreamでBuffer.concatしない冴えたやりかた
はじめに
この記事は Node.js Advent Calendar 2019 の 24 日目です。
https://twitter.com/yosuke_furukawa/status/1201778011286065153
Yosuke FURUKAWA @yosuke_furukawa
なんだこの誕生日アドベントカレンダーは / “Node.js Advent Calendar 2019 - Qiita”
午後5:19 · 2019年12月3日·はてなブックマーク最初は誕生日カレンダーみたいになってたので、ネタ記事を書こうかなーと思っていました。
ですが、始まってみると案外真面目な記事ばかりだったので、真面目に書くことにしました。
(そう、私は12/24生まれです!{クリスマス,誕生日}{ケーキ,プレゼント}はいつも一つ。)StreamでBuffer.concatしない冴えたやりかた
一般的にStreamを扱う時は
on("data", ...)
で処理することが多いと思います。
しかし、実際に使うサイズと流れてくるサイズは一致しないことが多く…const net = require('net'); function onRecv(packet){ /* ... */ } const server = net.createServer(conn => { let receivedQueue = Buffer.alloc(0); conn.on('data', chunk => { let data = Buffer.concat([receivedQueue, chunk]); //40バイトずつ読み出したい while(data.length >= 40){ onRecv(data.slice(0, 40)); data = data.slice(40); } receivedQueue = data; }); }).listen(3000);のような処理になりがちです。…なんだか冗長に感じませんか?
他の言語では読み出し可能バイト数を取得、指定バイト以上なら一気に読み出す…みたいな処理を書くことが多いと思います。Arduinoのシリアル通信とか。
Node.jsで同じような処理は書けないのか?書けます。Node.jsには、任意のバイト数を読み出すための
readable
イベントが存在します。しかし、以前までは "読み出し可能なバイト数" を取得する方法が
stream._readableState.length
しかありませんでした。_
が気になりますね…
でも今は大丈夫!
v9.4.0から、これを取得するためのreadableLength
プロパティが追加されています。
stream.readableLength
罪悪感(?)が無くなりましたね。これを使うと、先程のコードを以下のように書くことができます:
const net = require('net'); function onRecv(packet){ /* ... */ } const server = net.createServer(conn => { conn.on('readable', () => { while(conn.readableLength >= 40){ onRecv(conn.read(40)); } }); }).listen(3000);ああ…気持ちいい!これが書きたかった。これが求めていたもの・・・
コードが綺麗になることは勿論、Buffer.concatはデータの複製を行うので、処理速度の改善も期待できます。
このパターンが使える場面では積極的にreadableイベントを使っていきましょう。注意点
この方法で読み込める最大バイト数は
highWaterMark
までになります。
大きなデータをまとめて取得したい時は、highWaterMark
の上限値を拡大する必要があります。
※ただ、デフォルトのhighWaterMark
を超えるデータを一気に処理する処理自体あまり良くないので、少しずつ処理できるようにしたほうがいいかもしれません。p.s.
highWaterMark
は、シンプルに説明すると他言語のbufferSizeみたいなものです。(実際にはもっとイケてる実装の一部だったりしますが、とりあえずはそんなものだと思えば十分。)さいごに
あまりreadableイベントを使った記事を見かけないので書いてみました。お試しあれ。
おわりー。
- 投稿日:2019-12-23T15:32:41+09:00
MacにNode.jsをインストール(anyenv + nodenv編)
プロジェクトごとにNode.jsのバージョンを管理できる!?
っていう話を聞いたのでnodenvをインストールすることにしました。
nodebrewだとnodebrew use [バージョン番号]
みたいに切り替えるのがめんどくさくて大変で。
nodenvだと簡単にできるっぽいのでこっちを使ってみます。
いろいろなプロジェクトに関わってくるとこういうのがすごくありがたい。
開発者の方に感謝です。インストールの流れ
- Homebrewのインストール
- anyenvのインストール
- nodenvのインストール
- Node.jsのインストール
・Homebrew
HomebrewはMac用のパッケージマネージャ。
ツールのインストールとか諸々を管理してくれます。
http://brew.sh/index_ja.html
nodebrewをインストールするためにHomebrewを使います。・nodenv
順番前後しますが、これがメイン。Node.jsのバージョン管理ツール。
https://github.com/nodenv/nodenv・anyenv
様々なenv系ツールをまとめてくれるらしい。
https://github.com/anyenv/anyenv
nodenvだけでいいんじゃないのって思ってましたが、nodenv単体でインストールするときには環境変数など色々操作するみたいですが(そうでもない?)、anyenvを使えばあらあら簡単にインストールできるみたいです。
あと他のenv系ツール使ってたら色々把握するのに面倒だけど、anyenv使ってれば簡単に把握できるみたい。
(間違ってたらご指摘ください・・・)
ということでanyenvを使ってみます。Homebrewインストール
まずはHomebrewから。
Mac使ってるとこれはまあ外せない。
もうインストール済みって人はスキップで。http://brew.sh/index_ja.html にあるスクリプトを実行する。
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"※ 2019/12/23現在は上記
インストール後は以下コマンドでHomebrewのバージョンが確認できます。
% brew -v
私の場合は以下でした。
Homebrew 2.1.11 Homebrew/homebrew-core (git revision be0385; last commit 2019-09-12) Homebrew/homebrew-cask (git revision d34609; last commit 2019-09-12)anyenvインストール
とその前にnodebrew用に設定していた環境変数をコメントアウトします。
nodebrewを使っていた人はこちらをやっておいたほうがベターかと。
使ったことないって人はスキップでOK。
僕の場合、nodebrewが優先になるように記述していたのでコメントアウトしました。% vi .zprofile # export PATH=$HOME/.nodebrew/current/bin:$PATH # ↑この部分をコメントアウトviを使ってコメントアウトしました。
まあ、テキストエディタならなんでも良いと思うので適宜使いやすいやつを使ってください。Homebrewを使ってanyenvインストール
さっそくインストールしていきましょう。
% brew install anyenv
そして初期化
% anyenv init # Load anyenv automatically by adding # the following to ~/.zshrc: eval "$(anyenv init -)"うーん?つまりこの一文を ~/.zshrcに記述しろっちゅうこうとなんやな!
(たぶんbashとか使っている人は~/.bashrcとかになるのかな)ということで~/.zshrcに追記
% echo 'eval "$(anyenv init -)"' >> ~/.zshrcターミナルを再起動してみます。
すると。。。ANYENV_DEFINITION_ROOT(/Users/whoami/.config/anyenv/anyenv-install) doesn't exist. You can initialize it by: > anyenv install --initはい、しょっぱなエラーで焦ります。
You'll see a warning if you don't have manifest directory.
とマニュアルにあるのでマニフェストディレクトリ作っちゃいましょう。
マニュフェストディレクトリを作る
指示に従って
anyenv install --init
と打ってみます。% anyenv install --init Manifest directory doesn't exist: /Users/whoami/.config/anyenv/anyenv-install Do you want to checkout ? [y/N]: y Cloning https://github.com/anyenv/anyenv-install.git master to /Users/whoami/.config/anyenv/anyenv-install... Cloning into '/Users/whoami/.config/anyenv/anyenv-install'... remote: Enumerating objects: 48, done. remote: Total 48 (delta 0), reused 0 (delta 0), pack-reused 48 Unpacking objects: 100% (48/48), done.途中check outするかってきたので「y(もろちん ちん!)」で承諾してます。
これでOKかな?
あってるかどうか念のためターミナルを再起動してみたら今度はエラーが出ない!!(やったー!)nodenvをインストール
さっそくanyenvでやってみましょうぞ!
anyenv install nodenv
を実行すればnodenvがインストールできます。% anyenv install nodenv /var/folders/sn/v1hx7kls3t3f2d9s2r40krns6qc63m/T/nodenv.20191223143232.16265 ~ Cloning https://github.com/nodenv/nodenv.git master to nodenv... Cloning into 'nodenv'... remote: Enumerating objects: 18, done. remote: Counting objects: 100% (18/18), done. remote: Compressing objects: 100% (13/13), done. remote: Total 3915 (delta 4), reused 13 (delta 2), pack-reused 3897 Receiving objects: 100% (3915/3915), 696.59 KiB | 1.04 MiB/s, done. Resolving deltas: 100% (2579/2579), done. ~ ~/.anyenv/envs/nodenv/plugins ~ Cloning https://github.com/nodenv/node-build.git master to node-build... Cloning into 'node-build'... remote: Enumerating objects: 77, done. remote: Counting objects: 100% (77/77), done. remote: Compressing objects: 100% (52/52), done. remote: Total 18197 (delta 33), reused 42 (delta 11), pack-reused 18120 Receiving objects: 100% (18197/18197), 3.19 MiB | 2.46 MiB/s, done. Resolving deltas: 100% (11638/11638), done. ~ ~/.anyenv/envs/nodenv/plugins ~ Cloning https://github.com/nodenv/nodenv-default-packages.git master to nodenv-default-packages... Cloning into 'nodenv-default-packages'... remote: Enumerating objects: 267, done. remote: Counting objects: 100% (267/267), done. remote: Compressing objects: 100% (151/151), done. remote: Total 531 (delta 151), reused 202 (delta 105), pack-reused 264 Receiving objects: 100% (531/531), 113.88 KiB | 291.00 KiB/s, done. Resolving deltas: 100% (272/272), done. ~ ~/.anyenv/envs/nodenv/plugins ~ Cloning https://github.com/nodenv/nodenv-vars.git master to nodenv-vars... Cloning into 'nodenv-vars'... remote: Enumerating objects: 211, done. remote: Total 211 (delta 0), reused 0 (delta 0), pack-reused 211 Receiving objects: 100% (211/211), 31.82 KiB | 264.00 KiB/s, done. Resolving deltas: 100% (76/76), done. ~ Install nodenv succeeded! Please reload your profile (exec $SHELL -l) or open a new session.インストール成功です!
最後にプロファイルのリロードのため(exec $SHELL -l)
するか再起動しろと書いてるので、おしゃれにコマンド打ってみます。% exec $SHELL -lうひょ。
env
で環境変数がどうなったかみてました。% env PATH=/Users/whoami/.anyenv/envs/nodenv/shims:/Users/whoami/.anyenv/envs/nodenv/bin:/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin:/usr/local/sbin:/Users/k akai/.nodebrew/current/bin NODENV_ROOT=/Users/whoami/.anyenv/envs/nodenv NODENV_SHELL=zsh (他省略)なんかnodenvやらanyenvのパスがPATHに追加されてますね。
NODENV_ROOTとかNODENV_SHELLみたいなのも追加されてます。
これで下準備は整いました。たぶん。Node.jsのインストール
インストール可能なバージョンを見る
% nodenv install -lこうすれば一覧でてきました。
すごく多いです。。。Node.jsをインストール
以下を実行すれば指定したバージョンをインストールできます。
% nodenv install {バージョン番号}ちなみにアンインストールは
~/.nodenv/versions
あるディレクトリを削除すれば良いようです。
以下のコマンドでインストール済みのバージョンを確認できます。
shell
% nodenv whence npm
nodenv globalでコンピュータのデフォルトに指定
nodenv global
でコンピュータのデフォルトを指定してみます。
指定できるのはインストールしたバージョンです。nodenv global {バージョン番号}僕は10.0.0にしてみました。
確認してみましょう。% node -v v10.0.0
10.0.0になってるうう!
nodenv localでプロジェクト指定
nodenv local
を使えばプロジェクト指定できるみたいです。
早速使ってみます。# プロジェクトのパスに移動し % cd {プロジェクトのパス} # 配下で実行 % nodenv local {バージョン番号}実行するとプロジェクト配下に.node-versionっていうファイルができました。
中をみるとバージョン番号が記述されてます。.node-version{バージョン番号}8.8.0を指定してみたので確認してみましょう。
% node -v v8.8.0
しゅ、しゅごい!
すごい便利です。
これでNodeの切り替えが楽になりました。ちなみに
nodenv shell
コマンドだとシェルごとに変えられるっぽいです。
便利すぎる。。。
是非みなさんも使ってみてください。参考
- 投稿日:2019-12-23T11:22:43+09:00
備忘録 Node.jsでexportsを使ったモジュール化の作り方
Node.jsには、モジュール化といってさまざまな機能を持ったプログラムを個別のファイルに分割することができる。
効率よくコードを管理できるだけでなく、さまざまなユーザーが作成したプログラムを再利用しやすくできる。
Node.jsにはこのようなモジュールを管理できるツール「npm」や「yarn」等がある。
「exports」
基本的なモジュール化の方法として「exports」がある。
exportsを使うと、指定した値を外部のNode.jsファイルから読み込んで再利用することができるようになる。
exports.プロパティ名 = 値
この値は文字列・数値・配列・オブジェクト・関数など、さまざまなものを設定できる。
exports.str = 'こんにちは';
exports.obj = {
name: 'メロン',
price: 500,
store: '東京店舗'
};exports.func = function() {
console.log('exportsの関数');
};「module.exports」
module.exportsを利用するのが一般的。
両者の大きな違いは、プロパティ名付与せずに値を直接格納できるか。
module.exportsの場合、下記の様にプロパティ名をつけずに値を設定できる。module.exports = {
name: 'メロン',
price: 500,
store: '東京店舗'
};「module.exports」の場合はmoduleオブジェクトの中にあるexportsプロパティを参照している。
exportsも同じようにデフォルト状態ではmoduleオブジェクトの中にあるexportsプロパティを参照しているが、値を直接格納してしまうとそれはただの新しいオブジェクトに変わってしまうからモジュール化にはならない。
まとめ
・exportsは任意のプロパティ名を付与することでさまざまな値をモジュール化できる
・module.exportsも同じことが実現できてプロパティ名無しでもモジュール化できる
・exportsは必ずプロパティ名を付与しないと正しく機能しない
- 投稿日:2019-12-23T03:32:28+09:00
Maceyarn installしたらnode-gypのエラーが出た
どうした
既存のプロジェクトの手直しをしようとして、ローカルにnode_modulesを入れたかったので以下のコマンドを実行したらエラーが出た。
yarn //yarn install の短縮形エラーは以下。
Command: node-gyp rebuild Arguments: Directory: /Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents Output: gyp info it worked if it ends with ok gyp info using node-gyp@5.0.5 gyp info using node@13.2.0 | darwin | x64 gyp info find Python using Python version 2.7.16 found at \"/System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python\" gyp info spawn /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python gyp info spawn args [ gyp info spawn args '/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py', gyp info spawn args 'binding.gyp', gyp info spawn args '-f', gyp info spawn args 'make', gyp info spawn args '-I', gyp info spawn args '/Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents/build/config.gypi', gyp info spawn args '-I', gyp info spawn args '/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/addon.gypi', gyp info spawn args '-I', gyp info spawn args '/Users/user/Library/Caches/node-gyp/13.2.0/include/node/common.gypi', gyp info spawn args '-Dlibrary=shared_library', gyp info spawn args '-Dvisibility=default', gyp info spawn args '-Dnode_root_dir=/Users/user/Library/Caches/node-gyp/13.2.0', gyp info spawn args '-Dnode_gyp_dir=/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp', gyp info spawn args '-Dnode_lib_file=/Users/user/Library/Caches/node-gyp/13.2.0/<(target_arch)/node.lib', gyp info spawn args '-Dmodule_root_dir=/Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents', gyp info spawn args '-Dnode_engine=v8', gyp info spawn args '--depth=.', gyp info spawn args '--no-parallel', gyp info spawn args '--generator-output', gyp info spawn args 'build', gyp info spawn args '-Goutput_dir=.' gyp info spawn args ] No receipt for 'com.apple.pkg.CLTools_Executables' found at '/'. No receipt for 'com.apple.pkg.DeveloperToolsCLILeo' found at '/'. No receipt for 'com.apple.pkg.DeveloperToolsCLI' found at '/'. gyp: No Xcode or CLT version detected! gyp ERR! configure error gyp ERR! stack Error: `gyp` failed with exit code: 1 gyp ERR! stack at ChildProcess.onCpExit (/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:351:16) gyp ERR! stack at ChildProcess.emit (events.js:210:5) gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:270:12) gyp ERR! System Darwin 19.2.0 gyp ERR! command \"/usr/local/Cellar/node/13.2.0/bin/node\" \"/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js\" \"rebuild\" gyp ERR! cwd /Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents前までは普通にできとったやんけ!と思いつつ解決法を探して彷徨ったので記録しておく。
環境
- ProductName: Mac OS X
- ProductVersion: 10.15.2
- BuildVersion: 19C57
- node: 13.5.0
- npm: 6.13.4
It works for me!
まずこのコマンドを実行して、何も出力がないことを確認して欲しい。
pkgutil --packages | grep CL出力が存在しないことを確認したら、ターミナルで以下のコマンドを実行して、MacのCommand Line toolsを再インストールしたところ、エラーが解決したことが確認できた。
yarn cache clean sudo rm -rf $(xcode-select -print-path) xcode-select --installxcodeを入れ直すと解決するようだ。
もしこの方法で解決しなければ、このGitHubのIssueが参考になるので漁って見て欲しい。
まとめ
もっと親切なメッセージを出してくれ頼む(たのむ)
- 投稿日:2019-12-23T03:32:28+09:00
Macでyarn installしたらnode-gypのエラーが出た
どうした
既存のプロジェクトの手直しをしようとして、ローカルにnode_modulesを入れたかったので以下のコマンドを実行したらエラーが出た。
yarn //yarn install の短縮形エラーは以下。
Command: node-gyp rebuild Arguments: Directory: /Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents Output: gyp info it worked if it ends with ok gyp info using node-gyp@5.0.5 gyp info using node@13.2.0 | darwin | x64 gyp info find Python using Python version 2.7.16 found at \"/System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python\" gyp info spawn /System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python gyp info spawn args [ gyp info spawn args '/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py', gyp info spawn args 'binding.gyp', gyp info spawn args '-f', gyp info spawn args 'make', gyp info spawn args '-I', gyp info spawn args '/Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents/build/config.gypi', gyp info spawn args '-I', gyp info spawn args '/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/addon.gypi', gyp info spawn args '-I', gyp info spawn args '/Users/user/Library/Caches/node-gyp/13.2.0/include/node/common.gypi', gyp info spawn args '-Dlibrary=shared_library', gyp info spawn args '-Dvisibility=default', gyp info spawn args '-Dnode_root_dir=/Users/user/Library/Caches/node-gyp/13.2.0', gyp info spawn args '-Dnode_gyp_dir=/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp', gyp info spawn args '-Dnode_lib_file=/Users/user/Library/Caches/node-gyp/13.2.0/<(target_arch)/node.lib', gyp info spawn args '-Dmodule_root_dir=/Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents', gyp info spawn args '-Dnode_engine=v8', gyp info spawn args '--depth=.', gyp info spawn args '--no-parallel', gyp info spawn args '--generator-output', gyp info spawn args 'build', gyp info spawn args '-Goutput_dir=.' gyp info spawn args ] No receipt for 'com.apple.pkg.CLTools_Executables' found at '/'. No receipt for 'com.apple.pkg.DeveloperToolsCLILeo' found at '/'. No receipt for 'com.apple.pkg.DeveloperToolsCLI' found at '/'. gyp: No Xcode or CLT version detected! gyp ERR! configure error gyp ERR! stack Error: `gyp` failed with exit code: 1 gyp ERR! stack at ChildProcess.onCpExit (/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:351:16) gyp ERR! stack at ChildProcess.emit (events.js:210:5) gyp ERR! stack at Process.ChildProcess._handle.onexit (internal/child_process.js:270:12) gyp ERR! System Darwin 19.2.0 gyp ERR! command \"/usr/local/Cellar/node/13.2.0/bin/node\" \"/usr/local/Cellar/node/13.2.0/libexec/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js\" \"rebuild\" gyp ERR! cwd /Users/user/.ghq/github.com/hppRC/gatsby-firebase-sample/node_modules/fsevents前までは普通にできとったやんけ!と思いつつ解決法を探して彷徨ったので記録しておく。
環境
- ProductName: Mac OS X
- ProductVersion: 10.15.2
- BuildVersion: 19C57
- node: 13.5.0
- npm: 6.13.4
It works for me!
まずこのコマンドを実行して、何も出力がないことを確認して欲しい。
pkgutil --packages | grep CL出力が存在しないことを確認したら、ターミナルで以下のコマンドを実行して、MacのCommand Line toolsを再インストールしたところ、エラーが解決したことが確認できた。
yarn cache clean sudo rm -rf $(xcode-select -print-path) xcode-select --installxcodeを入れ直すと解決するようだ。
もしこの方法で解決しなければ、このGitHubのIssueが参考になるので漁って見て欲しい。
まとめ
もっと親切なメッセージを出してくれ頼む(たのむ)
- 投稿日:2019-12-23T01:19:45+09:00
Node.js で stream を使って gzip ファイルの先頭1行を読み込む
この記事は Node.js アドベントカレンダー 2019 の 23 日目です。
はじめに
gzip ファイルなどの圧縮されたファイルを読み込む際、たとえば csv など圧縮率の高いファイル形式かつ大きなファイルの場合、全てをメモリに乗せ切れないことが稀にあります。
そもそも csv のカラムだけ欲しいなどの場合にデータ全体を取得するのは時間もかかるし無駄です。
そこで、 Node.js は stream を扱いやすい言語なので、これを使って簡単に解決できるため紹介します。なお、 S3 からのデータ取得であっても
createReadStream()
すればstream.Readable
型になるため、同様の手法が可能です。そもそも、この話自体がローカルよりはクラウド絡みの方が多いケースになると思いますが……。おさらい: Node.js での stream でのファイル読み書き
例えば、ファイルを読み込んで標準出力に表示します。
import fs from 'fs'; const input = fs.createReadStream('tsconfig.json', 'utf-8'); input.pipe(process.stdout)書き込みの例として、大きなサイズの csv を生成するスクリプトをファイルに書き込みます。
src/create-big-csv.tsimport fs from 'fs'; const out = fs.createWriteStream('bigdata.csv', 'utf-8'); const arr = [...Array(100000)].map((_, idx) => idx); out.write("id,pow\n"); arr.forEach(idx => { out.write(`${idx}, ${idx * idx}\n`); })大きなデータなので複数に別れて buffer で流れます。データが来るたびに区切り文字を表示して標準出力に表示する場合はこうです。
import fs from 'fs'; const input = fs.createReadStream('bigdata.csv', 'utf-8'); input.on('data', (buf) => { console.log(buf.toString()) console.log('---') });gzip の展開を stream に適用する
標準ライブラリの zlib から pipe を作成し適用します。
import zlib from 'zlib'; import fs from "fs"; const gzip = zlib.createGunzip() async function main() { const readStream = fs.createReadStream('bigdata.csv.gzip') readStream .pipe(gzip) .on('data', (buf) => { console.log(buf.toString()) console.log('---') }) } main().catch((e) => { console.error(e); process.exit(1); });先頭一行を取得する
普通に
buf.toString()
した値を"\n"
で split できます。
なお、stream.destroy()
が間に合わず次のデータが流れてくることは普通にあるので、一度限りの処理に限定できるよう関数に切り出すのが良さそうです。import zlib from 'zlib'; import fs from "fs"; const gzip = zlib.createGunzip() async function main() { const readStream = fs.createReadStream('bigdata.csv.gzip') const firstLine = await getFirstLineFromStream(readStream.pipe(gzip)) console.log(firstLine) } async function getFirstLineFromStream(stream: Readable) { return new Promise((resolve, reject) => { stream.on('data', (buf) => { stream.destroy(); const string = buf.toString(); const [firstLine] = string.split("\n"); resolve(firstLine); }) stream.on('error', reject); }) } main().catch((e) => { console.error(e); process.exit(1); });おまけ: S3 から取得した gzip の先頭一行を取得する
s3.getObject().createReadStream()
するだけです。 await は要りません。import zlib from 'zlib'; import fs from "fs"; import { S3 } from 'aws-sdk'; const gzip = zlib.createGunzip() async function main() { const s3 = new S3() const readStream = s3.getObject({Bucket: 'your-awesome-bucket', Key: 'bigdata.csv.gzip'}).createReadStream(); const firstLine = await getFirstLineFromStream(readStream.pipe(gzip)) console.log(firstLine) } async function getFirstLineFromStream(stream: Readable) { return new Promise((resolve, reject) => { stream.on('data', (buf) => { stream.destroy(); const string = buf.toString(); const [firstLine] = string.split("\n"); resolve(firstLine); }) stream.on('error', reject); }) } main().catch((e) => { console.error(e); process.exit(1); });おわりに
大きなデータを扱うときは、メモリに乗り切らないこともあるので stream を使いましょう。
- 投稿日:2019-12-23T01:19:45+09:00
Node.js で stream を使って gzip ファイル全体をメモリに乗せずに先頭一行だけを取得する
この記事は Node.js アドベントカレンダー 2019 の 23 日目です。
はじめに
gzip ファイルなどの圧縮されたファイルを読み込む際、たとえば csv など圧縮率の高いファイル形式かつ大きなファイルの場合、全てをメモリに乗せ切れないことが稀にあります。
そもそも csv のカラムだけ欲しいなどの場合にデータ全体を取得するのは時間もかかるし無駄です。
そこで、 Node.js は stream を扱いやすい言語なので、これを使って簡単に解決できるため紹介します。なお、 S3 からのデータ取得であっても
createReadStream()
すればstream.Readable
型になるため、同様の手法が可能です。そもそも、この話自体がローカルよりはクラウド絡みの方が多いケースになると思いますが……。おさらい: Node.js での stream でのファイル読み書き
例えば、ファイルを読み込んで標準出力に表示します。
import fs from 'fs'; const input = fs.createReadStream('tsconfig.json', 'utf-8'); input.pipe(process.stdout)書き込みの例として、大きなサイズの csv を生成するスクリプトをファイルに書き込みます。
src/create-big-csv.tsimport fs from 'fs'; const out = fs.createWriteStream('bigdata.csv', 'utf-8'); const arr = [...Array(100000)].map((_, idx) => idx); out.write("id,pow\n"); arr.forEach(idx => { out.write(`${idx}, ${idx * idx}\n`); })大きなデータなので複数に別れて buffer で流れます。データが来るたびに区切り文字を表示して標準出力に表示する場合はこうです。
import fs from 'fs'; const input = fs.createReadStream('bigdata.csv', 'utf-8'); input.on('data', (buf) => { console.log(buf.toString()) console.log('---') });gzip の展開を stream に適用する
標準ライブラリの zlib から pipe を作成し適用します。
import zlib from 'zlib'; import fs from "fs"; const gzip = zlib.createGunzip() async function main() { const readStream = fs.createReadStream('bigdata.csv.gzip') readStream .pipe(gzip) .on('data', (buf) => { console.log(buf.toString()) console.log('---') }) } main().catch((e) => { console.error(e); process.exit(1); });先頭一行を取得する
普通に
buf.toString()
した値を"\n"
で split できます。
なお、stream.destroy()
が間に合わず次のデータが流れてくることは普通にあるので、一度限りの処理に限定できるよう関数に切り出すのが良さそうです。import zlib from 'zlib'; import fs from "fs"; const gzip = zlib.createGunzip() async function main() { const readStream = fs.createReadStream('bigdata.csv.gzip') const firstLine = await getFirstLineFromStream(readStream.pipe(gzip)) console.log(firstLine) } async function getFirstLineFromStream(stream: Readable) { return new Promise((resolve, reject) => { stream.on('data', (buf) => { stream.destroy(); const string = buf.toString(); const [firstLine] = string.split("\n"); resolve(firstLine); }) stream.on('error', reject); }) } main().catch((e) => { console.error(e); process.exit(1); });おまけ: S3 から取得した gzip の先頭一行を取得する
s3.getObject().createReadStream()
するだけです。 await は要りません。import zlib from 'zlib'; import fs from "fs"; import { S3 } from 'aws-sdk'; const gzip = zlib.createGunzip() async function main() { const s3 = new S3() const readStream = s3.getObject({Bucket: 'your-awesome-bucket', Key: 'bigdata.csv.gzip'}).createReadStream(); const firstLine = await getFirstLineFromStream(readStream.pipe(gzip)) console.log(firstLine) } async function getFirstLineFromStream(stream: Readable) { return new Promise((resolve, reject) => { stream.on('data', (buf) => { stream.destroy(); const string = buf.toString(); const [firstLine] = string.split("\n"); resolve(firstLine); }) stream.on('error', reject); }) } main().catch((e) => { console.error(e); process.exit(1); });おわりに
大きなデータを扱うときは、メモリに乗り切らないこともあるので stream を使いましょう。
- 投稿日:2019-12-23T00:34:26+09:00
toioで生き物っぽい何かを作ってみた
ご覧いただきありがとうございます。
この記事はSB-AI Advent Calendar 2019の18日目の記事となります。はじめに
突然ですが皆様は「人工生命」と聞いてどんなものをイメージしますか?
SFに出てくる怪物のようなイメージを持つ方も多いかもしれませんが、家を掃除してくれるルンバも人工生命の一つと言えます。
「人工生命」は国際的には「ALIFE」という名称で世界各国で研究が進められており、人工的に生命を作る事を通じて生命とは何かを理解しようとする研究分野の事です。私はこの分野の研究者でもなんでもない、ただのサラリーマンですが、仕事でロボットに関わる機会が多く、
また昨年ALIFE関連のハッカソンに参加しチームで作品を作った経験から、プライベートで「人工生命」を1つのテーマとしてモノづくりをしております。
(嬉しい事にチームで作った作品が、今年の文化庁メディア芸術祭のアート部門で審査委員会推薦作品として選出されました!動画はこちら)今回の記事は、私が最近注目している「toio(トイオ)」というロボットのあるアプリに着目し、それを参考にtoioで生き物っぽい何かを作ってみた。という記事です。
※この記事は筆者がtoioで遊んでみて勝手に思った事を書いているだけの記事となりますので、ALIFEの研究記事等ではございません。ご了承ください。toioについて
toioはsonyから2019年3月に発売されたロボットトイです。
※画像はこちらより引用:公式サイトを見ていただけると一目瞭然ですが、主に子供向けのプログラミングトイとしての用途が主で、
カセットを用いて遊べる内容を変えたり、白いキューブにレゴなどを用いてオリジナルな何かを作ったりできるようになっています。
またスクラッチベースのビジュアルプログラミング環境の提供もされています。画像にある白いキューブが「toio core cube」と言われるロボット本体なのですが、このcubeの仕様はネット上に公開されており、
またtoio.jsというnode.js用のライブラリも合わせて提供されていたり等、子供から上級者な大人まで遊べるようになっています。今後も新たなエンタメ系ソフトやアプリの登場が予定されており、今後の展開が楽しみなtoioですが、
個人的に気になったtoio用スマホアプリが11月から提供開始となりました。「ウロチョロス」現る
11月12日にtoioの新プロジェクトの発表があり、その際に発表された一つに「ウロチョロス」というアプリがありました。
(開発元のモリカトロンさんの記事より引用:https://morikatron.ai/2019/11/toio_urochoros/)当時はtoioを買おうか迷っていたものの、TLに流れてきた下記のツイートの動画たちを見てすぐにamazonで購入しました。
生き物っぽいものを作りたい私にとって、まさに目指すべき一例でしたので即決でした。
ウロチョロスたちが鬼ごっこをしています。よく見ると個体ごとの性格の違いが見えるかも。#toio pic.twitter.com/6meUmeykBM
— モリカトロンAIラボ (@morika_ailabo) November 12, 2019動画では鬼ごっこをしていますが、この他にも自由にcube達に動き回ってもらうモードや、ユーザーの表情判定をcube達が行うモードがあったりします。
(※顔の撮影はスマホで行う)
なお、ユーザーの表情判定は純粋に画像認識したときのスコアで順位をつけているのではなく、最終的に個々のcubeが持つ評価基準を用いて判定が下されるのだとか。ウロチョロスの生き物らしさとは
自分も実際にウロチョロスを試してみて、自由に動き回るモードでは様々なcube達のやり取りが観察できました。
・会話をしているようなアクション
・モノマネ(一方のcubeが他方のcubeに技を教える)
・パワー切れのcubeに対してのサポートアクション
・ダンス 等。ウロチョロス観察日記その1 pic.twitter.com/N8JUm0kXnk
— MARVO@anima (@marvo737) December 22, 2019ウロチョロス観察日記その2 pic.twitter.com/XnxS6CFTY3
— MARVO@anima (@marvo737) December 22, 2019そこでわかってきたのは、ウロチョロスはcube同士の相互作用によって、生き物らしく見せているのではないか、という点でした。
コードは確認できないため、あくまでも想像ではありますが、一方のアクションが他方のアクションに影響を及ぼすような制御が多くなされていると見受けられます。
ここで考えたいのは、双方のアクションを把握するためには、toioがどこにいるかなど、周辺環境の情報取得の方法です。
昨今のロボットはカメラやセンサーによって周辺環境位を把握し自己位置推定等を行いますが、toioには見た目の通りカメラなどはありません。
そこでポイントになってくるのが「プレイマット」です。toioのプレイマットについて
※画像はこちらより引用
ウロチョロスで遊ぶには、「トイオコレクション」と言う別売のキットに付属する「プレイマット」と呼ばれる台紙が必要となります。
この台紙はただの台紙ではなく、紙面上に座標を示すPositionIDが印刷されている特殊な台紙となっています。
cubeにはそのPositionIDを読み出すセンサーが搭載されており、そのおかげでcubeの現在のx,y座標、さらにはcubeのx軸に対しての時計回りの角度を利用したプログラミングをすることが可能です。つまり、このプレイマット上であればtoioを自由に操ることが可能であり、次からは2台のcubeの座標データを用いてそれっぽいものを作ってみたいと思います。
生き物っぽい何かを作ってみる
■実行環境
・macOS Catalina 10.15.2
・Node.js v12.13.0
・toio.js 42ae66a細かいセットアップ及び関数の説明については省略いたします。
下記が今回作成したコードとなります。toio.jsのサンプルをベースとしています。random_chase.jsconst { NearScanner } = require('@toio/scanner') // calculate chasing cube's motor speed function chase(jerryX, jerryY, tomX, tomY, tomAngle) { const diffX = jerryX - tomX const diffY = jerryY - tomY const distance = Math.sqrt(diffX * diffX + diffY * diffY) if (distance < 70) { return [0, 0] // stop } let relAngle = (Math.atan2(diffY, diffX) * 180) / Math.PI - tomAngle relAngle = relAngle % 360 if (relAngle < -180) { relAngle += 360 } else if (relAngle > 180) { relAngle -= 360 } const ratio = 1 - Math.abs(relAngle) / 90 let speed = 80 if (relAngle > 0) { return [speed, speed * ratio] } else { return [speed * ratio, speed] } } async function main() { // start a scanner to find nearest two cubes const cubes = await new NearScanner(2).start() // connect two cubes (tom chases jerry) const jerry = await cubes[0].connect() const tom = await cubes[1].connect() //set movable area const min_x = 100 const max_x = 400 const min_y = 100 const max_y = 400 // set light color and store position let jerryX = 0 let jerryY = 0 let jerryAngle = 0 jerry.turnOnLight({ durationMs: 0, red: 255, green: 0, blue: 255 }) jerry.on('id:position-id', data => { jerryX = data.x jerryY = data.y jerryAngle = data.angle }) // set light color and store position let tomX = 0 let tomY = 0 let tomAngle = 0 tom.turnOnLight({ durationMs: 0, red: 0, green: 255, blue: 255 }) tom.on('id:position-id', data => { tomX = data.x tomY = data.y tomAngle = data.angle }) const randRange = (min, max) => Math.floor(Math.random() * (max - min) + min); var random_x = 100 var random_y = 100 setInterval(() => { random_x = randRange(min_x,max_x) random_y = randRange(min_y,max_y) },2000) setInterval(() => { jerry.move(...chase(random_x, random_y, jerryX, jerryY, jerryAngle), 100) console.log("jerryX : " + random_x + " jerryY : " + random_y + " jerryAngle" + jerryAngle + "\n"); }, 50) setInterval(() => { tom.move(...chase(jerryX, jerryY, tomX, tomY, tomAngle), 100) }, 100) } main()これで動かしてみるとこのような動きになります。
自作ウロチョロスもどき pic.twitter.com/tczSdCawtk
— MARVO@anima (@marvo737) December 22, 2019X,Y座標を指定範囲内でランダムに一定時間で変化させ、cube1(jerry)がその座標を追いかけて、cube2(tom)がcube1を追いかける、といった内容です。
ただ、この状態だとcube同士がよくぶつかるため、cube内のモーションセンサで衝突検知して距離を取るなどしてあげないといけないのですが、そちらの実装はまた次回にしたいと思います。。
それができてやっと相互作用について少し語れるようになるかと思うのですが、時間の都合上今回はここまでといたします。最後に
今回はウロチョロスアプリの影響を受け、toioで生き物っぽい(定義が曖昧で申し訳ありません、、)何かを作ってみました。
本当はもう少しALIFEと絡めることができれば良かったのですが、完全に力不足です。。修行あるのみですね。。toioのcubeとプレイマットの組み合わせは、まだまだ面白そうなものが作れそうな予感がします。
マット上であれば座標だけでなく角度も取得できるので、cubeを出力デバイスではなく、入力デバイスとして使ってみるのも面白そうです。
(個人的にはTouchDesignerのコントローラーにしてみたいと考えています。)以上、ありがとうございました。