たくさんの自由帳
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=falseFAILURE: 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するとホームディレクトリに地味にでかいフォルダが生成されます。
今回使ったこれ、メモリに優しくない(めっちゃ消費する)ので、イマイチです。