Canvasから動画を生成するWebサービスを試作してみた。

##はじめに
3年ぶりの投稿です。
最近、[Codepen](https://codepen.io/)などを使ってCanvasで遊んでいるのですが、Canvasの画像・動きを動画として残せないかと考えるようになりました。
調べてみると、[CCapture.js](https://github.com/spite/ccapture.js/)など、Canvasから動画を生成するライブラリがあるようなので、[Heroku](https://jp.heroku.com/)を使って、簡単なCanvas動画生成サービスを作ってみました。
##作ったもの
成果物　　　：[CapCanvas](https://capcanvas.herokuapp.com/)
ソースコード：[Github](https://github.com/termat/capcanvas)

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Canvasから動画を生成するWebサービスを試作してみました。<a href="https://t.co/Nd265nEUGw">https://t.co/Nd265nEUGw</a> <a href="https://t.co/sweoAaaEZt">pic.twitter.com/sweoAaaEZt</a></p>&mdash; t_mat (@t_mat) <a href="https://twitter.com/t_mat/status/1101811720148811777?ref_src=twsrc%5Etfw">March 2, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

##使い方
htmlヘッダーに以下の行を追加します。なお、idはcanvasのid、fpsはフレームレート、timeは録画時間(ms)を指定します。
'C'キーを推すとCanvasのキャプチャが開始され、録画終了後、動画(webm)がダウンロードされます。
```<script type="text/javascript" src="https://capcanvas.herokuapp.com/capcanvas/id=app&fps=60&time=10000"></script>
```

##実装
■JavaScript
Jsは、[CCaputure](https://github.com/spite/ccapture.js/)のサンプルコードを参考に、以下のように実装しました。なお、<code>$URL</code>、<code>$ID</code>、<code>$FPS</code>、<code>$TIME</code>はサーバー側で文字列に置換します。

```CapCanvas.js
window.onload = function(){
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "$URL";
document.body.appendChild(script);
};
window.addEventListener('keydown', event => {
if(event.key=='c'||event.key=='C'){
let canvas=document.getElementById("$ID");
startCapture(canvas,$FPS,$TIME);
}
});
let startCapture=function(canvas,fps,time){
let cap=new CCapture({format:'webm',framerate: fps,verbose: true});
let render=function(){
requestAnimationFrame(render);
cap.capture( canvas );
};
requestAnimationFrame(render);
cap.start();
setTimeout(function(){
cap.stop();
cap.save();
},time);
};
```
■サーバー
サーバー側はリクエストパラメータを解析し、CapCanvas.jsを書き換えて送信します。
個人的にお気に入りの[Spark Framework](http://sparkjava.com/)で作成しています。

```Main.java
package net.termat.webapp.capcanvas;

import static spark.Spark.get;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import spark.ModelAndView;
import spark.Spark;
import spark.template.mustache.MustacheTemplateEngine;

public class Main {
	private static String code;

	public static void main(String[] args) {
		Spark.staticFileLocation("/public");
		code=getJs();
		Optional<String> optionalPort = Optional.ofNullable(System.getenv("PORT"));
        optionalPort.ifPresent(p -> {
            int port = Integer.parseInt(p);
            Spark.port(port);
        });

        get("/", (request, response) -> {
            Map<String, Object> model = new HashMap<>();
             return new ModelAndView(model, "index.mustache");
        }, new MustacheTemplateEngine());

        get("/capcanvas/:param", (request, response) -> {
        	try{
        		String url="https://capcanvas.herokuapp.com/js/CCapture.all.min.js";
        		Map<String,String> map=paramMap(request.params("param"));
        		String id=map.get("id");
        		String fps=map.get("fps");
        		String time=map.get("time");
                response.status(200);
                String ret=new String(code);
                ret=ret.replace("$URL", url);
                ret=ret.replace("$ID", id);
                ret=ret.replace("$FPS", fps);
                ret=ret.replace("$TIME", time);
                return ret;
        	}catch(Exception e){
        		e.printStackTrace();
	        	response.status(400);
	        	response.type("application/json");
	    		return "";
        	}
        });

	}

	private static String getJs(){
		try{
			URL url=Main.class.getResource("CapCanvas.js");
			BufferedReader br=new BufferedReader(new InputStreamReader(url.openStream()));
			StringBuffer buf=new StringBuffer();
			String line=null;
			while((line=br.readLine())!=null){
				buf.append(line);
			}
			return buf.toString();
		}catch(Exception e){
			return "";
		}
	}

	private static Map<String,String> paramMap(String param) throws Exception{
    	Map<String,String> ret=new HashMap<String,String>();
    	String[] p=param.split("&");
    	for(int i=0;i<p.length;i++){
    		String[] k=p[i].split("=");
    		if(k.length<2)continue;
    		ret.put(k[0], k[1]);
    	}
    	return ret;
    }
}
```
##デプロイ
[heroku](https://jp.heroku.com/)へは、heroku-maven-pluginを使用してデプロイしました。
Port番号の解決に気が付かず、実際に動作させるまで2～3時間かかってしまいました。