【MESHカスタムタグ】THETA Vと連動する


#できること
[MESH](http://meshprj.com/jp/)のSDKでThetaVと連携できるカスタムタグを作成しました。下記の3つの機能がMESH上のタグと組み合わせて利用する事が出来ます。
① 360 Live Photos：指定された時間だけ動画が撮影されます。
② 写真撮影：写真を撮影します。
③ 動画撮影：動画の開始・停止をコントロールできます。

360 Live Photos機能は、iPhoneのLive Photos機能が好きで良く使うので、あえて動画撮影機能と分けて実装しました。

![名称未設定-2.png](https://qiita-image-store.s3.amazonaws.com/0/159447/976f5be0-9c3e-db6b-04b5-1a3fa2320ba9.png)

#きっかけ
ThetaVで撮影をしていると本体ボタンを押している手や、スマホ越しにリモート撮影している自分が映ってしまうのがどうしても嫌だったので、色んなトリガーを設定できて、その場で調整が手軽にできるMESHと繋げてその問題を解決しようと思い、今回MESH用のThetaV連携タグを作成しました。

#実装方法
とりあえず使ってみたい方は、下記のJSON DATAをコピーしてimportしてご利用ください。
https://gloryroad0713.github.io/mesh.github.io/mesh_thetav.html

##360 Live Photos
![IMG_1856.jpg](https://qiita-image-store.s3.amazonaws.com/0/159447/e2042e83-b508-c970-dafe-c442d841cd99.jpeg)

360 Live Photos機能は、大まかに下記の流れで処理を実行しています。
① 撮影前状態の取得
② ビデオモード状態か否かの確認
③ もし写真モードだったら、ビデオモードに切り替える
④ 動画の撮影開始
⑤ 指定時間経過後、動画の撮影停止


```360LivePhotos_Execute.js
//Theta Endpoint URL の設定（デフォルトは192.168.1.1:80）
var endPointURL = "http://192.168.1.1:80"

wait().then((v) => {
    log("wait " + v + " millisecond");	
	closeCaptureSession(endPointURL) ;
	callbackSuccess( {
		resultType : "continue"
	} );
});

function sleep() {
	return new Promise(resolve => setTimeout(resolve,properties.millisecond));
}

async function wait() {
	checkState(endPointURL);
	//非同期処理が完了するまで待機
	await sleep();
	
	return properties.millisecond.toString();
}

function checkState(_endPointURL) {
	ajax( {
		url : _endPointURL + "/osc/state",
		type : "post",
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			checkCaptureMode(endPointURL);
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log(XMLHttpRequest + " | " + textStatus + " | " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	});
}

//Theta Vの撮影モードがvideoモードになっている確認
function checkCaptureMode(endPointURL) {
	var getOptionsJSONData = {
		name: "camera.getOptions",
		parameters: {
        optionNames: [
				"captureMode"
			]
        }
	}

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( getOptionsJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			var json_data = JSON.stringify(contents);
			var json_text = JSON.parse(json_data)
			log("checkCaptureMode = " + json_data);
			
			var mode = json_text["results"]["options"]["captureMode"];
			
			//写真モードだったら、動画モードに切り替える
			if(mode == "image"){
				setCaptureMode(endPointURL, "video");
			}
			else{
				startCaptureSession(endPointURL);
			}
				
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}


//Theta Vの撮影モードを変更する
function setCaptureMode(endPointURL, _mode) {
	var getOptionsJSONData = {
		name: "camera.setOptions",
		parameters: {
			options: {
				    captureMode: _mode
			}
		}
   	}	

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( getOptionsJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			log("動画モードに変更");
			startCaptureSession(endPointURL);
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}

//Theta の動画の撮影をスタートする
function startCaptureSession(endPointURL) {
	var takePictureJSONData = {
		name: "camera.startCapture",
	} 

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( takePictureJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
				log("動画撮影スタート");
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}

//動画の撮影を終了する
function closeCaptureSession(endPointURL) {
	var closeSessionJSONData = {
		name: "camera.stopCapture",
	} 

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( closeSessionJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			log("動画撮影ストップ");
			},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}

return {
	resultType : "pause"
};

```

##写真撮影
![IMG_1857.jpg](https://qiita-image-store.s3.amazonaws.com/0/159447/c48b73b3-d2a2-fbfb-4cb3-f84eff14b0ac.jpeg)

写真撮影機能は、大まかに下記の流れで処理を実行しています。
① 撮影前状態の取得
② 写真モード状態か否かの確認
③ もしビデオモードだったら、写真モードに切り替える
④ 写真の撮影開始

```Photo_Excute.js
//Theta Endpoint URL の設定（デフォルトは192.168.1.1:80）
var endPointURL = "http://192.168.1.1:80"

wait().then((v) => {
    log("wait " + v + " millisecond");	
	closeCaptureSession(endPointURL) ;
	callbackSuccess( {
		resultType : "continue"
	} );
});

function sleep() {
	return new Promise(resolve => setTimeout(resolve,properties.millisecond));
}

async function wait() {
	checkState(endPointURL);
	//非同期処理が完了するまで待機
	await sleep();
	
	return properties.millisecond.toString();
}

function checkState(_endPointURL) {
	ajax( {
		url : _endPointURL + "/osc/state",
		type : "post",
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			checkCaptureMode(endPointURL);
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log(XMLHttpRequest + " | " + textStatus + " | " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	});
}

//Theta Vの撮影モードがvideoモードになっている確認
function checkCaptureMode(endPointURL) {
	var getOptionsJSONData = {
		name: "camera.getOptions",
		parameters: {
        optionNames: [
				"captureMode"
			]
        }
	}

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( getOptionsJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			var json_data = JSON.stringify(contents);
			var json_text = JSON.parse(json_data)
			log("checkCaptureMode = " + json_data);
			
			var mode = json_text["results"]["options"]["captureMode"];
			
			//写真モードだったら、動画モードに切り替える
			if(mode == "image"){
				setCaptureMode(endPointURL, "video");
			}
			else{
				startCaptureSession(endPointURL);
			}
				
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}


//Theta Vの撮影モードを変更する
function setCaptureMode(endPointURL, _mode) {
	var getOptionsJSONData = {
		name: "camera.setOptions",
		parameters: {
			options: {
				    captureMode: _mode
			}
		}
   	}	

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( getOptionsJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			log("動画モードに変更");
			startCaptureSession(endPointURL);
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}

//Theta の動画の撮影をスタートする
function startCaptureSession(endPointURL) {
	var takePictureJSONData = {
		name: "camera.startCapture",
	} 

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( takePictureJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
				log("動画撮影スタート");
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}

//動画の撮影を終了する
function closeCaptureSession(endPointURL) {
	var closeSessionJSONData = {
		name: "camera.stopCapture",
	} 

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( closeSessionJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			log("動画撮影ストップ");
			},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}

return {
	resultType : "pause"
};
```

##動画撮影
![IMG_1858.jpg](https://qiita-image-store.s3.amazonaws.com/0/159447/b67f4b42-766a-49a2-620c-baa78f1c33e8.jpeg)

動画撮影機能は、大まかに下記の流れで処理を実行しています。
① 撮影前状態の取得
② ビデオモード状態か否かの確認
③ もし写真モードだったら、ビデオモードに切り替える
④ ビデオの撮影開始/停止

```Movie_Initialize.js
return {
    runtimeValues : {
        inputIndex : 0,
		outputIndex:0
    },
    resultType : "continue"
};
```

```Movie_Excute.js
//Theta Endpoint URL の設定（デフォルトは192.168.1.1:80）
var endPointURL = "http://192.168.1.1:80"

checkState(endPointURL);

function checkState(_endPointURL) {
	ajax( {
		url : _endPointURL + "/osc/state",
		type : "post",
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			checkCaptureMode(endPointURL);
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log(XMLHttpRequest + " | " + textStatus + " | " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	});
}

//Theta Vの撮影モードがvideoモードになっている確認
function checkCaptureMode(endPointURL) {
	var getOptionsJSONData = {
		name: "camera.getOptions",
		parameters: {
        optionNames: [
				"captureMode"
			]
        }
	}

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( getOptionsJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			var json_data = JSON.stringify(contents);
			var json_text = JSON.parse(json_data)
			log("checkCaptureMode = " + json_data);
			
			var mode = json_text["results"]["options"]["captureMode"];
			
			
			if(runtimeValues.inputIndex == 0){
				//写真モードだったら、動画モードに切り替える
				if(mode == "image"){
					setCaptureMode(endPointURL, "video");
				}
				else{
					startCaptureSession(endPointURL);
				}
			}
			else if(runtimeValues.inputIndex == 1){
				closeCaptureSession(endPointURL);			
			}
				
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}


//Theta Vの撮影モードを変更する
function setCaptureMode(endPointURL, _mode) {
	var getOptionsJSONData = {
		name: "camera.setOptions",
		parameters: {
			options: {
				    captureMode: _mode
			}
		}
   	}	

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( getOptionsJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			log("動画モードに変更");
			startCaptureSession(endPointURL);
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}

//Theta の動画の撮影をスタートする
function startCaptureSession(endPointURL) {
	var takePictureJSONData = {
		name: "camera.startCapture",
	} 

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( takePictureJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			log("動画撮影スタート");
			
			runtimeValues.outputIndex = 0;
			
			callbackSuccess({		
				resultType: "continue"
			});
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}

//動画の撮影を終了する
function closeCaptureSession(endPointURL) {
	var closeSessionJSONData = {
		name: "camera.stopCapture",
	} 

	ajax( {
		url : endPointURL + "/osc/commands/execute",
		type : "post",
		data : JSON.stringify( closeSessionJSONData),
		contentType: "application/json",
		dataType: "json",
		timeout : 5000,
		success : function (contents) {
			log("動画撮影ストップ");
			
			runtimeValues.outputIndex = 1;
				
			callbackSuccess({
					resultType: "continue"
			});
		},
		error : function (XMLHttpRequest, textStatus, errorThrown) {
			log("Error:" + textStatus + " " + errorThrown);		
			callbackSuccess({
				resultType: "stop"
			});
		}
	} );
}

return {
	resultType : "pause"
};
```

```Movie_Result.js
return {
    indexes : [ runtimeValues.outputIndex ],
    resultType : "continue"
};
```

#使ってみて
ボタンタグでリモート撮影や、マイクタグで盛り上がった時に自動撮影、明るさタグで手をかざして数秒後に撮影など、色々なトリガーを条件に撮影ができるのでより自然な360度写真/動画が撮影できた。ただ、トリガーが重なり命令が重なるとThetaVが上手くコントロールできず誤動作することがあったので、以前作った出力制御タグを活用しました。

- [【MESH カスタムタグ】出力制御(誤動作防止)をするタグの実装方法](https://qiita.com/glory/items/6709690862ba258a195f)

#参考
- [MESHをTHETA Sのリモートシャッターにしてみた](https://qiita.com/handa/items/6118a7e5e1bd58d1cc03)
- [RICOH THETA API：Getting Started](https://developers.theta360.com/ja/docs/v2.1/api_reference/getting_started.html)