たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 11944
目次
本題
今回ライブラリにするコード
完成品
参考
環境
作る
プロジェクトを作る
Wasm ターゲットを追加する
Wasm 専用処理を書くフォルダを作る
ドメインを使ったパッケージ名を作る
コードを書く
JavaScript 側に公開する準備
JavaScript と Kotlin のデータやり取り
JavaScript で呼び出せるようにエクスポートする
Configuration cache state could not be cached: field value of kotlin.reflect.jvm.internal.ReflectProperties$LazySoftVal bean found in field ...
Lock file was changed
npm install してみる!
インポートして使ってみる
実際に使ってみる
Next.js のクライアントコンポーネントで呼び出して使う
Kotlin/Wasm の JS コード修正
遅延読み込みさせる
完成!
ソースコード
おわりに
おわりに2
おわりに3
どうもこんばんわ。
花鐘カナデ*グラム Chapter:3 星泉コトナ 攻略しました。前作やって無くてもいけそう!
!???!?!?!?!!
今作、物語がすごい進んだような気がする!!!最終章に向けての伏線的なのが。
最終章すごい気になる。
なにかありそう最後のヒロイン
↓このあとのシーンめっちゃ良かった。えちえちだった。
今作もだけど冒頭のやつ何なんだろう、、最後のヒロインで分かるのかな。
ちなみに、今作は最後かわいいイベント CG で終れてよかった!!
(Chapter 2 の一番最後は何だったんだ、、?)
かわいい!!!おすすめです!
Kotlin Multiplatform
を使うと、Kotlin
の関数をエクスポートしてnpm ライブラリ
にして、npm install
してJavaScript
から呼び出すことが出来ます。Use Kotlin code from JavaScript | Kotlin
https://kotlinlang.org/docs/js-to-kotlin-interop.html
今回はこれを使ってJavaScript
のMediaRecorder
をシークできるようにするコードを、npm install
して、React
とかで使えるようにしてみます。
これですが、現状はUI
もKotlin/JS
のリソースの中にあるHTML
に書いている状況です。
これを、シーク処理
はKotlin
で書いてJS
向けにエクスポート。それをReact
か何かでUI
を作り、呼び出すようにしてみます。
超実験的ですが、
以下のGitHub
リポジトリを指定してnpm install
してください。JavaScript
のMediaRecorder
が生成するWebM
ファイルをシークできるようにする関数を提供します。
npm install takusan23/himari-webm-kotlin-multiplatform-npm-library
今のところ以下の環境で動くことを確認しています。
Next.js
のクライアントコンポーネント
で使うReact
とVite
で使う参考にしましあ!!
Android Studio
かIDEA
が必要?
npm ライブラリ
にしたあと、ブラウザで動かすには何かしらフロントエンドを作る必要があるのですが、今回はNext.js
で。React + Vite
でも動きます。
mapOf(
"Android Studio" to "Android Studio Narwhal | 2025.1.1",
"Kotlin" to "2.2.0",
"Next.js" to "15.3.4"
)
ここにKotlin Multiplatform
のライブラリを作るテンプレートがあります。
別にKotlin Multiplatform
のライブラリを作るわけじゃないです。がライブラリのテンプレートなので、ComposeMultiplatform
とか入って無い構成。
このリポジトリをgit clone
して、Android Studio
で開きます。
Gradle Sync
が終わるまでまちます!JS
の世界から戻ってくるとやっぱり遅いねんこれ
↑と同じです。
Kotlin/Wasm
を追加します。
今回、npm install
してJS
側から使う際にはWasm
が使われるようにします。Kotlin/JS
は分からず...
build.gradle.kts
を開いて、kotlin { }
のところを探し、中に足します。
kotlin {
// gradle wasmJsBrowserProductionRun
// JavaScript からは、kotlin/wasm でライブラリとして npm で登録する。
wasmJs {
outputModuleName = "fix-seekable-webm-kotlin-wasm"
binaries.executable()
browser {
}
generateTypeScriptDefinitions()
// https://kotlinlang.org/docs/wasm-js-interop.html#exception-handling
compilerOptions {
freeCompilerArgs.add("-Xwasm-attach-js-exception")
}
// npm の package.json に types を入れてくれないので
// https://youtrack.jetbrains.com/issue/KT-77319
// https://youtrack.jetbrains.com/issue/KT-77073
compilations["main"].packageJson {
this@packageJson.types = "kotlin/${outputModuleName.get()}.d.ts"
}
}
// 以下省略
}
outputModuleName
が出力される.wasm
ファイルの名前だったりbinaries.executable()
でブラウザで動くようになるはず。
generateTypeScriptDefinitions()
で、公開したKotlin
の関数に対応する、TypeScript
の型定義を作ってくれます。
ただ、その型定義をJS
側で認識させるために、this@packageJson.types
を指定する必要があります。Issue
があるので今後修正されるはずです。
他にもjvm()
やandroidTarget { }
、iosX64()
がありますがまあどっちでも良いです。
使わないので消しても良いかもしれません。
プラットフォーム別の処理を書くフォルダです。今回はWasm
ですね。
スクショのようにディレクトリを作ると、wasmJsMain
ってディレクトリが生成されるはずです、
ここではwasmJsMain
を選びます。
Kotlin Multiplatform
でもこの考えが引き継がれているのかは、わからないのですが、今回は使います。
どういうわけかというと、自分の持っているドメインをひっくり返したものをパッケージとして使う文化があります。
https://takusan.negitoro.dev/
だったら、パッケージ名がdev.negitoro.takusan
になるわけです。
この後にアプリ名がつくので、dev.negitoro.takusan.{APP名}
になるでしょうか。
ドメインなければGitHub Pages
の.io
ドメインを使ったり。
要は被らなければよいわけです。
というわけでやります。私はずっとGitHub Pages
のを使っているので今回もこれで。あと後ろにアプリ名を``付与して、以下のようになりました。io.github.takusan23.fixseekablewebmkotlinwasm
まあね、Kotlin/Wasm
をJS
ライブラリにするだけなら、多分する必要ないです。
書きます。
今回はすでにあるコードをコピーするだけ!
Kotlin Multiplatform
なので、Kotlin
標準ライブラリか、Multiplatform
対応ライブラリを使う必要があります。
ご存知でしょうが、例えばJVM
のクラスは、JVM
ターゲットでは使えますが、wasm (JavaScript)
ではもちろん使えません。
wasmJsMain
のKotlin
コードでは、JavaScript
に公開する(エクスポートする)ためのアノテーションが利用できます。なので、wasmJsMain
にJavaScript
側から呼び出したい関数を作ります。
ファイル名は何でも良いはず?
ここに、トップレベルの関数を作って、@JsExport
アノテーションを関数に付与すると、JavaScript
から呼び出すことが出来ます!!!
@OptIn(ExperimentalJsExport::class)
@JsExport
fun fixSeekableWebm() {
// common で書いた処理を呼び出すなど
}
今回はこうです!!WebM
ファイルのバイト配列を受け取って、Kotlin Multiplatform 製
のシーク出来るようにする修正関数を呼び出し、バイト配列を返すという感じです。
エクスポートするには引数の制限があるので、こんな感じになりました。
詳しくはこのあとの説明で!
@OptIn(ExperimentalJsExport::class)
@JsExport
fun fixSeekableWebm(webmByteArray: JsArray<JsNumber>): JsArray<JsNumber> {
val kotlinByteArray = webmByteArray.toList().map { it.toInt().toByte() }.toByteArray()
val elementList = HimariWebmParser.parseWebmLowLevel(kotlinByteArray)
val fixedByteArray = HimariFixSeekableWebm.fixSeekableWebm(elementList)
val wasmFixedByteArray = fixedByteArray.map { it.toInt().toJsNumber() }.toJsArray()
return wasmFixedByteArray
}
プリミティブ型は渡せます。
@JsExport
fun fixSeekableWebm(int: Int, string: String) // OK
Interoperability with JavaScript | Kotlin
https://kotlinlang.org/docs/wasm-js-interop.html
@JsExport
fun fixSeekableWebm(list: List<String>) // エラー
これは、JsArray<>
を使うことで配列もJavaScript
から受け取ることが出来ます。
@JsExport
fun fixSeekableWebm(intList: JsArray<JsNumber>, stringList: JsArray<JsString>) {
val kotlinIntList: List<Int> = intList.toList().map { it.toInt() }
val kotlinStringList = stringList.toList().map { it.toString() }
}
JsArray
は拡張関数があるので、相互変換は結構簡単かなと思います。Kotlin
らしくてすきすき大好き~
val kotlinByteArray: ByteArray = webmByteArray.toList().map { it.toInt().toByte() }.toByteArray()
val wasmFixedByteArray: JsArray<JsNumber> = fixedByteArray.map { it.toInt().toJsNumber() }.toJsArray()
そして、個人的にすごいと思ったのが、JsReference<>
です。
名前の通りこれは、Kotlin/Wasm
側の値の参照を表現するクラスです。Kotlin/Wasm
で使っている参照を表すためのものなので、JS
側からはよく分からないオブジェクトとかになるはずです。
例えば、Kotlin/Wasm
でKotlin
のクラスの参照をJsReference
で返します。
これは、JS
の世界ではよく分からないオブジェクトですが、これをKotlin/Wasm
側の関数に渡すと、参照からクラスを取得できるという魂胆です。
@JsExport
fun parseWebm(webmByteArray: JsArray<JsNumber>): JsReference<WebmParseResult> {
return HimariWebmParser.parseWebm(webmByteArray.toByteArray()).toJsReference()
}
@JsExport
fun getVideoWidthFromWebmParseResult(reference: JsReference<WebmParseResult>): Int? {
return reference.get().videoTrack?.videoWidth
}
これをJavaScript
で呼び出すと、Kotlin/Wasm
で使っている値の参照がもらえます。JS
側ではこの参照を、参照を必要とする関数に渡すくらいしか使い道はないと思います。
呼び出されると、Kotlin/Wasm
側の関数はget()
で参照からインスタンスに戻せるって。
const parseRef = parseWebm(intArray)
const width: number = getVideoWidthFromWebmParseResult(parseRef)
もちろんJsArray<>
にJsReference
を入れてもちゃんと動きます。JsArray<JsReference<T>>
ここからGradle
のコマンドを入力するウィンドウを出します。
そしたら、テキストフィールドに以下のコマンドを入力して、Enter
します。
gradle wasmJsBrowserProductionRun
これで待っているとnpm install
出来るライブラリが出力されます。
パスは→build/wasm/packages/{outputModuleName}
に存在するはずです。
なんか見覚えあるフォルダ構成だなって
value
of kotlin.reflect.jvm.internal.ReflectProperties$LazySoftVal
bean found in field ...なっがw
FAILURE: Build failed with an exception.
* What went wrong:
Configuration cache state could not be cached: field `value` of `kotlin.reflect.jvm.internal.ReflectProperties$LazySoftVal` bean found in field `descriptor$delegate` of `kotlin.reflect.jvm.internal.KClassImpl$Data` bean found in field `value` of `kotlin.InitializedLazyImpl` bean found in field `data` of `kotlin.reflect.jvm.internal.KClassImpl` bean found in field `nodeJsRootKlass` of `org.jetbrains.kotlin.gradle.targets.web.nodejs.NodeJsRootPluginApplier` bean found in field `this$0` of `org.jetbrains.kotlin.gradle.targets.web.nodejs.NodeJsRootPluginApplier$apply$nodeJsRoot$1` bean found in field `nodeJs` of `org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsRootExtension` bean found in field `nodeJsRoot` of `org.jetbrains.kotlin.gradle.targets.js.ir.KotlinBrowserJsIr` bean found in field `value` of `kotlin.InitializedLazyImpl` bean found in field `browserLazyDelegate` of `org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTarget` bean found in field `$this_wasmJs` of `Build_gradle$1$1$3` bean found in field `__packageJsonHandlers__` of task `:library:wasmJsPackageJson` of type `org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinPackageJsonTask`: error writing value of type 'java.lang.ref.SoftReference'
> Unable to make field private long java.lang.ref.SoftReference.timestamp accessible: module java.base does not "opens java.lang.ref" to unnamed module @81439d1
よくわからないですが、Gradle
のConfiguration Cache
が有効になってるとなぜか失敗する。
とりあえず成功させたいのでgradle.properties
ファイルを開き、以下のキーをfalse
にした。
org.gradle.configuration-cache=false
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':kotlinWasmStoreYarnLock'.
> Lock file was changed. Run the `kotlinWasmUpgradeYarnLock` task to actualize lock file
何もしてないはずなので、kotlin-js-store
フォルダーを消しました。とりあえずは治りました。
React + Vite
でもNext.js
でも何でも良いのですが、今回はReact
でUI
を作り、Kotlin/Multiplatform
の関数を呼び出そうかなと。
これはどっちでも良いと思います。なんならReact
以外でも良いんじゃない?
Next.js
+クライアントコンポーネントで Kotlin/Wasm の関数を呼び出す
Vite + React
今回は一番手間なNext.js + クライアントコンポーネント
でやってみます。
とにかくReact
で試したい場合はVite
がいいと思います。
というわけで新しいフォルダを作って、Next.js
のプロジェクトを作ります。
今回はTypeScript ON
、TailwindCSS ON
、AppRouter / src directory ON
でいきます。
npx create-next-app@latest
適当にフロントエンドのプロジェクトが出来たら、さっき作ったKotlin/Wasm
のライブラリを入れます。
フロントエンドのフォルダに、さっきのKotlin/Wasm 製
ライブラリをコピーして、ファイルパスを指定してnpm install
をします。
こんな感じにフロント側で、適当なTypeScript
を開いて(今回はpage,tsx
)、Kotlin/Wasm
でエクスポートした関数名を入力すると、VSCode
が補完を表示するはず。
もし出なくても、import { } from "ライブラリ名"
すれば取り込めるはず。
すごい!!!
import { fixSeekableWebm } from "fix-seekable-webm-kotlin-wasm";
export default function Home() {
fixSeekableWebm()
}
今回はMediaRecorder
のWebM
をシークできるようにするコードなので、MediaRecorder
から書いていきます。
もう面倒なのが理由ですが、全部一つのファイルに書いたほうが読む側は楽かな~って
今回はNext.js
+TailwindCSS
を使っています。
録画開始ボタン・終了ボタン。それからWebM
ファイルを受け付けるボタンをおいています。MediaRecorder
の画面録画は多分調べたら出てくるので省略します。
画面録画するとBlob[]
が取得できます。配列の中身を全部つなげるとWebM
ファイルになるという感じです。
なんとかしてArrayBuffer
に変換します。これをInt8Array()
とかに渡すと無事バイト配列として操作できるというわけです。
このバイト配列をKotlin/Wasm
の関数渡します。型エラーになるので無理やりキャストして渡しています。一応動いてます。
あとは、この関数の返り値をいい感じにInt8Array -> ArrayBuffer -> BlobUrl
にすることでダウンロードができます。
await import("fix-seekable-webm-kotlin-wasm")
しているのは後述します。
"use client"
import { useRef, useState } from "react";
export default function Home() {
const chunkList = useRef<Blob[]>([])
const mediaRecorder = useRef<MediaRecorder>(null)
const mediaStream = useRef<MediaStream>(null)
const [isRunning, setRunning] = useState(false)
async function handleStartClick() {
mediaStream.current = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: { channelCount: 2 } });
mediaRecorder.current = new MediaRecorder(mediaStream.current, { mimeType: 'video/webm; codecs="vp9"' })
mediaRecorder.current.ondataavailable = (event) => {
chunkList.current.push(event.data);
}
mediaRecorder.current.start(100)
}
async function handleStopClick() {
// 終了させる
mediaRecorder.current?.stop()
mediaStream.current?.getTracks().forEach((track) => track.stop())
// シークできるようにする関数
const webmArrayBuffer = await (new Blob(chunkList.current, { type: "video/webm" })).arrayBuffer()
startFixSeekableWebmLibAndDownload(webmArrayBuffer)
// リセット
chunkList.current.splice(0)
}
async function startFixSeekableWebmLibAndDownload(arrayBuffer: ArrayBuffer) {
// UI に反映
setRunning(true)
// Kotlin/Wasm の JsArray<Number> にする
const intArray = new Int8Array(arrayBuffer)
// Kotlin/Wasm でエクスポートした関数を呼び出す
// 今のところ遅延ロードでクライアントからインポートする必要がありそう
// window オブジェクトが使えるかの判定を内部でしている雰囲気
const { fixSeekableWebm } = await import("fix-seekable-webm-kotlin-wasm")
const fixSeekableWebmNumberList = fixSeekableWebm(intArray as any)
const fixSeekableWebmIntArray = new Int8Array(fixSeekableWebmNumberList as any)
// ダウンロード
const downloadBlob = new Blob([fixSeekableWebmIntArray], { type: "video/webm" })
const blobUrl = URL.createObjectURL(downloadBlob)
// <a> タグを作って押す
const anchor = document.createElement("a") as HTMLAnchorElement
anchor.href = blobUrl
anchor.download = `fix-seekable-webm-kotlin-wasm-${Date.now()}.webm`
document.body?.appendChild(anchor)
anchor.click()
anchor.remove()
// 戻す
setRunning(false)
}
async function handleWebmFilePicker(event: React.ChangeEvent<HTMLInputElement>) {
// 取得
const webmFile = event.currentTarget.files?.[0]
if (!webmFile) return
// 処理開始
const webmArrayBuffer = await webmFile.arrayBuffer()
startFixSeekableWebmLibAndDownload(webmArrayBuffer)
}
return (
<div className="flex flex-col items-start space-y-2">
<h1>JavaScript の MediaRecorder で出力される WebM をシークできるようにする Kotlin/Wasm</h1>
<h2>Kotlin/Wasm を npm install して Next.js から使っています。</h2>
{
isRunning && (
<p className="text-red-400">
WebM ファイルをシークできるように修正中です。すこし時間がかかります。
</p>
)
}
<div className="flex flex-col p-2 border-2 border-blue-400 rounded-lg">
<h3>MediaRecorder 録画開始・終了</h3>
<button className="border-2" onClick={handleStartClick}>録画開始</button>
<button className="border-2" onClick={handleStopClick}>録画終了</button>
</div>
<div className="flex flex-col p-2 border-2 border-blue-400 rounded-lg">
<h3>すでにある WebM ファイルをシークできるようにする</h3>
<input
className="border-2"
type="file"
accept=".webm"
onChange={handleWebmFilePicker} />
</div>
</div>
)
}
スクリーンショットです。
ちなみに、Vite+React
ではこの問題は起きません。process
オブジェクトがそもそもないので。
一方、Next.js
のクライアントコンポーネントから使おうとすると、このエラーが発生します。
Error: Cannot read properties of undefined (reading 'name')
調べた結果、Next.js
のサーバーコンポーネントの場合、グローバル変数のprocess
のprocess.release.name
の返り値が"node"
なんですが、
クライアントコンポーネントでは、process
オブジェクトは存在するものの、release
オブジェクトが存在しないのでエラーになる。
fix-seekable-webm-kotlin-wasm.uninstantiated.mjs
ファイルの、この行でrelease
オブジェクトが存在するかの条件を足すだけだと思います。kotlin/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/wasmCompiler.kt at c0389ea0ab922e0e3f53b5562251b7d53191239a · JetBrains/kotlin
The Kotlin Programming Language. . Contribute to JetBrains/kotlin development by creating an account on GitHub.
https://github.com/JetBrains/kotlin/blob/c0389ea0ab922e0e3f53b5562251b7d53191239a/compiler/ir/backend.wasm/src/org/jetbrains/kotlin/backend/wasm/wasmCompiler.kt
const isNodeJs = (typeof process !== 'undefined') && (process.release.name === 'node');
なので、こうやってtruthy
の値かの判定を入れるか、オプショナルチェーンするか。
今のところ、Kotlin
が出力したファイルを手直しするのが一番早そう。手直しするのが一番ダメそうな案だけど。
// truthy
const isNodeJs = (typeof process !== 'undefined') && process.release && (process.release.name === 'node');
// もしくは optional chaining
const isNodeJs = (typeof process !== 'undefined') && (process.release?.name === 'node');
これもVite+React
の場合は問題になりません。良くも悪くもブラウザ環境でしか描画されないためです。
Kotlin/Wasm
が.wasm
をロードする際、今の環境がサーバーかブラウザかでロード処理を分けています。
クライアントコンポーネントでKotlin/Wasm
の関数を呼び出して使う場合、ブラウザ環境で動かすことになります。
で、そのブラウザ環境かの判定にwindow
オブジェクトがグローバル変数に存在するか。で判定しています。Next.js
を使ったことある方、もうお分かりですね。
typeof window !== 'undefined'
Next.js
は御存知の通り、サーバー(Node.js
環境)で一回React
が描画されてから、ブラウザへ送信されます。Next.js
の強みです。
これがブラウザ環境で描画する場合、ブラウザ由来のAPI
、window
やdocument
が存在します。よってこの判定は問題なく動くわけですね。
一方Node.js
環境で描画する場合、ブラウザ由来のAPI
は存在しないため、判定結果がNode.js、でなります。いやNode.js
環境だってんだからそりゃそうだろ。
window
がundefined
以外であることが保証されているはずです。Guides: Lazy Loading | Next.js
Lazy load imported libraries and React Components to improve your application's loading performance.
今回はこれを使うことで、確実に(?)ブラウザ側でしかライブラリがロードしないようになりました。
必要になるまでロードしないため、高速化にもつながったはずです。
というわけで、このインポートを消して
import { fixSeekableWebm } from "fix-seekable-webm-kotlin-wasm"
代わりに必要なタイミングでawait import()
するように修正しました
async function startFixSeekableWebmLibAndDownload(arrayBuffer: ArrayBuffer) {
// 必要になるまでライブラリを import しない
const { fixSeekableWebm } = await import("fix-seekable-webm-kotlin-wasm")
const fixSeekableWebmNumberList = fixSeekableWebm(intArray as any)
}
Kotlin/Wasm
で作った関数をJavaScript
で呼び出して使うことが出来ました!!!
ちゃんと録画した動画がシークできるようになってます!
今回の例で使ったものたち
冒頭のGitHub
からnpm install
できるnpm ライブラリ
とKotlin Multiplatform
シークできる処理はKotlin
で書かれていて、Kotlin/JS
しています。
一方、Kotlin/JS
ではReact
なんかは使えない(使えたところで Kotlin で書くのは・・・)ため、見た目は調整せずHTML
を書いただけになっていました。
ですが、シークできる処理がnpm ライブラリ
になったことで、見た目はReact
で書いて、シークする処理はKotlin/Wasm
のライブラリを読み込むだけになった。
見た目はやっぱりReact
とかで作りたい。
それと、JavaScript
前提のブラウザ API
をKotlin
の文法で書くのは、、、コレジャナイ感ある。
すべてのブラウザ API
がKotlin/JS
で呼び出せる、、わけじゃない。
一部のAPI
は用意されてないので、js()
でJavaScript
のコードを文字列で渡していて、やっぱJavaScript
向けのAPI
はその言語で書くべきだよなって。
js(" ここに JS コード ")
←これがよく動いているのはすごいけど、、
Kotlin Multiplatform
するとホームディレクトリに地味にでかいフォルダが生成されます。
今回使ったこれ、メモリに優しくない(めっちゃ消費する)ので、イマイチです。