Chienomi

LWMP 1.0.0リリース - localwebmediaplayer

開発::util

localwebmediaplayer、通称LWMPはついにv1.0.0となった。

このソフトウェアは7/24から急速に、本当に急速に開発され、main.jsに至っては実に2.5倍にもなった。

もともとは本当に簡素で、最低限のコードで目的を発生するという意識だった。 しかし、欲しい機能を追加している間に「こうしたほうが親切なのでは」と思い始め、徐々に自分以外の環境も考慮するコードが増えていった。

結果的にかなりきれいに整うところまできてしまったし、使い勝手も良い。 きっと需要もかなりあるだろうと思い、正式リリースに踏み切った。 これにより、LWMPはメンテナンスされるプロジェクトになる。

超急激な開発

1.0を出すに至るまでがどれほど急な坂道だったか。

これは、コミットの日付と、main.js, *.rb, theme.css, READMEの行数/文字数を見るとわかる。

コミット日 js js(c) rb rb(c) css css(c) read me
2024-11-02 214 5880 68 1488 100 1476 62 1821
2024-11-03 274 7545 101 2558 118 1693 69 2016
2024-11-04 290 7957 101 2558 140 1993 69 2016
2024-11-05 319 9134 104 2768 153 2151 69 2016
2025-07-08 330 9391 104 2768 166 2516 69 2016
2025-07-24 596 18148 112 3042 239 3590 79 2356
2025-07-25 716 21011 113 3069 239 3590 112 3609
2025-07-26 715 20961 113 3069 239 3590 129 4045
2025-07-29 715 20961 158 3943 239 3590 264 8713
2025-07-30 751 21921 165 4243 280 4287 264 8711
2025-08-01 804 23491 250 6444 322 4884 317 11008

これではピンとこないだろうから、グラフにしたものがこちら。

機能追加コミットごとの行数/文字数推移

そして、X軸を日付にするとこうなる

機能追加日付軸での行数/文字数推移

だいぶクレイジーである。

主要な機能で見るとこう:

コミット日 主な更新
2024-11-02 最初のリリース
2024-11-03 セーフティ機能とプレイリストの追加
2024-11-05 見た目関連のアップデート、初期パスを与えられるように。スクロール位置を覚えるようになった
2025-07-08 コミット数はそれなりにあるが、細かな調整やバグ修正が中心
2025-07-24 イメージビューワーを追加
2025-07-24 ブックリーダーを追加
2025-07-25 mediaSession系の機能とキーボード操作を追加
2025-07-29 起動方式を変更
2025-07-30 テキストビューワを追加
2025-08-01 音声/ビデオのメタデータ関連機能を追加

改めてLWMPとは

概要

ローカルネットワーク/ローカルサイト向けにPC上にあるファイルをウェブで配信するソフトウェアである。

基本的にLinuxシステム/Lighttpdを想定しており、ミニマル&エッセンシャルな作りになっている。 (WindowsではWSLの利用を想定。)

Vanilla JavaScript SPAを採用しており、ファイルブラウザから快適にメディアを探して再生することができる。

「PC上の音楽やビデオをベッドでスマートフォンで楽しみたい」という気持ちから生まれた。 これは、様々な標準的な方法を試した上で満足できなかったから生まれたのである。

主要な機能

ファイルブラウザ

ファイルブラウザは再生可能なデータのみを表示するため、ファイルマネージャ的な目的ではなく、あくまで再生したいファイルにアクセスするためにある。

マルチメディアプレイヤー

音楽とビデオを再生するプレイヤーである。 再生機能自体はaudio, video要素を利用しており、基本的にブラウザ任せ。

プレイリスト機能を持っており、次のトラック/前のトラックという概念がある。 単体再生とディレクトリ再生に対応。相対パスでアクセスできる範囲なら.m3uプレイリストもサポートされている。

カバーアートやmediaSessionメタデータにも対応している。

画像ビューワ

3ゾーンタップナビゲーションの画像ビューワ。 ディレクトリ内の画像を次々に見ることもできる。

スマートフォン上では電子書籍を読むときもこれでいい。

ブックリーダー

見開き表示、進行方向反転、ページジャンプ、1/2ページめくりに対応した画像ビューワ。 5ゾーンタップナビゲーションになっており、キーボード操作も可能。

キー 進行
左へ2ページめくる
右へ2ページめくる
2ページ進む
2ページ戻る
PageDown 1ページ進む
PageUp 1ページ戻る

テキストビューワ

テキストファイルとして認識できるものをtextareaで表示するシンプルな機能。 編集はできない。

