PythonとWebRTCで画処理のウェブアプリを作る
ウェブアプリって気軽に使えて便利ですよね(・ω・)
動機
夏コミで「かみしば」という同人誌をボイロが呼んでくれるアプリ(Android)をリリースしました。
つづいでiOSへの移植を…なんて思ったのですが、開発環境が貧弱すぎるせいなのか必須ライブラリが使えなくて…ってことがありました。
それで「どうしようかなー」と考えていると、Android内でも機種による差異によって映らないという障害報告がありました。
カメラやコンパスなどのハードセンサーを使うアプリの難しさを改めて感じさせられます。だからこそ独自性が得られやすいのでしょうけど。
どの道、iOSでは完成の兆しが全く見えないのでもういっそのことサーバーで処理しちゃおう!ってことで所謂クラウド版の開発を始めました。
アプローチ
スマートフォンのブラウザ上でビデオカメラを起動
WebRTCのAPIを利用。と言ってもgetUserMediaを呼ぶと、カメラの視界をビデオとして再生してくれるという程度のものでプログラマーの負担は殆どありません。ここまでシンプルになるまで抽象化してくれてるので、よっぽどはぐれモノの端末でない限り互換性が期待できます。iOSももちろんiOS11から対応しているそうです。(が、私のiPodtouchG5はiOS9(´;ω;`))
今回は以前からずっと気になっていたTypeScriptを使ってみました。
撮影したフレームをそっくりそのままサーバーに送信
画像をPOSTで送って、インデックスのjsonが返ってくるREST風 APIです。Advanced REST clientというChrome拡張を見つけて使ってみたのですが、かなり便利、オヌヌメです!
画像処理と言えば、Python。せっかくなのでPythonのWebフレームワークDjangoを勉強がてら使ってみました。
結果(何ページ目の画像か)をレスポンスとして送信
レスポンス内のインデックス情報を元にmp3をSeek&Play&Pauseです。
サーバーサイド開発
Djangoはそれ自体がPythonで書かれたPython用のWEBフレームワークです。PythonのWEBフレームワークとしては最もメジャーなもののひとつらしいですが、Django自体はNodejsやRuby on railsほど人気でもなさそうな印象を受けました。(Pythonは人気なのにね)
MVCで言うところのControlっぽいところがViewだったりViewっぽいところがTemplateだったり、言葉で惑わしにかかってくる部分はありましたが、任意の処理をさせるまでには結構素早く達っせました。公式のチュートリアルが結構わかりやすいです。
はじめての Django アプリ作成、その 1 | Django documentation | Django
ざっくりとした流れは
# djangoのインストール > pip install django # django-adminコマンドはdjangoと一緒に入ってくる > django-admin startproject project_name # プロジェクト内にいくつか小さいアプリをポコポコ作っていくスタイル > django-admin startapp app_name
ビュー(コントロール)を一つ作るごとに以下の2か所の編集が必要です。関数ベースにする方式とクラスベースにする方式があります。クラスが推奨されていますが、APIとかならテンプレートもいらないし、関数ベースの方が読みやすいかもしれません。
# app_name/views.py def Index(request: HttpRequest) -> HttpResponse: # GETパラメータ(ディレクトリ表現を推奨)は引数でもとれる post_data = request.POST['key'] form_file = request.FILE['key'] # requestを元にHTMLやjsonを作る return HttpRequest(html) # return JsonRsponce({hoge: ’huga’, piyo: 5}) # Template(拡張htmlみたいなの)がクライアントに返る class IndexView(TemplateView): template_name = 'narrator/app.html'
# app_name/urls.py urlpatterns = [ path('/index', views.index, name='index'), path('/index-view', views.Index.as_views(),name='index_view'), ]
このビュー関数の中で自由自在にOpenCVを呼べるのがいいですよね!
# 送信された画像をOpenCV用の形式(ndarray)に変換 img = np.array(Image.open(request.FILES['image']))
ただやっぱり型がゆるゆるなので疲れます。
クライアントサイド開発
TypeScript環境
Nodejsは使いませんが、パッケージマネージャとしてNodejsのもの、npmを使うのが一般的みたいです。VisualStudioInstallerの方がいいかなとも思ったのですが、脱Windowsを迫られている身なのでクロプラなのを使っていこうとしています。
# TypeScriptのコンパイラtscだけではブラウザが # 名前解決できるところまでやってくれないので # webpackとの合わせ技です(なんでだよ💢) > npm install typescript ts-loader webpack webpack-cli webpack-dev-server --save-dev
TypeScriptを書く
class PgdetRetval{ public id:string; public index: number; private static prevPackage: string = ""; private static prevIndex: number = -1; public constructor(jsonObj: any){ if("id" in jsonObj && "index" in jsonObj && "score" in jsonObj && "cross" in jsonObj && "timing" in jsonObj){ this.id = jsonObj.id; this.index = jsonObj.index; }else{ throw `Server internal error! ${jsonObj.toString()}`; } } public past(pack:string, id:HTMLSpanElement, index:HTMLSpanElement, score:HTMLSpanElement, cross:HTMLSpanElement, image:HTMLImageElement, audio:HTMLAudioElement){ id.innerHTML = this.id; index.innerHTML = this.index.toString(); let imageUrl:string; if(this.cross >= 5){ imageUrl = image.src = `/static/narrator/loading.gif`; }else{ imageUrl = `/static/narrator/packages/${pack}/${("000" + this.index).slice(-3)}.jpg`; } if(image.src != imageUrl) image.src = imageUrl; if(this.cross >= 5){ // 404を表示する }else{ PgdetRetval.prevPackage = pack; PgdetRetval.prevIndex = this.index; } } }
う~ん、C#!流石MS製ですね。中核のアーキテクトもC#と同じらしいです。いいですね!
ただし、いくらC#の皮をかぶっても所詮はJavaScriptです。次のようにしたとき、コールバックの方はthisがコールバック呼び出し元になってしまうのでメンバを解決できません。
class Takahashi{ private id: number; public constructor(id:number){ this.id = id; # getIdが評価される時にはthisが書き変わっている! setInterval(getId, 100); } public getId(): number{ return this.id; } }
JavaScriptのthisはそのコンテクストでの関数なのでどうしようもなかったのかもしれませんね。ただ圧倒的にJavaScriptより書きやすいのは間違いないです。(型定義ファイルとか、準備が面倒くさいけど;要勉強)
WebRTC
驚くほどなんてことないです。video.srcObject = stream;
した瞬間には全てが終わっていて後はvideoに対する色々をするだけです。ただ一つだけ残念なのは、毎フレームのコールバックが無く、setIntervalで定期的に見るしかないという点ですかね。
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || window.navigator.mozGetUserMedia; // ここでBindせずに this.frameCallBack を渡してしまうとthisがイベントの発火元になってしまう let handle:CallPgdet = this; navigator.getUserMedia({video: {width: 360, height: 480, frameRate: 2}, audio: false}, this.frameCallBack.bind(handle), console.log ); ... private frameCallBack(stream: MediaStream){ this.video.srcObject = stream; this.video.play(); this.task = setInterval(()=>{ console.log(this.frameCount); // カメラが慣れるまでスキップ if(this.frameCount == 5) console.log("Camera maybe became stabled."); if(this.frameCount++ >= 5 && this.isPlay){ this.callPgdet(this.video, this.canvas); } }, 500); }
コンパイル
> npm run build
本番サーバーに展開
環境
Pythonの問題
PythonコマンドがPython2.xにaliasされていたので時々ハマりました…また、pipがyumで入りません。泣く泣くwgetで入れました。
> wget https://bootstrap.pypa.io/get-pip.py > sudo python3.6 get-pip.py
環境変数
DJANGO_SETTINGS_MODULE と PYTHONPATH の設定が必要です。Windowsでは普通に名前解決してくれたんですが…そもそもこんなの二つ以上プロジェクトがあったら詰むのでは?
export DJANGO_SETTINGS_MODULE=project_name.settings export PYTHONPATH=project_name
HTTPサーバーとのローカル通信
nginx<->uwsgi<->django が推奨されているが、uwsgiの設定が面倒くさすぎてdjangoの中に入っているwsgiでNポートで待ち受け、nginxからリバースプロキシ(https->http)してます。
> python manage.py runserver 0.0.0.0:5050
# nginx.conf server { listen 443; server_name kamishiba2.wallstudio.work; client_max_body_size 100M; # 画像なので大きめに # Let's Encrypt ssl_certificate xxxxxxxxxxxxxxxxxxxxxxxx; ssl_certificate_key xxxxxxxxxxxxxxxxxxxx; location /static { # 本当は開発用のstaticディレクトから別の # ところに実行時にエクスポするのが推奨 # らしいけど面倒くさい alias {開発中に使ってたstaticディレクトリ}; #root /static; } location / { proxy_pass http://localhost:5050; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header Connection keep-alive; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_cache_bypass $http_upgrade; #auth_basic "Makimaki"; #auth_basic_user_file /etc/nginx/.htpasswd; } }
完成品
「かみしば2」が完成しました!!
— 金西ち18「かみしば」同人誌読上げアプリ (@kamishiba_ws) 2018年10月19日
こんどはWEBブラウザで手軽に遊べるようになりました!
ブラウザさえ対応していれば、機種による不具合も格段に少なくなります。
夏コミからずっと待ってくださっていた方、本当にお待たせしました!
iOSでの動作確認が取れ次第、正式にリリースします。#かみしば pic.twitter.com/ddynQ0R5AR
精度も良好ですね!かなり思った通りに動いてくれています。
全然関係ないけど、マキマキのおなかえっちぃ!
自分で言うのもなんだけど!
はぁマキマキ可愛いなぁ (*´Д`)
タカハシ
ゲームじゃないものにテトリスくっ付けたらそれはゲームなので、ゲームジャムに出せる???
— うpハシ【C95?】🐚WallStudio (@yukawallstudio) 2018年10月19日
#ボイロゲームジャム