どうもこんばんわ。
忘年会でお酒飲まずにソフトドリンクだけ飲んでたんですけどやっぱ飲んだほうが得なんかな(??)
本題
JavaScript
にMediaRecorder
とかいうやつがあるんですけど(Android
のやつじゃないです)
これのおかげでJavaScript
を少し書いてブラウザで開くだけで画面録画ができます!
https://takusan.negitoro.dev/posts/javascript_webm_livestreaming/
画面録画がブラウザひとつで出来る一方、お悩みポイントもあって、出来た WebM ファイルがシークが出来ない点です
シークできない
Firefox
は超優秀ですね、シークが出来ます。
Firefox
は動画の長さを解析する機能があるのでしょうか。
一方、Chrome
とかWindows
の標準プレイヤーとか、その他の動画プレイヤーも再生中ではありますがシークバーが進みません。
シークできるようにしてみる
というわけで、今回はこのシークできないwebm
をシークできるようにしてみようと思います。
すでに先人がいますが、、、この次くらいにwebm
触る予定なのでちょうどいいです。自分でも作ってみます。
先に完成品
ここで WebM を選んで処理を始めればおけ。
ちょっと待てば勝手にダウンロードがされます。
https://webm.negitoro.dev
WebM について
その前にWebM
について、どの様に保存しているかをさらっっっと
WebM
の中身というかデータの格納方法とかは前も書いたのでそっちも見て
https://takusan.negitoro.dev/posts/video_webm_spec/
また、Matroska
のサブセットなのでそっちも見て
https://www.matroska.org/technical/elements.html
データの格納方法
EBML
という仕組みに乗っかってます。
xml
がよく例で出るけど閉じタグは無いので、yaml
とかが近そう。
閉じタグが無いので、要素にはサイズが記録されています。
EBML
はこんな感じの構造が続いています。
- ID
- その名の通り、何のデータかどうかを表す
- 可変長です
- ID の一覧です
- DataSize
DataSize
の後ろにあるData
の大きさです
DataSize
自身も可変長です
- Data
ID
とDataSize
は可変長になっていて、どこにサイズが入っているかというと、
最初のバイトを、16進数から2進数にした後、左から1
がどの位置に入っているか。でサイズが分かるようになっています。
1 の位置 | データの大きさ |
---|
1000_0000 | 1 |
0100_0000 | 2 |
0010_0000 | 3 |
0001_0000 | 4 |
0000_1000 | 5 |
0000_0100 | 6 |
0000_0010 | 7 |
0000_0001 | 8 |
ID
とDataSize
はコレに乗っかっているので、自分自身の大きさを含めながらID
/DataSize
としての役割も果たします。すごい!
これをVINT
とか言うらしい。
ちなみにDataSize
は左から最初の1
はVINT
のために1
が立っているので、データの大きさとして使う場合はその最初の1
を無視する必要があります(ビット演算的に言うとフラグを折る)。
あ、ちなみにパースではなく組み立てる際、1
を立てる位置には注意する必要があります。
例えば1バイト
のデータ0100 0000
なら先頭に1
を立てても問題ないですが、
1111 0000
の場合はすでに1
が立っているので1
を立てることは出来ません。代わりに2バイト
に増やして1000 0000 1111 0000
にする必要があると思います。
また、次のような2バイト
のデータ1000 0000 0000 0000
の場合、左から2バイト目に1
を立てたいところですが、この場合も1バイト増やす必要があります。
これで立てると左の最初の1
が2
番目から1
番目になってしまうため、パースに失敗してしまいます。
実装的には先頭の1バイトが、1を立てただけの1バイトよりも大きくなる場合は1バイト足す必要があります。1を立てただけのバイトの方が大きい場合はそのまま立てて良いはず。
例えば
例えば、これから追加する動画の時間はこんな感じ
まずID
を解析します。(これは動画の時間を表しているって答えを言っていますが聞かなかったことに)
44
は2進数にすると0100 0100
ですね。
1
の位置は左から2番目なので、VINT
の表と照らし合わせてID
は2バイト分あることになります。
2バイト
取り出して、ID
は44 89
であることが分かりました。
4489
をID一覧
から探します!ありました!
このデータはDuration
ですね!
次にDataSize
です。84
を2進数にすると1000 0100
ですね。同様に表から探すと1
バイト分だということが分かります。
DataSize
は1バイト
分だとわかったので84
です。なんですが、左から最初の1
はVINT
用のやつなので無視する必要があります。
というわけで折ります。16進数84
を2進数にすると1000 0100
ですが、左から最初の1
は無視したいので0000 0100
です。
0100
を10進数にしたら4
ですね。よってData
の大きさは4
バイト分だと分かるわけです。
というわけでData
は48 b6 c8 60
。これが動画の長さです。
なんか動画の時間にしては変に見えますが、整数じゃないんですよね。ID
一覧をよく見るとFloat
って書いてあります。なのでそれはまたおいおい
その他の話
親要素(Segment
/Info
/Tracks
/Cluster
等)はData
にデータではなくEBML
が入っているので、子要素を解析するようにする必要があります。
それもID
の表に書いてあるので見て
それと、DataSize
には長さ不明というのが既に予約されています。(0x01FFFFFFFFFFFFFF
)
もし長さが不明な場合は、子要素のDataSize
を足していけば分かるはず。
DataSize
が不明だと解析がめっちゃ面倒になるので無視したくなりますが、JavaScript
のMediaRecorder
が作ったWebM
には長さ不明が出現します。
なので、長さ不明を受け付けないようなコードを書いてはいけません。
出現すると言っても、出現箇所は決まってて(多分)Segment
とCluster
が長さ不明になるはずです。気をつけましょう(?)
コレを繰り返すことで、解析することが出来ます。
(他にもCodecPrivate
とかは別途説明が必要ですがそれは前書いた記事見て。シークしたいだけなら関係ないので。)
以上!!!データの格納方法!でした!
WebM に入っているデータ
EBML
に乗っかって入っているデータが以下
公式:
https://www.matroska.org/technical/diagram.html
シークできるようにするには
↑ のやつから欠けているやつを入れる必要がある
動画の長さが記録されてない(Duration)
動画の長さがwebm
に記録されていない。
webm
のファイル構造を見るためのGUI
アプリケーションがあるのでそれを開きます。
https://mkvtoolnix.download/
確かに動画の長さがJavaScript
のMediaRecorder
が吐き出したファイルには存在しないですね。
シークできる動画には動画の長さが記録されていそうです。
というわけでまずは動画の長さをWebM ファイル
に書き込む必要があるわけですね。
シークのための目印がない(Cue)
詳しくは:
https://www.matroska.org/technical/cues.html
そのシークの目印ってのがCue
要素とかいうやつで、これもJavaScript
のMediaRecorder
が吐き出したファイルには存在しません。
これは映像データ(後述)が、先頭から何バイト目にあるかというのを時間とともに目印として入れておきます。
これにより、プレイヤーがシークする際にCluster
を上から舐める必要がなくなり、シークした時間に一番近い目印を探して、Cluster
を読み出すことが出来るわけです。
・・・が後述しますが、Cue
とこの後説明するSeekHead
が無くても動画時間さえあれば再生できるプレイヤーも存在するらしい。
シークのための目印が入っていることを知らせることが出来ない(SeekHead)
時間と、シークの目印だけを入れればシークできるのですが、どこにCue
を入れるかによっては話が変わってきます。
というのも
上記のように、映像、音声データを入れているCluster
より前にCue
を置くことが出来る場合はおそらく問題無いでしょう。
一方、上記のようにCluster
の後にCue
が来る場合、Cluster
の一番最後にCue
がありますよと教えてあげる必要があります。
Cluster
は映像、音声データの分だけ増えていくため、一度に解析はしないで再生に必要な部分だけ、上から順に取り出すような処理になっているはずです。
で、上から順に再生に必要な分だけ取り出していると、Cue
の存在に誰も気付かないわけです。
Cluster
より先頭にあれば気付く事ができますが、いかんせんCluster
の数が多いので最後では気付かないでしょう。
で、最後にCue
ありますよと教えてあげるのがSeekHead
要素です。
これにCue
が一番最後にあると書いておけば、再生の際にファイルの最後の方にジャンプしてCue
を解析する事ができるわけです。
これもJavaScript
のMediaRecorder
が吐き出したWebM
にはありません。
Cue を先頭に持っていけば良いのでは?
コレをする方法もあるとは思いますが、圧倒的にCue
を後にするのが楽だと思います(あんまりバイナリ操作慣れてないし)。
というのも、Cue
のサイズを予測する必要があるわけですね。
すでに動画ファイルが存在して変換する場合ならまあ予測も出来ると思いますが、
撮影中の場合はどれだけ必要になるか見当もつかないため、Cue
最後が楽だと思います。
また、Cue
はシークの目印なので、Cluster
の位置がもちろん必要なのですが、
そのCluster
はCue
よりも後に来るため、位置計算の際に絶賛書き込み中のCue
自身のサイズまで予測する必要があり、これも大変だと思います。
しかもCue
が増えていくたびに全てのCluster
の位置を更新する必要があり、結構めんどくさい、考えたくない。
長い!はよ作れ!
作ります。
今回はJavaScript
のMediaRecorder
というわけで、やっぱブラウザで動くようにしたいですよね。
ですが、、、バイナリ操作にあんまり慣れてない。。。のでJavaScript
でもTypeScript
でも無く、いつも書いてるKotlin
で行かせてください。
JS / TS
で書ければ良いのですがちょっと自信無いかなあ、慣れてる(?)言語で行かせて欲しい。
Kotlin
で書いて、Kotlin/JS
でビルドすればブラウザで動くのでそれで許して~~~
(JVM
以外でもKotlin
のコード動かせます。もちろん動かすためにはJava
の機能を使わないようにする必要がありますが。)
(java.net.HttpUrlConnection
とかjava.io.File
とか使ったら無理。逆にKotlin
の標準ライブラリだけで書かれていればJS
でも動くかもしれない。)
あ、とりあえずはJava
で作って動かして、問題なければKotlin/JS
にコピーしようと思います。
環境
なまえ | あたい |
---|
Intellij IDEA | 2023.2.2 (Community Edition) |
Java | Eclipse Adoptium JDK 17 |
Kotlin | 1.9.0 |
2/10/16 進数変換出来る電卓があると良いです
(Windows
に最初から入ってる電卓をプログラマーモードにすれば良いです)
ID 一覧を用意する
はい。
それぞれ子要素を持っているか、子要素の場合は親要素のID、を持たせてみました。
https://www.matroska.org/technical/elements.html
要素を表現するクラス
EBML
で収納されたそれぞれのデータを表すクラスです。
ID
と、Data
の中身ですね。
EBML や VINT をよしなにやる拡張関数
よく使う関数は拡張関数として生やしておきましょう。
前回のVINT
周りは慣れないビット演算を使ったせいで、なんだかよく分からないけど動いているコードが誕生したので、
→ https://takusan.negitoro.dev/posts/video_webm_spec/#id
今回はVINT
とサイズの対応表とかを用意することでビット演算を少し回避しました。
あ、あとあんまり見かけたことがないのですが、Kotlin
で*
(アスタリスク)を使うと配列の中身を展開してくれます。
JavaScript
だとスプレッド構文とか言うやつですね。JavaScript
のそれと同じだと思います。
別に無くても、byteArrayOf() + byteArrayOf()
みたく+
で繋いでいけばいいんですけど、*
のが見やすそう。
詳しくはそれぞれの関数のコメントを見て下さい。
MatroskaExtension.kt
WebM をパースする処理
まずはWebM
の中身からそれぞれの要素を出そうと思います。
Info
とかSimpleBlock
とかを取り出していきます。
長さ不明の場合はサイズを求めるようにしていますが、これはSegment
とCluster
のみで、それ以外で長さ不明を使われたら例外になると思います。
(めんどいのとJavaScript
のMediaRecorder
はコレ以外の要素に関してはちゃんとサイズを入れているので)
ちなみにCluster
の場合は次のCluster
に当たるまで DataSize を取り出し続けます。Segment
は解析できなくなるまでかな。
ちなみにこれ計算リソースの無駄遣いと言われたらそれはそうだと思う(長さ不明の解析のために一回上から舐めて、長さが分かったら今度要素のパースのためにまた舐める)
とりあえず動くか試します。
とりあえずJava
で動くKotlin
で書いてからKotlin/JS
で動くようにしようと思うので、
こんな感じに書いて、適当なところにwebm
ファイルを置いて、ファイルパスを直してください。
あんまり関係ないですが、Windows
でファイルパスのコピーはファイルを選んでShift + 右クリック
するとコンテキストメニューにパスのコピー
というメニューが出てきます。それを押せば簡単にできます。
何故かShiftキー
を押しながら右クリックしないと出てきません。
適当に動かしてみました、
println
でWebM
の中身が出力されていれば成功です!!
WebM をシークできるように組み立て直す
WebM
をそれぞれの要素に分解出来たので、シークのために必要な要素を追加して、WebM
を完成させます。
やるべきことは上の方で説明した通りで、
- 動画の時間を入れる
Cue
をCluster
の後に入れる
Info
、Tracks
、Cue
の開始位置を宣言するSeekHead
を入れる
というわけで、それらをやる処理です。
めんどくさくなったので詳しくはコメント見てください。
あとはこれをparseWebm
の返り値と組み合わせて使えば動きます。
適当にダウンロードフォルダにでも保存するようにしましょう。
どうでしょう?
シークバー、進んでますか?シークも出来ますか?
ブラウザでももちろん見れます。やった~~~
mkvtoolnix
で見てみます。こんな感じ
SeekHead
、Cue
がそれぞれありますね。
また、要素を入れ直している関係で、長さ不明だったDataSize
が確定していますね。パーサーに優しくなった
動画の時間だけ入れればシークできるかも(プレイヤー次第、多分良くない)
動画の長さ、Duration
さえ入っていればシークできるプレイヤー実装があるらしいです。
が、まちまちなのでやっぱり Cue
や SeekHead
も入れないとダメなはずです。
- 動画の長さが無くてもシークできる
- なんで・・・?出来るの(先読みしてるの?)
- Firefox
- 動画の長さがあればシークできる
- 動画時間と再生位置を元に Cluster の位置を探すんですかね
- Chrome
- VLC(Windows / Android)
- 動画の長さ + Cue + SeekHead? があって初めてシークできる
- 大半がこっちのはず。
- シークバーは進むけど、シークしようとすると戻される、そもそもシークバーのつまみの部分が表示されないなど。
- Windows の動画プレイヤー
- Android の Google フォト / Files by Google
- Android の media3(動画再生ライブラリ)
ここまでのソースコード
どうぞ
多分最新の IDEA で動くはず。
https://github.com/takusan23/FixSeekableWebmKotlin
Kotlin/JS で使う
JavaScript
のMediaRecorder
で出来るWebM
をシーク出来るようにするためにわざわざIDEA
起動するのは面倒!
なんなら画面録画からシーク可能に修正までブラウザで出来ると良いのでKotlin/JS
で動かします!
Kotlin/JS プロジェクトを作る
セットアップ方法がなんか難しくなった?
前はKotlin/JS
すぐ使えた気がするんだけど
公式:
https://kotlinlang.org/docs/js-project-setup.html
うーん、前は新規プロジェクト作成でKotlin/JS
単品が選べた気がするんですけど、、今見るとKotlin Multiplatform
でしか作れない?
わかんない・・
とりあえずはKotlin/JVM
なプロジェクトを作った後に、Kotlin/JS
で動くように直そうと思います。
てな感じでNew Project
で作っていきます。Gradle
はbuild.gradle.kts
の方にします。(ほんとに無いの?)
次にbuild.gradle.kts
を開いて、Kotlin/JS
用に直します。
jvm
用の設定が消えた
多分コピペ直後は赤くなってるので、Gradle Sync
しましょう。
これです
ここに置けば良いって書いてあるのでそうします。
https://kotlinlang.org/docs/running-kotlin-js.html
src
を右クリックして、ディレクトリを作ります。
jsMain/kotlin
です。
出来たらsrc/jsMain/kotlin
へ移動して、App.kt
を作ります。
適当にconsole.log
するコードを置いておきます。
そしたらもうjvm
の方はいらないので、この2つは消して良いはず。
次にindex.html
を置きます。
さっきと同じ用にディレクトリを作る画面を出して、今度はjsMain/resources
を選びます。
そしたらsrc/jsMain/resources
にindex.html
を作ります。
index.html
の中身はそれぞれ調整しないといけないので注意してね。
調整しないといけない項目は、
<title>
<script>
のsrc
属性の値
{こ↑こ↓}.js
のここの名前は、プロジェクト名にする必要があります。
- 多分
build.gradle.kts
のwebpack
の設定で好きに変えられるとは思います
ここまで出来たら実行できます。やってみましょう。
実行(ホットリロードあり)
https://kotlinlang.org/docs/running-kotlin-js.html#run-the-browser-target
https://kotlinlang.org/docs/dev-server-continuous-compilation.html
ホットリロード付きです。
Web
界隈の開発では当たり前なのかReact Native
とかでもホットリロードされててすごいと思った。
Gradle
のコマンドを実行できるExecute Gradle Task
を開きます。
IDEA
の右側に🐘さんのマークがあるはずなので押してこれ押す。
もし見つけられなかったら、View
からTool Windows
からGradle
を押してもいいです。
そしたら以下を打ち込んで実行です!
どうでしょう?勝手にブラウザが開いてlocalhost:8080
が開くはず?
index.html
に何も無いので真っ白ですが、F12
を押してConsole
を押すと・・・
ありました!Hello
!
多分ちゃんとホットリロードになってるはず。
さっき作った WebM をシークできるようにするコードを持ってくる
ちなみにKotlin/JVM
で動かしてたものがKotlin/JS
で何で動かせるんやって話ですが、
Java
の機能を使ってないからなんですよね(さっきも話した気が
確かにMain.kt
ではimport java.xxxx
でJava
の機能を呼び出していますが、
それ以外のファイルではimport java.xxx
等は出てきていません。またKotlin/JVM
でしか動かない拡張関数も使っていないので、そのままコピペできるわけです。
というわけで持ってきました。
画面を作る(html を書く)
適当に画面を作ります。
最低限必要なのは、.webm
ファイルを選ばせる<input type="file">
ですね。
久しぶりにgetElementById
とかを使うな、、、Kotlin/JS
から触る要素にはid
を付けています。
index.html
つくりました、CSS
何も分からん、、
Kotlin/JS を書く
Kotlin/JS
なので、もちろんDOM
操作や、alert
、fetch
なんかも出来ます。
JS
のAPI
が無理やりKotlin
に来た感じなので型がところどころdynamic
になってますね。
で、まずは選んだファイルを取り出す処理ですね。
https://developer.mozilla.org/ja/docs/Web/API/File_API/Using_files_from_web_applications
App.kt
これでとりあえずWebM
のロード処理ができました。
適当に選ぶとロード完了
って出るはず
WebM をシーク可能にする処理
JavaScript
のFileReader
はバイト配列の表現にArrayBuffer
を使います。JS
のですね。
一方Kotlin
はバイト配列の表現にByteArray
を使うので、変換しないとさっき書いたWebM
をシーク可能にする処理が使えないです。
で既に同じような人がいたのでそれに乗っかります。
https://youtrack.jetbrains.com/issue/KT-30098
後はさっきのロード完了のあとに書き足していけば良いはず。
というわけでパースして修正して組み立て直してダウンロードまで書きました。やった~
ダウンロードはこの辺参考にしました
https://javascript.keicode.com/newjs/download-files.php
どうでしょうか?
ちゃんとシークできるWebM
がダウンロード出来ましたか?
うおおおおお~
ブラウザで録画して、ブラウザでシークできるWebM
が出来るようになった!!!
Firefox
でも動いた
公開する
index.html
とKotlin/JS
のjs
ファイルはGradle
のBuild
で出来るはず?
ドキュメント見たけど無さそうでよくわからない。とりあえず出来ているので良いのかな?
もしくは
保存先はここのはず。
インターネットで公開したい!!
ホスティングする(インターネットへ公開する)
静的サイトなので、適当な静的サイトホスティングサービスにアップロードすれば誰でもアクセスして使えるはずです。
でも静的サイト生成にJava
が必要なことってあんまり無いと思うから、そのホスティングサービスのビルド環境に入ってるか分からなくて調べたけど、Netlify / Cloudflare Pages
には入ってそう。
使う際は
Build command
はchmod +x ./gradlew && ./gradlew build
にします。
Build output directory
は/build/dist/js/productionExecutable
です。
今回は疲れたのでデプロイの部分は省略します。
最近はなんとなくAmazon S3 + CloudFront
がマイブーム(?)なのでそこに上げます。
詳しくは前書いたからそっち見て、ビルドコマンドとかが違うだけで使い回せると思う:
https://takusan.negitoro.dev/posts/aws_sitatic_site_hosting/
完成品(再掲)
https://webm.negitoro.dev
ソースコード
- Java の方
- JavaScript の方(ブラウザで動く方)
おまけ
せっかくなので画面録画機能も合体させました。
JavaScript
で出来た画面録画のコードをそのままKotlin/JS
で呼べるようにしただけです。
Kotlin/JS
の定義にMediaRecorder
が無くてなんかJavaScript
コードを埋め込む感じになっちゃった。
おわりに
Kotlin/JS
良いですね。プロジェクトの作り方分からんのだけはどうにかして欲しい。
JavaScript
でバイナリ操作するのやったこと無いからKotlin
で書いてみたけど、こういう使い方が良いのかな。あと既にKotlin
のコードがあってブラウザで動かしたいとか?
MediaRecorder
みたいにJavaScript
全部のAPI
があるわけじゃないらしい(?)ので気をつけないといけない。。。
ちなみに、js()
の文字列は定数である必要があるので、変数埋め込みとか使うと怒られる。
が、が、が、展開されるので、名前があっていれば変数等にそのままアクセスできます。
もう疲れたのでやり残したこととか
メモリ的によろしくないかも
説明難しいから擬似コードで書きますが、、
こんな感じに、Cluster
も(他の親要素もですが)ByteArray
を持っているんですよね。
このByteArray
、子要素全体のByteArray
になっているので、この後に続く子要素分も足すとメモリが二倍消費になっているはずです。
同じデータ(ByteArray
)をメモリに2つも持ってるのでめっちゃよろしく無い実装かもしれないです。
Chrome の作った WebM は途中でぶった斬る?
DataSize
の大きさだけByteArray
を取り出そうとすると足りない時がある(私のパーサーの実装ミスかも)
とりあえず次の要素のパースの前に後続が 3 バイト以上あるか確認するようにしました。
(ID
+DataSize
+Data
でそれぞれ最低 1 バイトずつ使えば 3 バイトになるはず)
参考にしました
ありがとうございます!!!