プログラムのソースコードは対象外だが、.json.yamlなんかは読める。

ソフトウェアとしての特徴

  • シンプル&ミニマル
  • それでいて欲しい機能は結構ある
  • クライアント側環境は自由で、モバイルだけでなくSSHFSの使いにくいWindows環境でも便利
  • 非常に軽快
  • マルチインスタンスで、複数のディレクトリをそれぞれのポートで配信できる
  • ローカル環境向けの割り切り
  • DLsite Voice Utilsからの連携もある
  • サーバーは基本的にLinux環境向け (Lighttpdへの依存とファイルパスの取り扱いが主な理由)

ちなみに、私がGitHub上でReleasesを切った初めてのソフトウェアでもある。

技術的な特徴

  • Vanilla JavaScript SPAとLighttpdありきのCGIというミニマル構成
  • 表示/再生機能は基本的にブラウザのネイティブな機能を利用する
  • 外部ライブラリ1を必要としない
  • cgi.rbを使いつつJSON APIを解釈する割と珍しい構成
  • メディアファイルへのアクセスはアプリケーションは介入せずHTTPサーバー任せ
  • Lighttpdありきで設計されている

NextCloudと連携してインターネットでも使ってみる

LWMPが「ローカル」である理由

LWMPがlocal web media playerであるのは次のような理由だ。

  • 認証機能がない
  • マルチユーザーでもない。HTTPアクセスすれば誰でも同じコンテンツにアクセスできる
  • ファイルパス処理など、悪意あるアクセスに対抗できるほど十分に安全に設計されていない

これらの問題は利用者が自分ひとりで、なおかつアクセスに十分に安全な認証を必要とすれば解決する問題でもある。

簡単な方法としてはBasic認証、もしくはDegest認証が十分に安全だとみなせるのであればLighttpdで実施できるため、意外と難しくない。

私の場合、インターネット経由での配信をしたいケースというのは基本的にスマートフォンからアクセスしたい場合であり、端末も固定なので、今回はmTLSを試してみる。

mTLSする

まずはルート証明書を作る

openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 -out ca.crt
openssl genrsa -out client.key 2048

中間鍵をつくる

openssl genrsa -out int_ca.key 4096
openssl req -new -key int_ca.key -out int_ca.csr 
openssl x509 -req -in int_ca.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out int_ca.crt -days 365 -sha256

クライアント証明書をつくる

openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr
openssl x509 -req -in client.csr -CA int_ca.crt -CAkey int_ca.key -CAcreateserial -out client.crt -days 365 -sha256
openssl pkcs12 -export -out client.p12 -inkey client.key -in client.crt -certfile int_ca.crt

CAファイルを配置し、Nginxに設定を入れてリロードしておく

ssl_client_certificate /path/to/cert/ca.crt;
ssl_verify_client on;

あとはモバイル端末にclient.p12を転送して開くだけ。

なのだが、Find X2 ProだとAESな証明書がインポートできない上に、インポートしたところでmTLSで証明書を送信しなかった。 X6のほうではうまく動作したので目的は達成したと言えなくはないが、PC側でもうまく動作せず、全体的にいまいちだった。

NextCloudとの連携

NextCloudはファイル自体は普通にファイルシステムに配置し、インデックス等はMySQLに持っている。 LWMPはファイルさえ見られればいいため、NextCloudのファイルを共有すれば良い。 場所は//volumes/data/$user/filesである。

NextCloudはボリューム外へのシンボリックリンクでは機能しないが、Lighttpdはルート外へのシンボリックリンクが機能する。 だからNextCloudのボリュームへのシンボリックリンクをmediaにすれば良いのだが、私のケースではパーミッション的に難しかったのでbind mountで解決した。

普通に/etc/fstabに書けば良いので、運用が固まっていれば簡単な解決方法。

LWMPの話題

普通にでかいJavaScript

ミニマルだと言っているが、1.0.0時点でJavaScriptは804行23491文字。 ミニマルの定義が行方不明である。

だが、実際見てみるとコード自体はessentialなので、減らそうとすると何かを犠牲にしなくてはいけない。 例えば、テキストビューワを表示するshow_textview()

const show_textview = async function(path) {
  const box = document.getElementById("TextViewerBox")
  const area = document.getElementById("TextViewer")
  box.style.height = window.innerHeight + "px"
  box.style.width = window.innerWidth + "px"
  const body = await http.get(("/media/" + path), {disable_parse_json: true})
  area.value = body
  box.style.display = "grid"
  currentState.currentView = "textview"
}

