🍤

ブラウザ上でEbitengineを使ってwebカメラから取得した映像を描画してみた

2024/05/09に公開

目的

GoCVを使わずに、ブラウザで実行可能なwebカメラを使ったEbitengine製アプリを作りたい。

成果物

ブラウザ上でwebカメラから取得した映像をEbitengineを用いて描画することができました。

環境

$ go version
go version go1.21.5 darwin/arm64

ブラウザはChromeです。

コード

.
├── go.mod
├── go.sum
├── index.html
├── main.go
├── main.wasm
└── wasm_exec.js
コード
main.go
package main

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"image"
	"log"
	"syscall/js"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)

var (
	video  js.Value
	stream js.Value
	canvas js.Value
	ctx    js.Value
)

const (
	ScreenWidth  = 320
	ScreenHeight = 240
)

func init() {
	doc := js.Global().Get("document")
	video = doc.Call("createElement", "video")
	canvas = doc.Call("createElement", "canvas")
	video.Set("autoplay", true)
	video.Set("muted", true)
	video.Set("videoWidth", ScreenWidth)
	video.Set("videoHeight", ScreenHeight)
	mediaDevices := js.Global().Get("navigator").Get("mediaDevices")
	promise := mediaDevices.Call("getUserMedia", map[string]interface{}{
		"video": true,
		"audio": false,
	})
	promise.Call("then", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
		stream = args[0]
		video.Set("srcObject", stream)
		video.Call("play")
		canvas.Set("width", ScreenWidth)
		canvas.Set("height", ScreenHeight)
		ctx = canvas.Call("getContext", "2d")
		return nil
	}))

}

type Game struct {}

func newGame() *Game {
	return &Game{}
}

func (g *Game) Update() error {
	if !ctx.Truthy() {
		return nil
	}
	ctx.Call("drawImage", video, 0, 0, ScreenWidth, ScreenHeight)
	return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
	if stream.Truthy() && ctx.Truthy() {
		b64 := canvas.Call("toDataURL", "image/png").String()
		dec, err := base64.StdEncoding.DecodeString(b64[22:])
		if err != nil {
			log.Fatal(err)
		}
		img, _, err := image.Decode(bytes.NewReader(dec))
		if err != nil {
			log.Fatal(err)
		}
		screen.DrawImage(ebiten.NewImageFromImage(img), nil)
	}
	ebitenutil.DebugPrint(screen, fmt.Sprintf("%f", ebiten.ActualFPS()))
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
	return ScreenWidth * 2, ScreenHeight * 2
}

func main() {
	ebiten.SetWindowSize(ScreenWidth*2, ScreenHeight*2)
	ebiten.SetWindowTitle("Hello, World!")
	if err := ebiten.RunGame(newGame()); err != nil {
		log.Fatal(err)
	}
}

その他のコードは以下の手順の通りに行ったものです。

https://ebitengine.org/ja/documents/webassembly.html

解説

syscall/jsを使うことでJavaScriptを実行し、画像データを取得して、それをEbitengineで描画しています。

JavaScriptによるwebカメラの使い方は以下の記事を参考にしてください。

https://reffect.co.jp/html/javascript-webcamera

Update内でvideoタグの映像をcanvasに移して、Draw内でbase64文字列を取得し、image.ImageにDecodeしてからebiten.Imageとしてscreenに描画しています。

以下の記事を参考にしてください。

https://qiita.com/n0bisuke/items/2a532fd192e0b29711c3

まとめ

まだ使い道は思いついていませんが、とりあえずEbitengineでwebカメラを使えることがわかってよかったです。

Discussion