Life is Tech ! Advent Calendar 2016 の24日目の記事です。
クリエイティブコーディング Advent Calendar 2016に書くか迷いました。
ということで、あまり初心者向けな丁寧な解説はそこまで載せてないので、またまだまだ修正できるとこがあると思うので書いていただける方編集リクエストお待ちしております。
この記事では、openFrameworksと Twitter Streaming APIs と Node.js を使って、リアルタイムにTweetを拾ってきてそれの描画みたいなことをしようと思います。
そもそも、この記事で紹介するものはは京大生の @YoshimasaSakuragi の多大な contribute があり、去年一緒にやった武蔵野美術大学芸術祭プロジェクションマッピング企画とかに向けて作ったものです(この時のものを少し修正しました)
例えば、東工大メディア研究会のOFFTHERAILSというイベントで作ったものとか、大勢の観客がいて気軽にインタラクションみたいなのできるのはTwitterなのではという考えの元に、そういったイベントで使ったりしています。
FacebookのGraph APIと連携するバージョンもそのうち書きたいと思います。
# 環境
今回はOSX環境のみの紹介になります。基本的に私の環境が少し古いですが、ほとんど、最新バージョンと互換性があるみたいなので最新に全部持っていけば問題ないかと思います。
本記事の環境 | Image | 最新版のURL |
---|---|---|
Xcode 7.3.1 | https://developer.apple.com/download/ | |
OSX 10.11.4 | https://support.apple.com/ja_JP/downloads/macos | |
openFrameworks 0.9.2 | http://openframeworks.cc/ja/download/ | |
ofxJSON | https://github.com/jefftimesten/ofxJSON | |
ofxTrueTypeFontUL2 | https://github.com/kentaroid/ofxTrueTypeFontUL2 | |
ofxOpenGLContextScope | https://github.com/satoruhiga/ofxOpenGLContextScope | |
Node.js v4.1.1 | https://nodejs.org/en/ | |
twit 2.0.1 | https://www.npmjs.com/package/twit |
# 事前準備
node.jsとStreaming APIs (twitを使うけどkeyとかアクセストークンが必要なので登録)に関しては他の方が色々まとめてくれているので割愛しますが、この準備が以下の前提になっているので参考サイトをリンクしていますのでご準備ください。twitが便利だと思います。
- http://qiita.com/kokushi_yayoi/items/e4a5682c082acfa3218c
- http://take-she12.hatenablog.com/entry/2016/03/14/120000
- https://elstreet.style/elstreets/332
ここまでで
var tweet = require('twit');
var fs = require('fs');
var file = 'twitter.json';
var T = new tweet({
consumer_key: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
consumer_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
access_token: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
access_token_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
});
var stream = T.stream('statuses/filter', {track: '#NowPlaying'});
stream.on('tweet', function(tw) {
fs.writeFile(file, JSON.stringify(tw), null, null);
fs.readFile(file, 'utf8', function (err, tw) {
console.log(JSON.stringify(tw));
});
});
みたいな、適当なハッシュタグとかでつぶやかれたもの(今回は#NowPlaying
)を、
のような感じで、
$ node streaming.js
と実行すればtwitter.json
とかいう名前(任意の名前で大丈夫です)のファイルに書き込むようなもの(今回の場合はstreaming.js
というファイル)を用意できていれば事前準備は完了です。
# 作るものの整理(概観)
以下、クラスの命名とかちゃんとできてないかもしれないので、この記事とか参考に、編集リクエストお待ちしております。また、たまに箇条書きの形で貼っているURLは参考になるかも!と思ったリンクです。
##1. Projectの作成
AddonにofxJSON
,ofxTrueTypeFontUL2
,ofxOpenGLContextScope
を入れてプロジェクトを作成します。ofxJSON
はJsonを監視したり読み取るため、ofxTrueTypeFontUL2
は日本語などの描画のため、ofxOpenGLContextScope
は別スレッドでの画像の読み込みのために必要です。
##2. TweetData
みたいなクラスを作る
取得したデータをopenFrameworksのプロジェクトで使いやすいようデータに格納しますが、その際のクラスを用意しておきます。
##3. WStringConverter
の準備
日本語などのマルチバイト文字列を扱うために、wstring
というクラスを使います、その変換のためのWStringConverter
を準備します。
##4. FontManager
の準備
日本語などのマルチバイト文字列を表示するためにofxTrueTypeFontUL2
を使いますが、その管理をするクラスを作ります。
##5. TweetJsonReceiver
の準備
Node.js
で取得し続けているJSONファイルをパースして先ほど用意したTweetData
みたいなクラスを準備してそれに格納していくみたいなことをします。
##6. JsonReceiveThread
の準備
メインスレッドでJsonから読み取った画像のURLをたたいて画像を取得していたりするとFPSが落ちるので、別スレッドで実行します。そのためのJsonReceiveThread
を用意します。
また、このスレッドで画像の取得をするためにofxOpenGLContextScope
を使っています。
##7. TwitterStreamingManager
みたいなManagerクラスを作る
上記の、Twitterから取得したJsonファイル読み取ったり、ThreadにしたりをまとめるManagerみたいなものを、用意します。
##8. 取得データを描画する何かを書く
せっかく取得しましたが、ここまででは何も描画しないので、適当に描画するコードを書きます。ここまでの労力の1/10くらいでできます。
# 実装
また、今回はTwitterStreamingSample
という名前にしましたが、このディレクトリと、Node.js
で書いたstreaming.js
が含まれるディレクトリ(TweetStreamという名前にしてみました)が同じディレクトリ内にあるようにしておいてください(コード中に出てくるパスの設定のため)。
##2. TweetData
みたいなクラスを作る
コードを書きます。特に特別なことはせずに、Twitterから取得できるデータをデータごとに記述し、一度取得すればいいデータなので中身がいろいろ変更になることはなく、コンストラクタ内の引数でそのデータを渡す形をとりました。
#include "ofMain.h"
class TweetData{
public:
wstring tweet_text; //twwet contents
wstring user_name; //user name
string twitter_id; //screen_name @account name
string icon_url; //icon image url
int friends_count; //number of following
int statuses_count; //number of tweet
int followers_count;//number of follower
ofImage image; //to store icon image
TweetData();
TweetData(const wstring &tweet_text,
const wstring &user_name,
const string &twitter_id,
const string &icon_url,
const int &friends_count,
const int &statuses_count,
const int &followers_count);
};
#include "TweetData.hpp"
//------------------------------------------------------------------------------------
TweetData::TweetData(){
}
//------------------------------------------------------------------------------------
TweetData::TweetData(const wstring &tweet_text,
const wstring &user_name,
const string &twitter_id,
const string &icon_url,
const int &friends_count,
const int &statuses_count,
const int &followers_count){
cout << twitter_id << endl; // output console
this->tweet_text = tweet_text;
this->user_name = user_name;
this->twitter_id = twitter_id;
this->icon_url = icon_url;
this->friends_count = friends_count;
this->statuses_count = statuses_count;
this->followers_count = followers_count;
this->image.load(this->icon_url);
}
ここでは、jsonで取得したtweetに紐づく内容などをまとめて一つのクラスにするためのもので、コンストラクタに引数でデータをあげています。
##3. WStringConverter
の準備
ここはもう難しいので、気にせずコピペしてみてください。
C++と一概に言っても、windowsとOSX環境でも文字コードなどの違いからいろんなことを機にする必要があります。
// This was quoted from @YoshimasaSakuragi
#include "ofMain.h"
class WStringConverter{
public:
static wstring convToWString(string src);
template <class T>
static wstring convToUCS4(basic_string<T> src);
};
#include "WStringConverter.hpp"
//--------------------------------------------------------------------------------------------
wstring WStringConverter::convToWString(string src) {
#ifdef TARGET_WIN32
wstring dst = L"";
typedef codecvt<wchar_t, char, mbstate_t> codecvt_t;
locale loc = locale("");
if(!std::has_facet<codecvt_t>(loc))
return dst;
const codecvt_t & conv = use_facet<codecvt_t>(loc);
const std::size_t size = src.length();
std::vector<wchar_t> dst_vctr(size);
if (dst_vctr.size() == 0)
return dst;
wchar_t * const buf = &dst_vctr[0];
const char * dummy;
wchar_t * next;
mbstate_t state = {0};
const char * const s = src.c_str();
if (conv.in(state, s, s + size, dummy, buf, buf + size, next) == codecvt_t::ok)
dst = std::wstring(buf, next - buf);
return dst;
#elif defined __clang__
wstring dst = L"";
for (int i=0; i<src.size(); ++i)
dst += src[i];
#if defined(__clang_major__) && (__clang_major__ >= 4)
dst = convToUCS4<wchar_t>(dst);
#endif
return dst;
#else
return convToUCS4<char>(src);
#endif
}
//--------------------------------------------------------------------------------------------
template <class T>
wstring WStringConverter::convToUCS4(basic_string<T> src) {
wstring dst = L"";
int size = src.size();
int index = 0;
while (index < size) {
wchar_t c = (unsigned char)src[index];
if (c < 0x80) {
dst += (c);
}else if (c < 0xe0) {
if (index + 1 < size) {
dst += (((c & 0x1f) << 6) | (src[index+1] & 0x3f));
index++;
}
}else if (c < 0xf0) {
if (index + 2 < size) {
dst += (((c & 0x0f) << 12) | ((src[index+1] & 0x3f) << 6) |
(src[index+2] & 0x3f));
index += 2;
}
}else if (c < 0xf8) {
if (index + 3 < size) {
dst += (((c & 0x07) << 18) | ((src[index+1] & 0x3f) << 12) |
((src[index+2] & 0x3f) << 6) | (src[index+3] & 0x3f));
index += 3;
}
}else if (c < 0xfc) {
if (index + 4 < size) {
dst += (((c & 0x03) << 24) | ((src[index+1] & 0x3f) << 18) |
((src[index+2] & 0x3f) << 12) | ((src[index+3] & 0x3f) << 6) |
(src[index+4] & 0x3f));
index += 4;
}
}else if (c < 0xfe) {
if (index + 5 < size) {
dst += (((c & 0x01) << 30) | ((src[index+1] & 0x3f) << 24) |
((src[index+2] & 0x3f) << 18) | ((src[index+3] & 0x3f) << 12) |
((src[index+4] & 0x3f) << 6) | (src[index+5] & 0x3f));
index += 5;
}
}
index++;
}
return dst;
}
とりあえず、WStringConverter::convToWString(string str)
でstr
がwstringに変換されると思ってもらえれば大丈夫です。
##4. FontManager
の準備
日本語などのマルチバイト文字列を表示できるようにしたいので、まず適当なフォントファイルそれをdata/font/hoge.otf
みたいな感じで用意しておきます。
次にFontのFontManagerクラスを作ります。
配置が終われば、このフォントを使って文字を画面に出力するためのクラスを作ります。FontManagerは不変で毎度インスタンス化する必要はないので、staticなメソッドとしていろいろ定義しています。
#include "ofMain.h"
#include "ofxTrueTypeFontUL2.h"
class FontManager{
public:
FontManager(){};
static ofxTrueTypeFontUL2 mbigfont;
static ofxTrueTypeFontUL2 msmallfont;
static void init();
static void loadSmallFont(const string &arg,const int &size);
static void loadBigFont(const string &arg,const int &size);
static void drawSmallString(const string &arg ,const float &x,const float &y);
static void drawBigString(const string &arg ,const float &x,const float &y);
static void drawSmallWString(const wstring &arg ,const float &x,const float &y);
static void drawBigWString(const wstring &arg ,const float &x,const float &y);
};
#include "FontManager.hpp"
ofxTrueTypeFontUL2 FontManager::msmallfont;
ofxTrueTypeFontUL2 FontManager::mbigfont;
//--------------------------------------------------------------------------------
void FontManager::init(){
setlocale(LC_CTYPE,"");
if(msmallfont.isLoaded())return;
FontManager::loadSmallFont("font/hoge.otf", 12);
FontManager::loadBigFont("font/hoge.otf", 22);
}
//--------------------------------------------------------------------------------
void FontManager::loadSmallFont(const string &arg,const int &size){
msmallfont.loadFont(arg, size);
}
//--------------------------------------------------------------------------------
void FontManager::drawSmallString(const string &arg ,const float &x,const float &y){
msmallfont.drawString(arg, x, y);
}
//--------------------------------------------------------------------------------
void FontManager::drawSmallWString(const wstring &arg ,const float &x,const float &y){
msmallfont.drawString(arg, x, y);
msmallfont.useProportional(true);
msmallfont.useVrt2Layout(true);
}
//--------------------------------------------------------------------------------
void FontManager::loadBigFont(const string &arg,const int &size){
mbigfont.loadFont(arg, size);
}
//--------------------------------------------------------------------------------
void FontManager::drawBigString(const string &arg ,const float &x,const float &y){
mbigfont.drawString(arg, x, y);
}
//--------------------------------------------------------------------------------
void FontManager::drawBigWString(const wstring &arg ,const float &x,const float &y){
mbigfont.drawString(arg, x, y);
mbigfont.useProportional(true);
mbigfont.useVrt2Layout(true);
}
細かな設定の命令などはこちらのREADMEを参考にしてください。
##5. TweetJsonReceiver
の準備
C++では通常のクラスはコンストラクタの呼び出し(newの使用や非ポインタ、非参照変数の宣言など) 毎に実体が生成されます。
しかしログや重いオブジェクトなど、プログラム実行中に実体は一つのみ生成し、その実体を使いまわしたい場合があります。
この様な場合にシングルトンパターンを適用すると、 実体がプログラム中で一つであることが保証されます。
こちらは先ほどの、FontManager
とは違い、状態が不変ではないので、シングルトンパターンっぽく書いています。先ほどのものはモノステイトパターンっぽいです(namespaceで区切るほうが良いかもしれません)。
#include "ofxJSONElement.h"
#include "TweetData.hpp"
#include "ofMain.h"
#include "WStringConverter.hpp"
//保持するtweetのデータの最大値
#define DATA_MAX_NUM 50
class TweetJsonReceiver {
public:
int64_t cachedTweetId;
ofxJSONElement jsonElement;
static int data_num;
static vector<TweetData> tweetData;
static TweetJsonReceiver &getInstance();
TweetJsonReceiver(){}
TweetJsonReceiver &operator=(const TweetJsonReceiver &other){}
vector<TweetData> getTweetData();
bool checkUpdateJson();
void parseJson();
void init();
void update();
bool receive();
};
更新を追いかけるため、tweetの固有番号であるidをcheckUpdateJson()
内で毎回確認するような仕組みです。
#include "TweetJsonReceiver.hpp"
vector<TweetData> TweetJsonReceiver::tweetData;
int TweetJsonReceiver::data_num;
//--------------------------------------------------------------------------------------------
TweetJsonReceiver &TweetJsonReceiver::getInstance() {
static TweetJsonReceiver instance;
return instance;
}
//--------------------------------------------------------------------------------------------
vector<TweetData> TweetJsonReceiver::getTweetData(){
return tweetData;
}
//--------------------------------------------------------------------------------------------
bool TweetJsonReceiver::checkUpdateJson(){
if(jsonElement["id"].asInt64() != cachedTweetId){
cachedTweetId = jsonElement["id"].asInt64();
return true;
}else{
return false;
}
}
//--------------------------------------------------------------------------------------------
void TweetJsonReceiver::init(){
data_num = 0;
TweetJsonReceiver::tweetData.clear();
bool parsingSuccessful = jsonElement.openLocal("../../../TweetStream/twitter.json");//Nodeで取得したJSON
if (parsingSuccessful){
cout << "Parsing Successful" << endl;
cachedTweetId = jsonElement["id"].asInt64();
}else{
cout << "Failed to parse JSON" << endl;
}
}
//--------------------------------------------------------------------------------------------
void TweetJsonReceiver::parseJson(){
string tmptext = jsonElement["text"].asCString() ;
string tmpusername = jsonElement["user"]["name"].asCString();
wstring wtmptext = WStringConverter::convToWString(tmptext);
wstring wtmpusername = WStringConverter::convToWString(tmpusername);
tweetData.push_back((TweetData){
WStringConverter::convToWString(tmptext),
WStringConverter::convToWString(tmpusername),
jsonElement["user"]["screen_name"].asCString(),
jsonElement["user"]["profile_image_url"].asCString(),
jsonElement["user"]["friends_count"].asInt(),
jsonElement["user"]["statuses_count"].asInt(),
jsonElement["user"]["followers_count"].asInt()
});
data_num++;
return;
}
//--------------------------------------------------------------------------------------------
//Jsonの数に変化があればその分をtweetDataに追加
void TweetJsonReceiver::update(){
//tweetDataが無限にたまらないようにFIFOさせる
if(TweetJsonReceiver::tweetData.size()>DATA_MAX_NUM){
TweetJsonReceiver::tweetData.erase(TweetJsonReceiver::tweetData.begin());
cout << "CLear data" <<endl;
}
receive();
}
//--------------------------------------------------------------------------------------------
bool TweetJsonReceiver::receive(){
bool parsingSuccessful = jsonElement.openLocal("../../../TweetStream/twitter.json");//Nodeで取得したJSON
if (parsingSuccessful){
if(checkUpdateJson()){
parseJson();
return true;
}
}else{
cout << "Failed to parse JSON" << endl;
}
return false;
}
init()
を一度読んであげたのち、update()
で新しいtweetの情報が更新されていればTweetData
型のvectorであるtweetDataに要素が追加されていくようになっています。またコメントでも書きましたが、TweetJsonReceiver::tweetData
に要素が追加されすぎてメモリが足りなくなることを防ぐため、update()
内で要素数が増えすぎたら消すようにしています。
##6. JsonReceiveThread
の準備
先ほど用意したTweetJsonReceiver
をメインスレッドで呼ぶとFPSが落ちたりあまりいいことがないので、tweetDataへの要素の追加や画像の読み込みなどは、別スレッドで行いましょうということをするための準備をします。
このスレッドを用いない場合はメインスレッドで画像取得なども行うことになり処理速度が劇的に低下します。
#include "ofMain.h"
#include "TweetJsonReceiver.hpp"
#include "ofxOpenGLContextScope.h"
class JsonReceiveThread : public ofThread{
public:
void threadedFunction(){
ofxOpenGLContextScope scope;
while (isThreadRunning()) {
if(lock()){
TweetJsonReceiver::getInstance().update();
unlock();
sleep(100);
}
}
}
};
ヘッダファイルに実装も書いたので、JsonReceiveThread.cpp
はいじらなくて大丈夫です。(これwindowsの環境だと動かないかも)
このスレッドでは100msに一回TweetJsonReceiver::getInstance().update();
を読んでいるので、メインスレッドでアップデートしなくても、このスレッドで、tweetDataへの要素の追加や画像の読み込みなどやってくれることになります。
また、ofxOpenGLContextScope scope
というおまじないがないと、メインスレッド外での画像のallocate的なことができないっぽいのでおまじないだと思って書いておきましょう。
##7. TwitterStreamingManager
みたいなManagerクラスを作る
今までのを簡単に使えるようにManagerクラスとしてまとめたいと思います、(componentとかの方が名前としてはいいのかな?)
これも実体は一つでいいので、そのことを保証するためにも先ほどのデザインパターンを適用してもいいかもしれません。
#include "ofMain.h"
#include "TweetJsonReceiver.hpp"
#include "TweetData.hpp"
#include "ofxOpenGLContextScope.h"
#include "JsonReceiveThread.hpp"
class TwitterStreamingManager{
private:
JsonReceiveThread mJsonReceieveThread;
public:
void setup();
void exit();
};
#include "TwitterStreamingManager.hpp"
//------------------------------------------------------------------------------------
void TwitterStreamingManager::setup(){
//Twitter初期化はメインスレッドで行う
if(TweetJsonReceiver::tweetData.size()>0)return;
TweetJsonReceiver::getInstance().init();
//別スレッドで画像読み込みも含めてデータ取得する
ofxOpenGLContextScope::setup();
mJsonReceieveThread.startThread();
}
//------------------------------------------------------------------------------------
void TwitterStreamingManager::exit(){
mJsonReceieveThread.stopThread();
}
mJsonReceieveThread.startThread()
してしまえば、100msに一回TweetJsonReceiver::getInstance().update()
されるはずなので大丈夫です。また、end()
でしっかりスレッドをstopThread()
してあげましょう。
##8. 取得データを描画する何かを書く
これはもうなんでも大丈夫です。
わかりやすく参考までにofApp.h
とofApp.cpp
に書いてみようと思います。今回は簡単にこんな感じのものを作ります。リアルタイムで #NowPlaying とつぶやいた人の情報を表示し続けるものです。
今回の場合以下のように、ofApp.h
とofApp.cpp
に描画関係の命令を書いていきます。(普通ならクラスとかに切り分けたほうがいいですが、今回は簡単にするため直書きです)
#include "ofMain.h"
#include "TwitterStreamingManager.hpp"
#include "FontManager.hpp"
class ofApp : public ofBaseApp{
public:
void setup();
void update();
void draw();
void exit();
void keyPressed(int key);
void keyReleased(int key);
void mouseMoved(int x, int y );
void mouseDragged(int x, int y, int button);
void mousePressed(int x, int y, int button);
void mouseReleased(int x, int y, int button);
void mouseEntered(int x, int y);
void mouseExited(int x, int y);
void windowResized(int w, int h);
void dragEvent(ofDragInfo dragInfo);
void gotMessage(ofMessage msg);
TwitterStreamingManager twitterStreamingManager;
TwitterStreamingManagerのインスタンスを一つ用意しておきました。またexit()
命令を追加しました。
#include "ofApp.h"
//--------------------------------------------------------------
void ofApp::setup(){
twitterStreamingManager.setup();
FontManager::init();
}
//--------------------------------------------------------------
void ofApp::update(){
ofSetWindowTitle("FPS: " + ofToString(ofGetFrameRate()) +"/ num of element: "+ofToString(TweetJsonReceiver::data_num));
}
//--------------------------------------------------------------
void ofApp::draw(){
ofBackground(0);
ofPushMatrix();
ofTranslate(ofGetWidth()/2-300, ofGetHeight()/2);
if(TweetJsonReceiver::tweetData.size()>0){
TweetJsonReceiver::tweetData.back().image.draw(-100,-100,200,200);
FontManager::drawSmallString("@"+ofToString(TweetJsonReceiver::tweetData.back().twitter_id),150,-60);
FontManager::drawSmallWString(TweetJsonReceiver::tweetData.back().user_name,150,-40);
FontManager::drawSmallString("followers_count: "+ofToString(TweetJsonReceiver::tweetData.back().followers_count),150,-20);
FontManager::drawSmallString("friends_count: "+ofToString(TweetJsonReceiver::tweetData.back().friends_count),150,0);
FontManager::drawSmallWString(TweetJsonReceiver::tweetData.back().tweet_text,150,20);
}
ofPopMatrix();
}
//--------------------------------------------------------------
void ofApp::exit() {
twitterStreamingManager.exit();
}
//~~以降特に追記しないので省略~~
ここでは、特に難しいことはしていません、画面の中央付近にtweetした人のいろんな情報を表示しているだけです。
この描画部分を頑張ってたくさん書くと、最初の例ようなリッチな画面ができます。
# 最後に
最後に今回のサンプルはgithubにあげてますのでそちらも参考にしてみてください。
今回Twitterに特化して扱ったように書きましたが、ofxJSONは便利で、例えばFaceBook Graph API使って取得した何かとか、とにかくJSONであれば、この記事の__ 実装__のところを読めば汎用的だと思うので、大体できると思うので試してみてください。
また、いろいろ修正してくださる方いらっしゃいましたら修正リクエストお願いします🙇