となっている。

ちなみに、例外はキャッチしていない。 http.get()が例外を発生したとして、伝播する先もないので処理が中断される。 get()する前に行ったのは非表示の要素のheight/width調整だけなので特に問題はない。 こんな感じで相当essentialに作ってある。

ただ、もともとは微妙なところがあっても使う分には支障なければOKとしていた。 しかし、そのあたりは1.0.0に至るまでに心配りをするように変更されてきており、それによってコード量は5〜10%程度増加している。

例えば、Loading Splashはロード中であることがわかるようにするもので、本質的には必要ない。 しかしあったほうが親切だ。 そして一般的にそう長くはないロード中にウィンドウリサイズする人はまずいないと思うし、したとしたら表示がおかしくなってもなぜおかしくなったのかはわかると思うが、一応resizeイベントに追従してresizeするようにしている。

CGI

CGIというのが非合理的に思える人もいるかもしれないが、実のところCGIであることは結構重要である。

まず、LWMPの構造としては//media/の2つのルートを持っている。 そして、これらの大部分は静的ファイルの配信を行う。

アプリケーションサーバー(例えばPuma+Rack)に静的ファイル配信をやらせると、効率や機能の面で問題が出やすい。 基本的にこういうのは、それを得意とする汎用のウェブサーバーにやらせるほうが良い。

そして相対的にアプリケーションの役割が軽い。 アプリケーション要素は現状「ディレクトリのアイテムリストの取得」と「メタデータの取得」に限られている。 頻度は高くない上に自分一人しか使わないものなので、トラフィックを憂慮する必要はない。

汎用ウェブサーバーとアプリケーションサーバーの2つにしてしまうと、ユーザーが起動すべきサーバーが2つになる。 ホストレベルでウェブサーバーを立てるような構造にしてしまうと一気に取り回しが悪くなるし、より手軽な方向に倒すならひとつにしたい。

bundlerを使うようになるとハードルも上がるし手間も増える。 CGIにしておけば「起動するサーバーは1つでいい」「ライブラリの導入などの手順が不要」といったメリットがある。 少なくともcgi.rbが標準ライブラリである間はこの構造のメリットが大きい。 ちなみに、cgi.rbがgem化されたら、それはもうcgi.rbの存在価値が失われるときだと思って良いだろう。

LWMPの歩み

  • 「PC上のメディアファイルをベッドでスマートフォンで楽しみたい」と考えた
  • 最初はAndroidでSSHFSを使う方法を考えたが、うまくいかなかった
  • DLNAを試したが、あまりうまく行かなかった
  • VLCを試したが、うまく行かなかった
  • HTTPで配信し、mpvで再生する方式を試したが、あまりうまく行かなかった
  • ウェブサーバー+ディレクトリ表示機能に頼ってみたが、快適ではなかった
  • もはや自作するしかない、ということでLWMPを作り始める
  • ファイルブラウザ, メディアプレイヤー, プレイリストを持つLWMPが誕生
  • .m3uプレイリストのサポートを追加。プレイリストがオーディオ/ビデオ混合を許すようになった
  • プレイリストのnext/prevが可能に
  • スマートフォン向けのUIを強化
  • ファイルブラウザはディレクトリごとにスクロール位置を記憶するように
  • ディレクトリ単位の再生のみカバーアートをサポート
  • クエリパラメータで起動時の初期パスを指定できるようになった
  • 「電子書籍も読みたい」と思うようになった。NextCloudだといまいち
  • 手始めにイメージビューワを追加
  • 大掛かりに手を書けてブックリーダーを追加
  • ちゃんとドキュメントを整備し始めた
  • ブックリーダーでキーボード操作できないのがストレスなので、MComix風のショートカットを追加
  • 併せて、nexttrack/previoustrackにも対応
  • ついバックナビゲーションしがちなので、バックナビゲーションが動作するように改良
  • ユーザー向けのやさしい機能としてスタートアップスクリプトを追加
  • MEGAではコピペ用テキストをモバイル用に共有していたりしたので、テキストビューワを追加
  • 1.0.0に向けて整備

  1. 正確にはfetchwrapperを使っているが、src以下に普通に置いているし、そもそも私が作ったものだし、文字通り標準のfetch()を使いやすくする関数を提供するだけなので、外部ライブラリとはまぁ言わないでいいと思う。metadata.rbgdbmを読むけれども、あれは(Rubyディストリビューションに含まれる)Default Gemsだから外部ライブラリではないと言うのは問題ないだろう。↩︎