[環境構築]React+TypeScriptでWebAssembly005。C++,OpenCVでWasmビルドの開発環境を構築
<- React+TypeScriptでWebAssembly004
React+TypeScriptでWebAssembly006 ->
Abstract
やっと出来た!! Windows,OpenCV,C++,CMake,VSCode,WebAssermblyの開発環境。
①Windows,C++,OpenCVで開発→ ②Ubuntuでwasmビルド→ ③React+TypeScriptで開発&実行の順序で開発する。
とはいえ、[①Windows,C++,OpenCVで開発→ ②Ubuntuでwasmビルド]の部分は[環境構築]WindowsでC++開発->Ubuntuでwasmビルドの開発環境(OpenCV,C++,CMake)を構築してみた。を参照。
残りjs/wasmのReact+TypeScript組み込み環境を構築する。
結論
今回の成果物はココ↓
前提
- windowsでOpenCV,C++の開発環境構築済み。 [環境構築]React+TypeScriptでWebAssembly002。WindowsでOpenCV,C++,VSCode,CMake。
- WebAssemblyのビルド環境も構築済み。[環境構築]React+TypeScriptでWebAssembly003。UbuntuでWebAssemblyビルド。
- React+Typescriptの開発環境は構築済 [環境構築]WindowsにVSCode+React+TypeScriptの開発環境を構築してみた。
- ベースはこれ→cppwasm_template。
手順
不要ファイルの削除
- public/wasm/hello.wasm
- src/asm-cpp/hello.cpp
- src/asm-cpp/hello.js
今回は使わないので削除 - 下記コードも削除
- 3: import { helloif } from './asm-cpp/helloif';
①WindowsでC++開発。
Windows, OpenCV, Cmake, VSCode。
C++はコミット済のを使うので割愛。
②Wasmビルド
Ubuntuでwasmビルド -> 動作確認まで実施する。
②-1. Ubuntuでテンプレートのソースコード一式を取得
$ cd ~
$ git clone https://github.com/aaaa1597/cppwasm_template.git
$ mv cppwasm_template reacttscppwasm_tmplate
$ cd reacttscppwasm_tmplate
②-2. Emscriptenでwasmビルド。
$ cd ~/reacttscppwasm_tmplate/wasm
$ mkdir build && cd build
$ emcmake cmake ..
$ emmake make
$ mv cppmain.js .. # ← これが出来る。
$ mv cppmain.wasm .. # ← これが出来る。
②-3. Webカメラの設定(内蔵カメラならこの手順は不要)
- Webカメラ挿して。
- Windowsの設定 Windowsで設定
- Virtual Box設定 Virtual Box->デバイス->Webカメラ->HD Web Cameraにチェック
②-4.pythonコマンドでサーバ起動->動作確認。
$ cd ~/reacttscppwasm_tmplate/wasm
$ python3 -m http.server 8080
- ブラウザから http://localhost:8080/ にアクセスする。
Ubuntu環境でビルド成功!! 動作確認も成功!!
③React+TypeScriptで開発。
Windowsで。
③-0.テンプレートのソースコード一式を取得
$ cd D:\Products\React.js\
$ git clone https://github.com/aaaa1597/cppwasm_template.git
$ ren cppwasm_template reacttscppwasm_tmplate
$ cd D:\Products\React.js\reacttscppwasm_tmplate
③-1. npm installを実行。
$ cd D:\Products\React.js\reacttscppwasm_tmplate/reactts
$ npm install
③-2. VSCodeで開く。
reacttscppwasm_tmplate\reacttsフォルダで右クリック。
③-3.cppmain.jsの修正。
③-3.1 見やすくするためにcppmain.jsをフォーマット。
Prettier コードフォマッターをインストールして、
↓
Shift + Ctrl + P → Format Document (Forced)を実行
↓
いい感じに整形してくれる。
③-3.2 Moduleのエクスポート化
- 1: var Module = typeof Module != 'undefined' ? Module : {};
+ 1: export var Module = typeof Module != 'undefined' ? Module : {};
③-3.3 cppmain.wasmの読込み元を修正。
cppmain.wasmをpuclic\wasmに配置するのを前提に。
- 715: wasmBinaryFile = 'hello.wasm';
+ 715: wasmBinaryFile = 'wasm/cppmain.wasm';
③-4. カメラ読込み処理の実装
カメラパラメータの定義
+ 4:const constraints: MediaStreamConstraints = {
+ 5: audio: false,
+ 6: video: {
+ 7: width: 640,
+ 8: height: 480,
+ 9: },
+ 10:};
カメラ入力をvideoタグで表示するための準備
20:function App() {
21: const refWasm = useRef<any>(null)
+ 22: const videoRef = useRef<HTMLVideoElement>(null);
カメラ入力をvideoタグで表示するための準備2
+ 36: /* 初期化 */
+ 37: useEffect(() => {
+ 38: const openCamera = async () => {
+ 39: const video = videoRef.current;
+ 40: const canvas = canvasRef.current;
+ 41: if (video && canvas) {
+ 42: canvas.width = 640;
+ 43: canvas.height = 480;
+ 44: video.width = 640;
+ 45: video.height = 480;
+ 46: const stream = await navigator.mediaDevices.getUserMedia(constraints);
// ↑ここで、カメラ使用権限を求めて、
+ 47: video.srcObject = stream;
// ↑取得したstreamをvideoタグに設定する
+ 48: video.play();
+ 49: ctxRef.current = canvas.getContext('2d');
+ 50: loop();
+ 51: }
+ 52: };
+ 53: openCamera();
カメラのためのHTMLElementを配置する。
+ 77: return (
+ 78: <div className="App">
+ 79: hello world!!
+ 80: <video autoPlay playsInline={true} ref={videoRef} />
## ↑ videoタグ
+ 81: <canvas width="640" height="480" ref={canvasRef} />
## ↑ canvasタグ videoタグの画像データを加工してここに表示する。
+ 82: </div>
+ 83: );
③-5. この状態で実行すると下記エラーがでる!!
fsがない。pathがない。
Compiled with problems:
ERROR in ./src/maincpp.js 75:11-24
Module not found: Error: Can't resolve 'fs' in 'D:\Products\React.js\ReactTs-WebAsm004\src'
ERROR in ./src/maincpp.js 76:17-32
Module not found: Error: Can't resolve 'path' in 'D:\Products\React.js\ReactTs-WebAsm004\src'
BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.
If you want to include a polyfill, you need to:
- add a fallback 'resolve.fallback: { "path": require.resolve("path-browserify") }'
- install 'path-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
resolve.fallback: { "path": false }
③-6. エラー解決のためpackage.jsonに下記を追加
"dependencies": {
~略~
},
+ "browser": {
+ "fs": false,
+ "path": false
+ },
③-7. App.tsxでcppmain.jsを読み込みを実装
cppmain.jsは、Emscriptenのビルド生成物だけど、App.tsxで読込みできるように、"③-2.2"でexport化したので読み込むことが出来る。
requireで読み込む様に修正。
21: const refWasm = useRef<any>(null)
+ 28: /* wasm読込み */
- 29: const hWasm = require('./asm-cpp/hello.js');
+ 29: const h = require('./cppmain.js');
30: h.Module.onRuntimeInitialized = () => { // ← requireでのjs読込み完了通知
+ 31: console.log("Wasm loaded.");
+ 32: h.Module["canvas"] = canvasRef.current // cppmain.js内でもcanvasにいろいろやってるので設定。
33: refWasm.current = h.Module // wasmハンドラを保持っとく
+ 34: }
④. OpenCV実行処理を実装
cppmain.jsを読み込んで取得した、wasmのハンドラから自作のcpp関数を呼ぶ処理を実装する。
+ 58: /* ループ */
+ 59: const loop = () => {
+ 60: const ctx = ctxRef.current;
+ 61: const video = videoRef.current;
+ 62: const canvas = canvasRef.current;
+ 63: const wasm = refWasm.current;
+ 64: if(ctx && video && canvas && wasm) {
+ 65: ctx.drawImage(video, 0, 0);
// ↑videoから画像を取得
+ 66: const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
// ↑取得したデータを取り出す
+ 67: const buffer = wasm._creata_buffer(data.data.length);
// ↑自作関数を呼び出し
+ 68: wasm.HEAPU8.set(data.data, buffer);
+ 69: wasm._Convert(buffer, data.width, data.height, cnt);
// ↑自作関数を呼び出し
+ 70: wasm._destroy_buffer(buffer);
// ↑自作関数を呼び出し
+ 71: cnt++;
+ 72: if(cnt>=200) cnt = 0;
+ 73: }
+ 74: loopRef.current = requestAnimationFrame(loop);
+ 75: }
⑤. この状態で実行すると下記エラーがでる!!
eslintエラーs
ERROR in [eslint]
src\App.tsx
Line 7:16: Require statement not part of import statement @typescript-eslint/no-var-requires
src\asm-cpp\hello.js
Line 80:12: Require statement not part of import statement @typescript-eslint/no-var-requires
Line 81:18: Require statement not part of import statement @typescript-eslint/no-var-requires
Line 145:13: 'read' is not defined no-undef
Line 150:29: 'readbuffer' is not defined no-undef
Line 152:16: 'read' is not defined no-undef
Line 162:39: Unexpected empty arrow function @typescript-eslint/no-empty-function
Line 171:18: 'scriptArgs' is not defined no-undef
Line 173:18: 'arguments' is not defined no-undef
Line 195:9: 'quit' is not defined no-undef
Line 203:40: Read-only global 'console' should not be modified no-global-assign
Line 205:125: 'printErr' is not defined no-undef
Line 567:10: Unexpected constant condition no-constant-condition
Line 890:7: Expected to return a value in method 'get' getter-return
Line 966:7: Expected to return a value in method 'get' getter-return
Line 1015:7: Expected a 'break' statement before 'case' no-fallthrough
Line 1045:7: Expected a 'break' statement before 'case' no-fallthrough
eslintが出力するエラーを無視する。
2-1. Require statement not part of import statement @typescript-eslint/no-var-requires
"rules": {
+ "@typescript-eslint/no-var-requires": "off",
}
2-2. 'xxx' is not defined no-undef
+ "globals": {
+ "read": false,
+ "readbuffer": false,
+ "scriptArgs": false,
+ "arguments": false,
+ "quit": false,
+ "printErr": false,
+ },
"rules": {
2-3. Unexpected empty arrow function @typescript-eslint/no-empty-function
"rules": {
"@typescript-eslint/no-var-requires": "off",
+ "@typescript-eslint/no-empty-function": "off",
}
2-4. Read-only global 'console' should not be modified no-global-assign
"rules": {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-function": "off",
+ "no-global-assign": "off",
}
2-5. Unexpected constant condition no-constant-condition
"rules": {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-function": "off",
"no-global-assign": "off",
+ "no-constant-condition": ["error", { "checkLoops": false }],
}
2-6. Expected to return a value in method 'get' getter-return
"rules": {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-function": "off",
"no-global-assign": "off",
"no-constant-condition": ["error", { "checkLoops": false }],
+ "getter-return": "off",
}
2-7. Expected a 'break' statement before 'case' no-fallthrough
"rules": {
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-function": "off",
"no-global-assign": "off",
"no-constant-condition": ["error", { "checkLoops": false }],
"getter-return": "off",
+ "no-fallthrough": "off",
}
修正完了。
App.tsx全体
import React, { useEffect, useRef } from 'react';
import './App.css';
const constraints: MediaStreamConstraints = {
audio: false,
video: {
width: 640,
height: 480 ,
},
};
function App() {
const refWasm = useRef<any>(null)
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const ctxRef = useRef<CanvasRenderingContext2D|null>();
const loopRef = useRef(0);
let cnt = 0;
/* wasm読込み */
const h = require('./cppmain.js');
h.Module.onRuntimeInitialized = () => {
console.log("Wasm loaded.");
h.Module["canvas"] = canvasRef.current
refWasm.current = h.Module
}
/* 初期化 */
useEffect(() => {
const openCamera = async () => {
const video = videoRef.current;
const canvas = canvasRef.current;
if (video && canvas) {
canvas.width = 640;
canvas.height = 480;
video.width = 640;
video.height = 480;
const stream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = stream;
video.play();
ctxRef.current = canvas.getContext('2d');
loop();
}
};
openCamera();
return () => cancelAnimationFrame(loopRef.current);
}, [])
/* ループ */
const loop = () => {
const ctx = ctxRef.current;
const video = videoRef.current;
const canvas = canvasRef.current;
const wasm = refWasm.current;
if(ctx && video && canvas && wasm) {
ctx.drawImage(video, 0, 0);
const data = ctx.getImageData(0, 0, canvas.width, canvas.height);
const buffer = wasm._creata_buffer(data.data.length);
wasm.HEAPU8.set(data.data, buffer);
wasm._Convert(buffer, data.width, data.height, cnt);
wasm._destroy_buffer(buffer);
cnt++;
if(cnt>=200) cnt = 0;
}
loopRef.current = requestAnimationFrame(loop);
}
return (
<div className="App">
hello world!!
<video autoPlay playsInline={true} ref={videoRef} />
<canvas width="640" height="480" ref={canvasRef} />
</div>
);
}
export default App;
- 実行
出来た!!
見た目地味だけど、WebAssemblyが実現できてるぞ!!
Wasm.ModuleのAny型なので、扱いにくいかな~と思ったけど、そうでもなくって。
型定期ファイル(.d.ts)使うのは、困ってからでいいや。
<- React+TypeScriptでWebAssembly004
React+TypeScriptでWebAssembly006 ->
Discussion