[Astro] #192 Ruffle をセルフホストして作るブラウザ版 Flash プレイヤー「SWF Reel」の実装
はじめに
Flash の SWF ファイルを、ブラウザの中で再生するツールを作りました。
再生エンジンは自作ではなく、Rust 製のオープンソースのエミュレーター Ruffle を自分のサイトに置いて使っています。この記事は、その包み方(読み込み・設定・出力)の実装をまとめたものです。
- 再生: SWF のドロップ(複数可)、一覧での切り替え、一時停止、全画面、音量
- 設定: 拡大方法、画質、描画方式、リンクの扱い、ネットワーク、背景色
- 出力: スクリーンショット(PNG)、録画(WebM、音なし)
- 調査: ファイル情報、ActionScript の
trace()出力 - ファイルはブラウザの中で再生し、サイトへは送りません(SWF 自身が外へ通信する作りの場合を除く)
GitHub - ruffle-rs/ruffle: A Flash Player emulator written in Rust
A Flash Player emulator written in Rust. Contribute to ruffle-rs/ruffle development by creating an account on GitHub.
github.comスクリーンショット
動画
サンプル
サンプルデータは以下のサイトよりお借りしています。
Download .SWF Sample Files - Download Sample Files
Download free SWF sample files from 360p to 1440p with embedded Sorenson Spark video and MP3 audio, plus file details and conversion tips now that Flash is gone.
www.samplefiles.org構成
| パス | 中身 |
|---|---|
src/pages/swf-reel.astro | 画面(3 ペイン) |
public/tool/swf-reel/swf-reel.js | アプリ本体(素の JS、1 ファイル) |
public/tool/swf-reel/ruffle/ | Ruffle の selfhosted 一式 |
Ruffle は npm の @ruffle-rs/ruffle(0.7.1、MIT OR Apache-2.0)から取得しました。npm pack で展開すると、次のファイルが入っています。
ruffle.js(約 466KB)core.ruffle.*.js2 本*.wasm2 本(各 約 14MB)LICENSE_MIT/LICENSE_APACHE
.map は置いていません。WASM が 2 本あるため、リポジトリは約 29MB 増えます。1 ファイルずつは Cloudflare Pages の 25MiB の上限の下です。更新するときは、ruffle/ を丸ごと置き換えます。
名前は、Ruffle が別プロジェクトの名前なので使わず、「SWF Reel」にしました。画面のフッターとヘルプに、Ruffle で再生していること・ライセンス・Ruffle プロジェクトとは関係がないことを書いています。
Ruffle の読み込み
ruffle.js を <script> で直接書かず、swf-reel.js から、最初のファイルが来たときに読み込みます。WASM が約 14MB あるので、使わない人には読ませないためです。
window.RufflePlayer = window.RufflePlayer || {};
window.RufflePlayer.config = Object.assign({
publicPath: '/tool/swf-reel/ruffle/',
polyfills: false, // ページ内の <embed>/<object> を書き換えない
logLevel: 'warn',
splashScreen: false,
unmuteOverlay: 'hidden'
}, window.RufflePlayer.config || {});
// ruffle.js を <script> として足して、onload を待つ
const api = window.RufflePlayer.newest();
publicPathは、core.ruffle.*.jsと*.wasmを探す場所です。末尾の/を付けています。- 読み込み方は
import()ではなく、<script>を足す形です。/publicの JS をimport()すると、Vite の dev サーバーが拒否するためです。 - WASM は 2 本ありますが、どちらを読むかは Ruffle が決めます。
プレイヤーの作り方
SWF は File のまま持ち、再生するときに Blob URL にして渡します。ファイルを切り替えるたびに、プレイヤーの要素を作り直します。前の設定や状態が残らないようにするためです。
const url = URL.createObjectURL(item.file);
const p = api.createPlayer();
stage.appendChild(p);
await p.load({
url,
autoplay: 'off', // 後で play() する(理由は TRACE の節)
unmuteOverlay: 'hidden',
splashScreen: false,
warnOnUnsupportedContent: false,
allowFullscreen: true,
scale: S.scale,
forceScale: true, // SWF 自身の指定より、こちらの SCALE を優先
openUrlMode: S.url,
allowNetworking: S.net,
logLevel: 'warn',
// 以下は設定されたときだけ
// quality, preferredRenderer, backgroundColor
});
操作に使った Ruffle のプレイヤー要素の API は、load play pause isPlaying volume metadata loadedConfig enterFullscreen traceObserver です。suspend / resume は、このビルドにはありませんでした。
設定の値
| 設定 | 値 |
|---|---|
scale | showAll / noBorder / exactFit / noScale |
quality | low / medium / high / best(未設定で SWF の指定) |
preferredRenderer | webgpu / wgpu-webgl / canvas(未設定で自動) |
openUrlMode | allow / confirm / deny |
allowNetworking | all / internal / none |
openUrlMode の既定は allow です(confirm ではありません)。SWF が外のページを自由に開けてしまうので、このツールの初期値は confirm にしています。
設定を変えると、同じファイルを最初から読み込み直します(実行中のプレイヤーの設定は、後から変えられないため)。設定と音量は localStorage に保存します。
metadata は、読み込みのあとで埋まる
load() が解決した直後は、player.metadata が空です。ステージの大きさ・fps・フレーム数・SWF バージョン・AS3 かどうか・展開後のサイズは、少し後に入ります。情報欄は 100ms ごとに最大 15 秒、入るまで待ってから更新します。
function waitMetadata(p, token) {
return new Promise(resolve => {
let tries = 0;
(function poll() {
if (token !== loadToken) return resolve(null); // 別のファイルに切り替わった
const m = p.metadata;
if (m) return resolve(m);
if (++tries > 150) return resolve(null);
setTimeout(poll, 100);
})();
});
}
loadToken は、読み込みのたびに増やす番号です。ファイルを続けて切り替えたとき、古い読み込みの結果で画面を上書きしないために使っています。
TRACE(ActionScript の trace())
player.traceObserver に関数を入れると、trace() の出力が届きます。ただし、load() の前に設定しても効きません(インスタンスが無いため、無視されます)。load() の後に設定すると、最初のフレームの trace() を取りこぼす可能性があります。
そこで、autoplay: 'off' で読み込み、observer を設定してから play() する順にしています。
await p.load(config); // autoplay: 'off' なので、まだ止まっている
p.traceObserver = onTrace; // ここで設定
p.play();
出力は最大 2000 行まで持ち、描画は requestAnimationFrame でまとめます。
スクリーンショット
Ruffle の描画先は、プレイヤー要素の shadowRoot の中の <canvas> です。WebGL(wgpu-webgl)のとき、この canvas の中身はフレームが画面に出たあと消えるので、drawImage で読むと透明になることがあります。
対策は 2 つです。
requestAnimationFrameの中でコピーし、中身のあるフレームが取れるまで、最大 30 フレーム再試行する- 一時停止中は何度やっても取れないので、PAUSE を押した瞬間に 1 コマ保存しておき、停止中のスクリーンショットはそれを使う
function grabFrame(c, tries, cb) {
requestAnimationFrame(() => {
const tmp = document.createElement('canvas');
tmp.width = c.width; tmp.height = c.height;
const ctx = tmp.getContext('2d', { willReadFrequently: true });
ctx.drawImage(c, 0, 0);
// 5 点をサンプルして、すべて透明なら「空」
// 空なら tries を減らして再試行、0 になったら null を返す
});
}
30 フレーム試しても空なら、「RENDERER を Canvas 2D にして、もう一度試してください」と表示します。canvas 描画のときは、バッファが残るので普通に取れます。
録画
canvas.captureStream(60) と MediaRecorder を使います。形式は WebM で、video/webm;codecs=vp9 → vp8 → video/webm の順に、使えるものを選びます(8Mbps、1 秒ごとに区切って保存)。
音は入りません。 Ruffle の音は Web Audio の内側にあり、外から取り出せないためです。ボタンの名前も「REC (WebM, no audio)」にしています。
検証
ヘッドレスのブラウザ(Chromium 153、WebGL はソフトウェア描画)
手で作った最小の SWF(背景色だけのもの、trace() を 2 回出すもの、800×200、壊れたファイル)で、37 項目を確認しました。
- 再生した画面の中央の画素が、SWF の背景色になる
- 複数ファイルの一覧・前 / 次・削除・全部消して空に戻る
- ステージの大きさ・fps・AS の種類が表示される
trace()の 2 行が TRACE に出る- 一時停止の表示が切り替わり、音量が
player.volumeに入る - スクリーンショット(動作中・一時停止中とも、PNG の中身が SWF の色)
- 録画(WebM を保存し、
<video>で読み直して、1 秒後の画素が SWF の色) - 設定が
loadedConfigに入り、canvas描画でも描ける .swf以外のファイルを弾く、F1 / Esc、コンソールとページのエラーが 0 件- 外部ホストへの通信をすべて遮断した状態で、上のすべてが通る
実機(Windows の Chrome、Astro の dev サーバー)
sample-360p.swf(7.32MB、636×360、25fps、1345 フレーム、AS1/AS2)が再生され、情報欄も表示されました。描画は wgpu-webgl、WebAssembly 拡張が ON でした。
制限と未確認
- 外のファイルを読む SWF は、動かないことがあります。 ファイルを Blob URL で渡すので、相対パスで画像・XML・別の SWF を読むゲームは、読み込めません。
- ActionScript 3 の SWF は、部分的な対応です。 Ruffle 側の制限です。情報欄の ActionScript の欄で、AS1/AS2 と AS3 を見分けられます。
- ヘッダーに書かれた長さと、実際のファイルサイズが合わない(途中で切れた)ファイルは、Ruffle 自身のエラー画面が出ます。
- 未確認:
- 音(ヘッドレスでは確認できません)
- 実機でのスクリーンショットと録画
- ゲーム系の SWF(キー・マウス操作)
- AS3 の SWF
- Quest・スマホ・Safari・Firefox
- RENDERER を WebGPU にした場合
- 窓モード
- デプロイ先での動作
まとめ
- Ruffle は、
ruffle.jsと*.wasmを自分のサイトに置き、publicPathを指定すれば、そのまま動きます。 - 実装で注意した点は、次の 5 つです。
openUrlModeの既定はallowtraceObserverはload()の後でないと効かないmetadataは読み込みの後に埋まる- WebGL の canvas は、フレームの外では読めない
- 録画に音は入らない
- ファイルは、ブラウザの中で再生します。
- 再生エンジンは Ruffle(MIT OR Apache-2.0)です。Ruffle プロジェクトとは関係のない、個人の作品です。
GitHub - ruffle-rs/ruffle: A Flash Player emulator written in Rust
A Flash Player emulator written in Rust. Contribute to ruffle-rs/ruffle development by creating an account on GitHub.
github.com