Eagle Eye Networks APIで簡単ビデオ連携 その4 オブジェクトストレージへの動画の保存

#Eagle Eye Networks APIで簡単ビデオ連携 その4 オブジェクトストレージへの動画の保存

BOSCH製カメラによる人数測定の巻は、都合により次回以降にしたいと思います。期待させて申し訳ありません。
[前々回の動画取得](http://qiita.com/c-nova/items/16cff2e933be9ff9adbe)では、取得した動画はそのまま再生して、ハイ終わりという感じだったと思います。ということは、これって実はダウンロードでは無いのでは・・・、と内心思っておりましたので、ここではクラウドらしくクラウド（Bluemix）上のオブジェクトストレージに動画ファイルを置く、ということをやってみたいと思います。

## オブジェクトストレージとは
Bluemixのオブジェクトストレージは、簡単に言ってしまえばIBMの別クラウド~~である~~だった**「SoftLayer」上の「オブジェクトストレージ」サービス**です。これは[ここ](https://www.change-makers.jp/docs/softlayer-config-guide/10354)に詳しい説明があるとおりですが、ほぼそのまま「OpenStack Swift」です。OpenStack Swiftの説明は[ここ](http://docs.openstack.org/developer/swift/overview_architecture.html )にありますが、特徴をまとめると、

- オブジェクトストレージは大量データの保管に好適
- オブジェクトストレージでは、無停止でストレージ容量を増設可能
- 保存データは、内部で3重のコピーを保持
- 一貫性検査のタスクが常時実行
- (物理障害に対するという意味で)データのバックアップは不要
- 一般のディスクのようなアクセスは不可。HTTP及びHTTPSを使用したデータの入出力のみ可能

という感じです。EENに限った話ではありませんが、どうしても大容量になりがちな動画ファイルの保存に最適なソリューションと言えると思います。特にこれから4K, 8Kで30, 60fpsと3～16倍という単位で容量が増加することが考えられるため、**クラウドで動画を扱うには必須のソリューション**と言えます。またSoftLayer, BluemixのオブジェクトストレージはCDNとの連携が行えるため、コンテンツ配信の面から見ても非常に優位が高いと思います。

##環境、事前確認
大元の環境条件や事前確認項目は[第1回](http://qiita.com/c-nova/items/6da5482019b9ca7b7f62#%E7%92%B0%E5%A2%83)と同様ですので、<-のリンク先を参照して下さい。

今回は上記環境とは別にオブジェクトストレージのサービスが必要になります。バインドはしてもしなくても問題なく、また無料プランでお試し頂いても問題ありません。この場合アップロード上限が5GBに制限されることに注意して下さい。

また下記の例を実際に行う場合には、コンテナとして「**EENStr001**」という名称で事前に作成しておいてください。

###nodered.orgにある「node-red-contrib-objectstore」について

Node-REDで使えるノードやフローなど、有志によってContributesされている様々な便利コードが[nodered.org](node-red-contrib-objectstore)に置かれています。実はこの中にオブジェクトストレージを簡単に扱うための「[node-red-contrib-objectstore](https://flows.nodered.org/node/node-red-contrib-objectstore)」というノードがあります。2016年10月頃に作成されたようで、私がこのフローを作成した時点では無かったので「お、いいね」と思って使おうと思って説明を読むと、「**This node helps to deliver images (up to 5MB) as a payload via the REST API Interface of Swift.**」と記載されていました。ということは、動画だと数十M、場合によってはGB単位になることもあるので、今回は使用を見合わせました。皆様もご注意下さい。

##Node-REDでインポート

下記のJSON DocをデプロイしたNode-RED上にインポートしてみてください。

- EEN認証、認可用ノード

```
[{"id":"48806d55.aa5ce4","type":"inject","z":"ba43e00b.410be","name":"","topic":"","payload":"","payloadType":"str","repeat":"3600","crontab":"","once":true,"x":110,"y":120,"wires":[["d8c2c88f.77af08"]]},{"id":"d8c2c88f.77af08","type":"function","z":"ba43e00b.410be","name":"Get Auth Head","func":"var user = \"<User ID>\";\nvar password = \"<Password>\";\nmsg.payload = 'username=' + user + '&password=' + password;\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":120,"wires":[["12ce9b05.337e65"]]},{"id":"12ce9b05.337e65","type":"http request","z":"ba43e00b.410be","name":"","method":"POST","ret":"obj","url":"https://login.eagleeyenetworks.com/g/aaa/authenticate","x":330,"y":180,"wires":[["3b5b7804.a42e28"]]},{"id":"3b5b7804.a42e28","type":"http request","z":"ba43e00b.410be","name":"","method":"POST","ret":"obj","url":"https://login.eagleeyenetworks.com/g/aaa/authorize","x":330,"y":240,"wires":[["f13c946.c2fa068"]]},{"id":"f13c946.c2fa068","type":"contrib-json","z":"ba43e00b.410be","engine":"JSONPath","command":"jq","expr":"$.headers.set-cookie[1]","complete":"complete","prop":"payload","name":"Put Cookie","x":570,"y":120,"wires":[["537bcaff.f5d6a4"]]},{"id":"537bcaff.f5d6a4","type":"function","z":"ba43e00b.410be","name":"Form Auth Key","func":"var cookies = msg.payload[0].split(\"; \");\nfor (var i = 0; i < cookies.length; i++) {\n\tvar str = cookies[i].split(\"=\");\n\tif (str[0] === \"auth_key\") {\n\t\tglobal.set(\"auth_key\", unescape(str[1]));\n\t\tbreak;\n\t}\n}\nglobal.set(\"een_cookie\", msg.headers['set-cookie']);\nmsg.payload = msg.statusCode;\nreturn msg;","outputs":1,"noerr":0,"x":580,"y":180,"wires":[["d77b9c74.997fd"]]},{"id":"d77b9c74.997fd","type":"http response","z":"ba43e00b.410be","name":"","x":550,"y":240,"wires":[]}]
```

- オブジェクトストレージ認証用ノード

```
[{"id":"1c16a99a.698436","type":"function","z":"ba43e00b.410be","name":"Gen Auth Token","func":"var vcapsvc = context.global.VCAP_SERVICES;\n\nvar objcred = [];\n\nobjcred.projectId = \"<Project ID>\";\nobjcred.userId = \"<User ID>\";\nobjcred.password = \"<Password>\";\n\nvar authbody = {\n    \"auth\": {\n        \"identity\": {\n            \"methods\": [\n                \"password\"\n            ],\n            \"password\": {\n                \"user\": {\n                    \"id\": objcred.userId,\n                    \"password\": objcred.password\n                }\n            }\n        },\n        \"scope\": {\n            \"project\": {\n                \"id\": objcred.projectId\n            }\n        }\n    }\n};\n\nmsg.payload = JSON.stringify(authbody);\n\nreturn msg;","outputs":"1","noerr":0,"x":340,"y":680,"wires":[["23e6a7cd.41ac58"]]},{"id":"23e6a7cd.41ac58","type":"http request","z":"ba43e00b.410be","name":"","method":"POST","ret":"obj","url":"https://identity.open.softlayer.com/v3/auth/tokens","x":330,"y":740,"wires":[["278b943a.d8b4bc"]]},{"id":"278b943a.d8b4bc","type":"contrib-json","z":"ba43e00b.410be","engine":"JSONPath","command":"jq","expr":"$.token.catalog[?(@.type === \"object-store\")].endpoints[?(@.region_id === \"dallas\" && @.interface === \"public\")].url","complete":"property","prop":"payload","name":"URL JSON Parse","x":600,"y":680,"wires":[["a40563b.6916ca"]]},{"id":"d6b98c59.cd2fa","type":"comment","z":"ba43e00b.410be","name":"Generate Object Storage Auth Key loop","info":"","x":220,"y":620,"wires":[]},{"id":"dc0820.a855c7e","type":"inject","z":"ba43e00b.410be","name":"","topic":"","payload":"","payloadType":"str","repeat":"3600","crontab":"","once":true,"x":110,"y":680,"wires":[["1c16a99a.698436"]]},{"id":"a40563b.6916ca","type":"function","z":"ba43e00b.410be","name":"Add_Auth_Header","func":"global.set(\"obj_token\", msg.headers['x-subject-token']);\nglobal.set(\"epurl\", msg.payload[0]);\n\nmsg.payload = global.get(\"obj_token\") + \" \" + global.get(\"epurl\");\n\nreturn msg;","outputs":1,"noerr":0,"x":592,"y":740,"wires":[["b32a942f.aa2c08"]]},{"id":"b32a942f.aa2c08","type":"debug","z":"ba43e00b.410be","name":"","active":false,"console":"false","complete":"false","x":810,"y":740,"wires":[]}]
```

- 動画ダウンロード用ノード

```
[{"id":"b0ca2077.42a85","type":"subflow","name":"P_ObjStor","info":"","in":[{"x":90.00001525878906,"y":80.00000953674316,"wires":[{"id":"4ecd6847.4c0198"}]}],"out":[{"x":960,"y":80,"wires":[{"id":"f5af6563.c06d68","port":0}]}]},{"id":"b0a3895.39e6f78","type":"http request","z":"b0ca2077.42a85","name":"Put the video to ObjStr","method":"PUT","ret":"txt","url":"{{{epurl}}}/EENStr001/{{flvfilename}}","tls":"","x":520,"y":80,"wires":[["f5af6563.c06d68"]]},{"id":"4ecd6847.4c0198","type":"function","z":"b0ca2077.42a85","name":"Add_ObjStor_Header","func":"msg.headers['X-Auth-Token'] = global.get(\"obj_token\");\nmsg.headers['X-Object-Meta-camName'] = msg.camName;\nmsg.headers['X-Object-Meta-camID'] = msg.camID;\nmsg.headers['X-Object-Meta-stets'] = msg.vidStart;\nmsg.headers['X-Object-Meta-etets'] = msg.vidEnd;\n\nmsg.flvfilename = msg.camID + \"_\" + msg.vidStart + \"_\" + msg.vidEnd + \".flv\";\n\nmsg.epurl = global.get(\"epurl\");\nreturn msg;","outputs":1,"noerr":0,"x":270.00001525878906,"y":80.00000953674316,"wires":[["b0a3895.39e6f78"]]},{"id":"f5af6563.c06d68","type":"template","z":"b0ca2077.42a85","name":"Status Response","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Status Code is returned : {{statusCode}}","x":790,"y":80,"wires":[[]]},{"id":"be5d194f.8245c8","type":"subflow","name":"Q_ObjStor","info":"","in":[{"x":160,"y":260,"wires":[{"id":"7379857d.1c345c"}]}],"out":[{"x":1000,"y":260,"wires":[{"id":"8aa1db99.119098","port":0}]}]},{"id":"7379857d.1c345c","type":"function","z":"be5d194f.8245c8","name":"Add ObjStor Header","func":"msg.headers = [];\nmsg.headers['X-Auth-Token'] = global.get(\"obj_token\");\nmsg.headers['X-Context'] = \"search\";\nmsg.epurl = global.get(\"epurl\");\n\nreturn msg;","outputs":1,"noerr":0,"x":340,"y":260,"wires":[["253c99f3.bcb436"]]},{"id":"253c99f3.bcb436","type":"http request","z":"be5d194f.8245c8","name":"","method":"GET","ret":"obj","url":"{{{epurl}}}/EENStr001?format=json","x":590,"y":260,"wires":[["8aa1db99.119098"]]},{"id":"8aa1db99.119098","type":"function","z":"be5d194f.8245c8","name":"Search the video","func":"var nfyLength = msg.payload.length;\n\nfor (itr = 0; itr < nfyLength; itr++){\n    if ( msg.payload[itr].meta.camid ) {\n        if ( msg.camName == msg.payload[itr].meta.camname && msg.vidStart == msg.payload[itr].meta.stets && msg.vidEnd == msg.payload[itr].meta.etets) {\n            msg.flvname = msg.payload[itr].name;\n        }\n    }\n}\n\nmsg.payload = \"\";\nmsg.payload = msg.flvname;\n\nreturn msg;","outputs":1,"noerr":0,"x":830,"y":260,"wires":[[]]},{"id":"8be8f396.9b7ee","type":"comment","z":"8f46ef33.15d9a","name":"Get EEN Video (Range)","info":"### Usage\nhttp://xxxxxx.mybluemix.net/eengetvideo?type=<queryType>&camName=<Camera Name>&st=<Start Time>&et=<End Time>\n\n- type\n    - playvideo\n    - getvideo\n    - putvideo\n    - listvideo\n\n- camName\n    - Camera Display Name\n\n- st\n  Start time of request type. It must be 'Date' and 'Time' format.\n  ex) 2016/02/16 10:00:00\n  note) Don't enter as EEN Timestump format\n\n- et\n  Same above.\n  If omit 'et' option, list video fuction is retrieve a video that nearest one from specified 'st'.\n\n### Example\nhttp://xxxxx.mybluemix.net/eengetvideo?type=listvideo&camName=EN-CDUM-002a&st=2016/02/16%2010:00:00&et=2016/02/16%2011:00:00","x":170,"y":80,"wires":[]},{"id":"14dbc351.82116d","type":"function","z":"8f46ef33.15d9a","name":"Get Auth & Set EENTS","func":"msg.orgheaders=msg.req.headers;\n\nif (msg.payload.auth_key) {\n    msg.auth_key = msg.payload.auth_key;\n} else {\n    msg.auth_key = global.get(\"auth_key\");\n}\n\nif (msg.payload.type) {\n    msg.type = msg.payload.type;\n} else {\n    msg.type = \"playvideo\";\n}\nif (msg.payload.camName) {\n    msg.camName = msg.payload.camName;\n} else {\n    msg.camName = \"all\";\n}\nif (msg.payload.st) {\n    var st_JST = msg.payload.st;\n} else {\n    var st_JST = \"2016/1/1 00:00:00\";\n}\nif (msg.payload.et) {\n    var et_JST = msg.payload.et;\n} else {\n    var et_JST = \"2016/1/31 23:59:59\";\n}\nif (msg.payload.test) {\n    msg.test = msg.payload.test;\n}\n\nvar st_JST_do = new Date(st_JST);\nmsg.st_JST = [st_JST_do.getFullYear(),\n                ( '0' + (st_JST_do.getMonth() + 1)).slice(-2),\n                ( '0' + st_JST_do.getDate() ).slice(-2),\n                ( '0' + st_JST_do.getHours() ).slice( -2 ),\n                ( '0' + st_JST_do.getMinutes() ).slice( -2 ),\n                ( '0' + st_JST_do.getSeconds() ).slice( -2 )\n                ].join(\"\");\n\nvar st_UTC_do = new Date(st_JST_do.getTime() - 32400000);\nmsg.st_UTC = [st_UTC_do.getFullYear(),\n                ( '0' + (st_UTC_do.getMonth() + 1)).slice(-2),\n                ( '0' + st_UTC_do.getDate() ).slice(-2),\n                ( '0' + st_UTC_do.getHours() ).slice( -2 ),\n                ( '0' + st_UTC_do.getMinutes() ).slice( -2 ),\n                ( '0' + st_UTC_do.getSeconds() ).slice( -2 )\n                ].join(\"\");\n\nvar et_JST_do = new Date(et_JST);\nmsg.et_JST = [et_JST_do.getFullYear(),\n                ( '0' + (et_JST_do.getMonth() + 1)).slice(-2),\n                ( '0' + et_JST_do.getDate() ).slice(-2),\n                ( '0' + et_JST_do.getHours() ).slice( -2 ),\n                ( '0' + et_JST_do.getMinutes() ).slice( -2 ),\n                ( '0' + et_JST_do.getSeconds() ).slice( -2 )\n                ].join(\"\");\n\nvar et_UTC_do = new Date(et_JST_do.getTime() - 32400000);\nmsg.et_UTC = [et_UTC_do.getFullYear(),\n                ( '0' + (et_UTC_do.getMonth() + 1)).slice(-2),\n                ( '0' + et_UTC_do.getDate() ).slice(-2),\n                ( '0' + et_UTC_do.getHours() ).slice( -2 ),\n                ( '0' + et_UTC_do.getMinutes() ).slice( -2 ),\n                ( '0' + et_UTC_do.getSeconds() ).slice( -2 )\n                ].join(\"\");\n\nif (msg.payload.et) {\n    msg.urltmp = \"https://login.eagleeyenetworks.com/asset/list/video?start_timestamp=\" + msg.st_UTC + \".000;end_timestamp=\" + msg.et_UTC + \".000;A=\" + msg.auth_key + \";options=coalesce\";\n} else {\n    msg.urltmp = \"https://login.eagleeyenetworks.com/asset/list/video?start_timestamp=\" + msg.st_UTC + \".000;count=1;A=\" + msg.auth_key + \";options=coalesce\";\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":390,"y":139,"wires":[["af451c94.ae188"]]},{"id":"d6ba669.513e998","type":"http in","z":"8f46ef33.15d9a","name":"GetVideoRange","url":"/eengetvideo","method":"get","swaggerDoc":"","x":140,"y":140,"wires":[["14dbc351.82116d","a20c873e.54a508"]]},{"id":"af451c94.ae188","type":"http request","z":"8f46ef33.15d9a","name":"Get Camera ID","method":"GET","ret":"obj","url":"https://login.eagleeyenetworks.com/g/device/list?A={{auth_key}}&t=camera&s=ATTD","tls":"","x":360,"y":200,"wires":[["c605696f.f533f8","d797abe1.d260e8"]]},{"id":"c605696f.f533f8","type":"function","z":"8f46ef33.15d9a","name":"Pull CamID & Set URL","func":"var camLength = msg.payload.length;\n\nfor (itr = 0; itr < camLength; itr++){\n    var camIdx = msg.payload[itr].indexOf(msg.camName);\n    if ( camIdx > 0 ) {\n        msg.camID = msg.payload[itr][camIdx - 1];\n    }\n}\n\nmsg.url = msg.urltmp + \";id=\" + msg.camID;\n\nreturn msg;","outputs":1,"noerr":0,"x":380,"y":260,"wires":[["11a23b6c.ac6455","d797abe1.d260e8"]]},{"id":"11a23b6c.ac6455","type":"http request","z":"8f46ef33.15d9a","name":"Get Video List","method":"GET","ret":"obj","url":"","tls":"","x":360,"y":320,"wires":[["9ea25155.56689"]]},{"id":"9ea25155.56689","type":"function","z":"8f46ef33.15d9a","name":"Pull Video Range","func":"delete msg.url;\n\nvar vidLength = msg.payload.length;\n\nif ( msg.payload[0] ) {\n    msg.vidStart = msg.payload[0].s;\n} else {\n    msg.vidStart = null;\n}\n\nif ( vidLength == 1 ) {\n    msg.vidEnd = msg.payload[0].e;\n}\n\nfor (itr = 0; itr < vidLength; itr++){\n    if ( itr == (vidLength - 1) ) {\n        msg.vidEnd = msg.payload[itr].e;\n    }\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":370,"y":380,"wires":[["f78ee296.411cb","3e96fd65.7e77e2"]]},{"id":"9b2135e6.1272a8","type":"switch","z":"8f46ef33.15d9a","name":"Switch Type","property":"type","propertyType":"msg","rules":[{"t":"eq","v":"listvideo","vt":"str"},{"t":"else"}],"checkall":"true","outputs":2,"x":650,"y":200,"wires":[["d4de5871.260428"],["cbe26ee9.31757"]]},{"id":"f6b32c92.a3d9e","type":"switch","z":"8f46ef33.15d9a","name":"ObjStr Exist","property":"payload","propertyType":"msg","rules":[{"t":"nnull"},{"t":"null"}],"checkall":"true","outputs":2,"x":650,"y":320,"wires":[["7b3e58d6.b54778"],["ecbf32b6.3870d"]]},{"id":"cbe26ee9.31757","type":"subflow:be5d194f.8245c8","z":"8f46ef33.15d9a","name":"","x":650,"y":260,"wires":[["f6b32c92.a3d9e"]]},{"id":"7b3e58d6.b54778","type":"switch","z":"8f46ef33.15d9a","name":"Switch Type","property":"type","propertyType":"msg","rules":[{"t":"eq","v":"putvideo","vt":"str"},{"t":"eq","v":"playvideo","vt":"str"},{"t":"eq","v":"getvideo","vt":"str"}],"checkall":"true","outputs":3,"x":890,"y":320,"wires":[["d633f332.73b0b"],["fde03821.8b5248"],["f3a8e7a8.448c78"]]},{"id":"f78ee296.411cb","type":"switch","z":"8f46ef33.15d9a","name":"No record","property":"vidStart","propertyType":"msg","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","outputs":2,"x":640,"y":140,"wires":[["ddaf5439.a3adc8"],["9b2135e6.1272a8"]]},{"id":"ecbf32b6.3870d","type":"change","z":"8f46ef33.15d9a","name":"Delete Headers","rules":[{"t":"delete","p":"headers","pt":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":660,"y":380,"wires":[["978c9649.08cce8"]]},{"id":"6333a38f.80234c","type":"comment","z":"8f46ef33.15d9a","name":"Fetch CamID, Vid List","info":"","x":160,"y":400,"wires":[]},{"id":"24616d0b.a2b882","type":"comment","z":"8f46ef33.15d9a","name":"Exist check on ObjStr","info":"","x":680,"y":80,"wires":[]},{"id":"3e96fd65.7e77e2","type":"debug","z":"8f46ef33.15d9a","name":"","active":true,"console":"false","complete":"false","x":350,"y":440,"wires":[]},{"id":"d4de5871.260428","type":"template","z":"8f46ef33.15d9a","name":"Video List","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n    <head>\n        <script src=\"https://code.jquery.com/jquery-2.2.0.min.js\"></script>\n    </head>\n    <body>\n        {{vidStart}}\n        {{vidEnd}}\n    </body>\n</html>\n","x":1180,"y":200,"wires":[["ac42b9c4.9f6a38"]]},{"id":"a3169c03.d0ab","type":"http request","z":"8f46ef33.15d9a","name":"Get video from ObjStr","method":"GET","ret":"bin","url":"{{{epurl}}}/EENStr001/{{payload}}","x":1460,"y":380,"wires":[["ac42b9c4.9f6a38"]]},{"id":"f3a8e7a8.448c78","type":"function","z":"8f46ef33.15d9a","name":"Set DL File Name","func":"msg.headers = [];\nmsg.headers['X-Auth-Token'] = global.get(\"obj_token\");\nmsg.epurl = global.get(\"epurl\");\n\nmsg.flvfilename = msg.camID + \"_\" + msg.st_JST + \"_\" + msg.et_JST + \".flv\";\nmsg.orgheaders['Content-Disposition'] = \"attachment; filename=\" + msg.flvfilename;\n\nreturn msg;","outputs":1,"noerr":0,"x":1210,"y":380,"wires":[["a3169c03.d0ab"]]},{"id":"d633f332.73b0b","type":"template","z":"8f46ef33.15d9a","name":"Already putted","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Video file {{camID}}_{{st_JST}}_{{et_JST}}.flv was already putted.","x":1200,"y":260,"wires":[["ac42b9c4.9f6a38"]]},{"id":"ddaf5439.a3adc8","type":"template","z":"8f46ef33.15d9a","name":"null response","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"Requested video range is not recorded anything.\n","x":1200,"y":140,"wires":[["ac42b9c4.9f6a38"]]},{"id":"a36d644f.5621f8","type":"comment","z":"8f46ef33.15d9a","name":"Exist and download","info":"","x":910,"y":260,"wires":[]},{"id":"17663ec6.7189b1","type":"comment","z":"8f46ef33.15d9a","name":"Exist and putted","info":"","x":1660,"y":260,"wires":[]},{"id":"22484b9b.a3e5b4","type":"http response","z":"8f46ef33.15d9a","name":"","x":1950,"y":440,"wires":[]},{"id":"ac42b9c4.9f6a38","type":"change","z":"8f46ef33.15d9a","name":"Restore Header","rules":[{"t":"delete","p":"headers","pt":"msg"},{"t":"set","p":"headers","pt":"msg","to":"orgheaders","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1760,"y":380,"wires":[["22484b9b.a3e5b4"]]},{"id":"9bc458d.9c2b6a8","type":"http request","z":"8f46ef33.15d9a","name":"Download the video","method":"GET","ret":"bin","url":"https://login.eagleeyenetworks.com/asset/play/video.flv?c={{camID}};t={{vidStart}};e={{vidEnd}};A={{auth_key}}","tls":"","x":910,"y":500,"wires":[["8239e8ab.65ccd8","de74c74f.6d93f8"]]},{"id":"8239e8ab.65ccd8","type":"switch","z":"8f46ef33.15d9a","name":"Switch Type","property":"type","propertyType":"msg","rules":[{"t":"eq","v":"getvideo","vt":"str"},{"t":"eq","v":"putvideo","vt":"str"},{"t":"eq","v":"playvideo","vt":"str"}],"checkall":"true","outputs":3,"x":890,"y":620,"wires":[["1f2fed37.a67513"],["9528f178.74e38"],["4ea31a9e.13b114"]]},{"id":"1f2fed37.a67513","type":"function","z":"8f46ef33.15d9a","name":"Set DL File Name","func":"msg.flvfilename = msg.camID + \"_\" + msg.st_JST + \".flv\";\nmsg.orgheaders['Content-Disposition'] = \"attachment; filename=\" + msg.flvfilename;\n\nreturn msg;","outputs":1,"noerr":0,"x":1210,"y":560,"wires":[["ac42b9c4.9f6a38"]]},{"id":"2774d3fc.5f769c","type":"comment","z":"8f46ef33.15d9a","name":"Get the video","info":"","x":650,"y":500,"wires":[]},{"id":"f4fdec90.8ae02","type":"comment","z":"8f46ef33.15d9a","name":"Put the video","info":"","x":1430,"y":620,"wires":[]},{"id":"a20c873e.54a508","type":"debug","z":"8f46ef33.15d9a","name":"","active":true,"console":"false","complete":"false","x":413.75,"y":83.75,"wires":[]},{"id":"978c9649.08cce8","type":"switch","z":"8f46ef33.15d9a","name":"Test ?","property":"test","propertyType":"msg","rules":[{"t":"eq","v":"1","vt":"num"},{"t":"else"}],"checkall":"true","outputs":2,"x":630,"y":440,"wires":[["16a09c5d.a8ab04"],["9bc458d.9c2b6a8"]]},{"id":"9f43c15d.ea6f7","type":"function","z":"8f46ef33.15d9a","name":"code to payload","func":"msg.payload = msg.statusCode;\n//msg.payload = msg.headers[3];\nreturn msg;","outputs":1,"noerr":0,"x":1200,"y":440,"wires":[["22484b9b.a3e5b4"]]},{"id":"16a09c5d.a8ab04","type":"http request","z":"8f46ef33.15d9a","name":"Download the video","method":"GET","ret":"txt","url":"https://login.eagleeyenetworks.com/asset/play/video.flv?c={{camID}};t={{vidStart}};e={{vidEnd}};A={{auth_key}}","tls":"","x":910,"y":440,"wires":[["9f43c15d.ea6f7","de74c74f.6d93f8","8b25b608.cf4988"]]},{"id":"de74c74f.6d93f8","type":"debug","z":"8f46ef33.15d9a","name":"","active":true,"console":"false","complete":"statusCode","x":1200,"y":500,"wires":[]},{"id":"9528f178.74e38","type":"subflow:b0ca2077.42a85","z":"8f46ef33.15d9a","name":"","x":1190,"y":620,"wires":[["ac42b9c4.9f6a38"]]},{"id":"5b4fd325.14de0c","type":"template","z":"8f46ef33.15d9a","name":"Gen Page Vid","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"<head>\n  <link href=\"http://vjs.zencdn.net/5.9.2/video-js.css\" rel=\"stylesheet\">\n\n  <!-- If you'd like to support IE8 -->\n  <script src=\"http://vjs.zencdn.net/ie8/1.1.2/videojs-ie8.min.js\"></script>\n</head>\n\n<body>\n    <h2>Start Time: {{vidStartHRJ}} <br> End Time: {{vidEndHRJ}}</h2>\n    <a href=\"http://xxxxx.mybluemix.net/eengetflv?camName={{camName}}&st={{nextVideoJ}}&auth_key={{auth_key}}\">次のビデオ</a><br><br>\n  <video id=\"my-video\" class=\"video-js vjs-default-skin\" controls preload=\"auto\" width=\"640\" height=\"480\"\n  data-setup=\"{}\">\n    <source src=\"{{{liveurl}}}\" type='video/x-flv'>\n    <p class=\"vjs-no-js\">\n      To view this video please enable JavaScript, and consider upgrading to a web browser that\n      <a href=\"http://videojs.com/html5-video-support/\" target=\"_blank\">supports HTML5 video</a>\n    </p>\n  </video>\n  <script src=\"http://vjs.zencdn.net/5.9.2/video.js\"></script>\n</body>","x":1440,"y":320,"wires":[["ac42b9c4.9f6a38"]]},{"id":"fde03821.8b5248","type":"function","z":"8f46ef33.15d9a","name":"Set Play File","func":"msg.headers = [];\nmsg.headers['X-Auth-Token'] = global.get(\"obj_token\");\nmsg.epurl = global.get(\"epurl\");\nmsg.liveurl = msg.epurl + \"/EENStr001/\" + msg.payload;\n\nvar vidStartHR = msg.vidStart.substr(0,4) + \"/\" + msg.vidStart.substr(4,2) + \"/\" + msg.vidStart.substr(6,2) + \" \" + msg.vidStart.substr(8,2) + \":\" + msg.vidStart.substr(10,2) + \":\" + msg.vidStart.substr(12,2);\nvar vidEndHR = msg.vidEnd.substr(0,4) + \"/\" + msg.vidEnd.substr(4,2) + \"/\" + msg.vidEnd.substr(6,2) + \" \" + msg.vidEnd.substr(8,2) + \":\" + msg.vidEnd.substr(10,2) + \":\" + msg.vidEnd.substr(12,2);\nvar st_UTC_do = new Date(vidStartHR);\nvar nv_UTC_do = new Date(vidEndHR);\nvar st_JST_do = new Date(st_UTC_do.getTime() + 32400000);\nvar nv_JST_do = new Date(nv_UTC_do.getTime() + 32400000);\nvar vidStartJ = [st_JST_do.getFullYear() + '/',\n            ( '0' + (st_JST_do.getMonth() + 1)).slice(-2) + '/',\n            ( '0' + st_JST_do.getDate() ).slice(-2) + ' ',\n            ( '0' + st_JST_do.getHours() ).slice( -2 ) + ':',\n            ( '0' + st_JST_do.getMinutes() ).slice( -2 ) + ':',\n            ( '0' + st_JST_do.getSeconds() ).slice( -2 )\n            ].join(\"\");\nmsg.nextVideoJ = [nv_JST_do.getFullYear() + '/',\n            ( '0' + (nv_JST_do.getMonth() + 1)).slice(-2) + '/',\n            ( '0' + nv_JST_do.getDate() ).slice(-2) + ' ',\n            ( '0' + nv_JST_do.getHours() ).slice( -2 ) + ':',\n            ( '0' + nv_JST_do.getMinutes() ).slice( -2 ) + ':',\n            ( '0' + (nv_JST_do.getSeconds() + 1)).slice( -2 )\n            ].join(\"\");\nmsg.vidStartHRJ = st_JST_do.getFullYear() + \"年\" + (st_JST_do.getMonth() + 1) + \"月\" + st_JST_do.getDate() + \"日 \" + st_JST_do.getHours() + \"時\" + st_JST_do.getMinutes() + \"分\" + st_JST_do.getSeconds() + \"秒\";\nmsg.vidEndHRJ = nv_JST_do.getFullYear() + \"年\" + (nv_JST_do.getMonth() + 1) + \"月\" + nv_JST_do.getDate() + \"日 \" + nv_JST_do.getHours() + \"時\" + nv_JST_do.getMinutes() + \"分\" + nv_JST_do.getSeconds() + \"秒\";\n\nreturn msg;","outputs":1,"noerr":0,"x":1190,"y":320,"wires":[["5b4fd325.14de0c"]]},{"id":"4ea31a9e.13b114","type":"function","z":"8f46ef33.15d9a","name":"Set Play File","func":"msg.liveurl = \"https://login.eagleeyenetworks.com/asset/play/video.flv?c=\" + msg.camID + \";t=\" + msg.vidStart + \";e=\" + msg.vidEnd + \";A=\" + msg.auth_key;\n\nvar vidStartHR = msg.vidStart.substr(0,4) + \"/\" + msg.vidStart.substr(4,2) + \"/\" + msg.vidStart.substr(6,2) + \" \" + msg.vidStart.substr(8,2) + \":\" + msg.vidStart.substr(10,2) + \":\" + msg.vidStart.substr(12,2);\nvar vidEndHR = msg.vidEnd.substr(0,4) + \"/\" + msg.vidEnd.substr(4,2) + \"/\" + msg.vidEnd.substr(6,2) + \" \" + msg.vidEnd.substr(8,2) + \":\" + msg.vidEnd.substr(10,2) + \":\" + msg.vidEnd.substr(12,2);\nvar st_UTC_do = new Date(vidStartHR);\nvar nv_UTC_do = new Date(vidEndHR);\nvar st_JST_do = new Date(st_UTC_do.getTime() + 32400000);\nvar nv_JST_do = new Date(nv_UTC_do.getTime() + 32400000);\nvar vidStartJ = [st_JST_do.getFullYear() + '/',\n            ( '0' + (st_JST_do.getMonth() + 1)).slice(-2) + '/',\n            ( '0' + st_JST_do.getDate() ).slice(-2) + ' ',\n            ( '0' + st_JST_do.getHours() ).slice( -2 ) + ':',\n            ( '0' + st_JST_do.getMinutes() ).slice( -2 ) + ':',\n            ( '0' + st_JST_do.getSeconds() ).slice( -2 )\n            ].join(\"\");\nmsg.nextVideoJ = [nv_JST_do.getFullYear() + '/',\n            ( '0' + (nv_JST_do.getMonth() + 1)).slice(-2) + '/',\n            ( '0' + nv_JST_do.getDate() ).slice(-2) + ' ',\n            ( '0' + nv_JST_do.getHours() ).slice( -2 ) + ':',\n            ( '0' + nv_JST_do.getMinutes() ).slice( -2 ) + ':',\n            ( '0' + (nv_JST_do.getSeconds() + 1)).slice( -2 )\n            ].join(\"\");\nmsg.vidStartHRJ = st_JST_do.getFullYear() + \"年\" + (st_JST_do.getMonth() + 1) + \"月\" + st_JST_do.getDate() + \"日 \" + st_JST_do.getHours() + \"時\" + st_JST_do.getMinutes() + \"分\" + st_JST_do.getSeconds() + \"秒\";\nmsg.vidEndHRJ = nv_JST_do.getFullYear() + \"年\" + (nv_JST_do.getMonth() + 1) + \"月\" + nv_JST_do.getDate() + \"日 \" + nv_JST_do.getHours() + \"時\" + nv_JST_do.getMinutes() + \"分\" + nv_JST_do.getSeconds() + \"秒\";\n\nreturn msg;","outputs":1,"noerr":0,"x":1190,"y":680,"wires":[["5d90401f.2ae83"]]},{"id":"5d90401f.2ae83","type":"template","z":"8f46ef33.15d9a","name":"Gen Page Vid","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"<head>\n  <link href=\"http://vjs.zencdn.net/5.9.2/video-js.css\" rel=\"stylesheet\">\n\n  <!-- If you'd like to support IE8 -->\n  <script src=\"http://vjs.zencdn.net/ie8/1.1.2/videojs-ie8.min.js\"></script>\n</head>\n\n<body>\n    <h2>Start Time: {{vidStartHRJ}} <br> End Time: {{vidEndHRJ}}</h2>\n    <a href=\"http://xxxxx.mybluemix.net/eengetvideo?type=playvideo&camName={{camName}}&st={{nextVideoJ}}&auth_key={{auth_key}}\">次のビデオ</a><br><br>\n  <video id=\"my-video\" class=\"video-js vjs-default-skin\" controls preload=\"auto\" width=\"640\" height=\"480\"\n  data-setup=\"{}\">\n    <source src=\"{{{liveurl}}}\" type='video/x-flv'>\n    <p class=\"vjs-no-js\">\n      To view this video please enable JavaScript, and consider upgrading to a web browser that\n      <a href=\"http://videojs.com/html5-video-support/\" target=\"_blank\">supports HTML5 video</a>\n    </p>\n  </video>\n  <script src=\"http://vjs.zencdn.net/5.9.2/video.js\"></script>\n</body>","x":1440,"y":680,"wires":[["ac42b9c4.9f6a38"]]},{"id":"d797abe1.d260e8","type":"debug","z":"8f46ef33.15d9a","name":"","active":true,"console":"false","complete":"url","x":640,"y":40,"wires":[]},{"id":"8b25b608.cf4988","type":"debug","z":"8f46ef33.15d9a","name":"","active":true,"console":"false","complete":"headers","x":890,"y":380,"wires":[]}]
```

インポートが完了すると、それぞれ下記のように表示されるはずです。
![screenshot-een-api.mybluemix.net-2017-01-25-17-03-26.png](https://qiita-image-store.s3.amazonaws.com/0/106468/73b49a99-a144-3dbe-be84-a97232f018b2.png)
![screenshot-een-api.mybluemix.net-2017-01-25-17-12-40.png](https://qiita-image-store.s3.amazonaws.com/0/106468/256fc671-6737-a852-f3fc-29835daa624d.png)
![screenshot-een-api.mybluemix.net-2017-01-25-17-11-39.png](https://qiita-image-store.s3.amazonaws.com/0/106468/0534de8f-34b1-393d-ed4e-6ef4693b0806.png)

今回はかなり大掛かり仕組みに見えますが、オブジェクトストレージにデータをアップロードする際に認証が必要なことや、アップロード/ダウンロード時の既存ファイルチェックなど、色々なロジックが必要になるためこのような大きさになってしまいます。本来はサブフローを増やしたりライブラリ化するなどの省力化を行なうべきですが、説明用に冗長にしている部分もありますのでご了承下さい。

##繋げてみよう
これで準備は完了です。
実際にURLにアクセスして確認してみましょう。
接続先のURLは、

```
http://Bluemixアプリ名+myblemix.net等/eengetvideo
```
です。

この仕組はカメラ名、タイプ（アップロードなのかダウンロードなのか）、開始時間、終了時間が必要ですので、

```
?type=タイプ&camName=カメラ名&st=開始時間&et=終了時間
```

というパラメータを上記のURLに付加します。最終的には、

```
http://Bluemixアプリ名+myblemix.net等/eengetvideo?type=タイプ&camName=カメラ名&st=開始時間&et=終了時間
```

となります。タイプは、

| タイプ名 | 実行内容 |
|:-------:|:--------|
|getvideo|動画のダウンロード|
|putvideo|動画のアップロード|
|listvideo|動画のリスト表示|
|playvideo|動画の再生※|

※playvideoは未実装

になります。


###動画のアップロード
***
動画のアップロードを行なうには、例えば、

https://yyyyyyyy.mybluemix.net/eengetvideo?type=putvideo&camName=xxxxxxxxxxx&st=2017/01/26 10:00:00&et=2017/01/26 10:05:00

こんな感じのURLを作成します。カメラIDと日時はご自身で管理している**カメラのESN名**、日時は「**YYYYMMDD （スペース）HH:MM:SS**」で指定します。
このURLでアクセスを行うと、以下のようなメッセージが表示されます。

> Status Code is returned : 201

上記メッセージはopenstackの[「Object Storage API」](http://developer.openstack.org/api-ref/object-storage/?expanded=create-or-replace-object-detail#create-or-replace-object)によると、

> The Created (201) response code indicates a successful write.

ということですので、無事にアップロードができたということになります。

ではここで、オブジェクトストレージにアップロードされているか確認しましょう。まずはBluemixのダッシュボードを開き、オブジェクトストレージのサービス名をクリックします。

![bm_dashboard.png](https://qiita-image-store.s3.amazonaws.com/0/106468/47b4bd7c-ad9c-b776-e909-b367c23b2a78.png)

サービスの詳細が開き、以下のような画面が表示されます。動画はコンテナ内に入りますのでコンテナ名（ここでは**EENStr001**）をクリックします。

![bm_objectstorage.png](https://qiita-image-store.s3.amazonaws.com/0/106468/d388e8a3-2ee2-fd81-d677-54d96b6cd911.png)

コンテナの内容が表示されます。複数アップロードされたファイルがある場合には、ファイル名にカメラのESNと開始、終了時間（共にUTC）が記載されていますので、それを頼りに探します。

![bm_container.png](https://qiita-image-store.s3.amazonaws.com/0/106468/cbce638c-041b-103c-414b-64f861a8c684.png)

試しにチェックボックスをonにし、「アクションの選択」メニューから「ファイルのダウンロード」をクリックします。

![bm_objdl.png](https://qiita-image-store.s3.amazonaws.com/0/106468/178165e8-7df7-c9cb-70f9-8ed078cb902b.png)

ファイルがダウンロードされましたら、試しに再生してみましょう。
VLCのようなFLVを再生可能なプレイヤーがインストールされていれば、以下のように再生されるはずです。

![bm_flv.png](https://qiita-image-store.s3.amazonaws.com/0/106468/48bf8eda-b2ad-5c6f-335f-dd69937cda63.png)

###動画のダウンロード
***
それではせっかくなので先程のURLからオブジェクトストレージ内のファイルをダウンロードしてましょう。
ダウンロードを行なうにはアップロードで使用した「putvideo」の代わりに「**getvideo**」を使用します。

https://yyyyyyyy.mybluemix.net/eengetvideo?type=getvideo&camName=xxxxxxxxxxx&st=2017/01/26 10:00:00&et=2017/01/26 10:05:00

ブラウザで実行すると、**唐突にファイルのダウンロードが開始されます**のでご注意下さい。
これだと「**直接Eagle Eyeから落としてるんじゃないの？？**」と穿った見方をされるので、ここでNode-REDの画面を見てましょう。

![bm_objget.png](https://qiita-image-store.s3.amazonaws.com/0/106468/0061f050-15ac-a360-aa53-7a4e309ae937.png)

ダウンロードと同時にNode-REDの画面を見ると、「*Get video from ObjStr*」というノードで「requesting」と青いボールが表示されるのがわかります。フローを見て頂ければわかるかと思いますが、このノードに到達するには途中のノード（*Q_ObjStor、ObjStr Exist*）でオブジェクトストレージ内に指定したファイルが存在する場合のみという条件のクリアが必須になります。その為、このノードでダウンロードが実行されているというタイミングで、ダウンロードされたファイルはオブジェクトストレージを経由していることがおわかりになるかと思います。


##Node-REDにおけるオブジェクトストレージの使い方

冒頭でも記載しましたとおり、Node-REDのContributesされているノードを使用すると扱えるファイルサイズが5MBに制限されるため、地道にコードを書いて対応する必要があります。

###オブジェクトストレージの認証
オブジェクトストレージの認証には以下のように、

- プロジェクトID
- ユーザーID
- パスワード

が必要になります。これを下記のコードのそれぞれの場所に記入します。

```
var vcapsvc = context.global.VCAP_SERVICES;

var objcred = [];

objcred.projectId = "<プロジェクトID>";
objcred.userId = "<ユーザーID>";
objcred.password = "<パスワード>";

var authbody = {
    "auth": {
        "identity": {
            "methods": [
                "password"
            ],
            "password": {
                "user": {
                    "id": objcred.userId,
                    "password": objcred.password
                }
            }
        },
        "scope": {
            "project": {
                "id": objcred.projectId
            }
        }
    }
};

msg.payload = JSON.stringify(authbody);

return msg;
```

これで認証のbodyができあがったので、これを

https://identity.open.softlayer.com/v3/auth/tokens

にPOSTします。
ここから出力されたJSON responseを、JSONPathのノードで、

```
$.token.catalog[?(@.type === "object-store")].endpoints[?(@.region_id === "dallas" && @.interface === "public")].url
```

とクエリすることで、アクセスするためのエンドポイントURLが返され、*x-subject-token*というトークンもResponse headersの中で返されます。ここではregionがdallasとなっていますが、ロンドンの方は*london*に変更して下さい。

```
global.set("obj_token", msg.headers['x-subject-token']);
global.set("epurl", msg.payload[0]);

msg.payload = global.get("obj_token") + " " + global.get("epurl");

return msg;
```

ここでやっとグローバル変数にエンドポイントのURLと、アクセス用のトークンが格納されます。これを使うことでファイルのアップロード、ダウンロード等が可能になります。もちろんコンテナの作成なども可能です。

以下はオブジェクトストレージの内容を取得するためのコードです。ここでは先程取得したヘッダの内容を実際のRequest headersに格納し、ダウンロードするファイルをバイナリバッファからAttachmentに指定することでダウンロードを行えるようにしています。

```
msg.headers = [];
msg.headers['X-Auth-Token'] = global.get("obj_token");
msg.epurl = global.get("epurl");

msg.flvfilename = msg.camID + "_" + msg.st_JST + "_" + msg.et_JST + ".flv";
msg.orgheaders['Content-Disposition'] = "attachment; filename=" + msg.flvfilename;

return msg;
```

次にこれを次のURLに対してGETを行います。

{{{epurl}}}/EENStr001/{{payload}}

ここで指定しているpayloadはサブフロー内の「*Search the video*」ノードで生成しています。

```
var nfyLength = msg.payload.length;

for (itr = 0; itr < nfyLength; itr++){
    if ( msg.payload[itr].meta.camid ) {
        if ( msg.camName == msg.payload[itr].meta.camname && msg.vidStart == msg.payload[itr].meta.stets && msg.vidEnd == msg.payload[itr].meta.etets) {
            msg.flvname = msg.payload[itr].name;
        }
    }
}

msg.payload = "";
msg.payload = msg.flvname;

return msg;
```

この流れで動画の取得を行なうことができます。
次にファイルのアップロード方法を見てましょう。ファイルのアップロードを行なう際にも同様の方法で行なうことができますが、ここではせっかくですので
オブジェクトストレージの機能の一つである「[メタデータ](http://developer.openstack.org/api-ref/object-storage/?expanded=create-or-replace-object-detail#objects)」の格納も行います。メタデータはあっても無くてもいいのですが、メタデータを使った全文検索が行えますので、大変便利です。メタデータの検索は「[MetadataSearchAPI](https://wiki.openstack.org/wiki/MetadataSearchAPI)」に詳細がありますのでご参照ください。

```
msg.headers['X-Auth-Token'] = global.get("obj_token");
msg.headers['X-Object-Meta-camName'] = msg.camName;
msg.headers['X-Object-Meta-camID'] = msg.camID;
msg.headers['X-Object-Meta-stets'] = msg.vidStart;
msg.headers['X-Object-Meta-etets'] = msg.vidEnd;

msg.flvfilename = msg.camID + "_" + msg.vidStart + "_" + msg.vidEnd + ".flv";

msg.epurl = global.get("epurl");
return msg;
```

上記にありますとおり、メタデータの格納は、

```
msg.headers['X-Object-Meta-camName'] = msg.camName;
```

のようにRequest Headerに**['X-Object-Meta-xxxxxx']**とすることでメタデータの名称を決定し、この内容を**「 = msg.camName」**と代入することでメタデータの内容を格納できます。

この内容で以下のURLにPUTします。

{{{epurl}}}/EENStr001/{{flvfilename}}

この実行結果はHTTP Responseで返ってきますので、これをpayloadとして返します。

##最後に

今回のようにオブジェクトストレージのようなNode-REDで用意されていない（または用意されているけれど制約があるなど）場合には、コードで表現し、実行する必要があります。これは一見面倒ですが、新しいサービスが発表されてもすぐに対応できることを考えると、できるようになったほうが後々楽だと思いました。

オブジェクトストレージはBluemixではデフォルトでCDNが有効化されているのか（間違ってる？？）、ストレージがダラスにある割にはダウンロード速度が非常に早く感じました。また無料プランの場合は5GBという制限はあるものの、某クラウドサービスと異なり流量での課金が無いことも大きな特徴です。これは開発、テストを行なう時には課金されたくない場合など、非常に有用と思いました。
オブジェクトストレージは総じてアップロード、ダウンロード共に優秀だと考えられます。

この次こそ都合が合えば、BOSCH製カメラによる人数カウントを書きたいと思います。

##ライセンス、著作権等

本スクリプト、ノード構成、エクスポート内容についてはMIT Licenseを適用します。
Eagle Eye Networksの商標およびロゴについてはEagle Eye Networks, Inc.に帰属します。
IBMおよびIBM Bluemix、Node-Redの商標およびロゴについてはInternational Business Machines Corporationに帰属します。
node.jsの商標およびロゴについてはNode.js contributorsに帰属します。