ReactでRuffleを使ってFlashを再生する

# 使いどころ

遺産 Flash ファイル（*.swf）を React などの SPA で使いたいとき！
当然、Flash Player も頼れない。
React + Ruffle + *.swf または、フロントWebフレームワーク + Ruffle + *.swf

# 公開に至るまで

Adobe が Flash Player のサポートを完全終了してから久しいですね。
あれは、2020年12月31日の出来事でした…。

遺産 Flash ファイル（*.swf）を再生する方法は、未だに需要があります。
動画内にクリックできる部分が必要だったりして、
単純にMP4とかに動画にすれば済むわけでもありません。
かといって、HTML5+CSS3アニメーションで１から作るのも面倒です。

そのため [Ruffle](https://ruffle.rs/)とか、[swf2js](https://swf2js.com/) を検証してみました。
静的ページなら　[Ruffle の公式ドキュメント](https://ruffle.rs/)を見れば簡単です。
しかし動的ページや SPA だと、そうはいきません。

- URL が切り替わると再生されない！
- Rust から自分でコンパイルすればいいらしい？
- wasm-loader が要るらしいけど、webpack なの？ create-react-app なの？

みたいな感じで大混乱です。

再生する方法を模索していて、かなり簡単な解決策を見つけたので紹介します。

# 方法

SPA内コンテンツが描画されるとき、動的にカスタムイベントを発生させます。
そのカスタムイベント内で、Flash の再生を開始させます。

キーワードは、下記４つです。

- `componentDidMount`
- `dispatchEvent`
- `addEventListener`
- `CustomEvent`

※後述する例は　React + TypeScriptです。しかし React + JavaScript や他のフロント Web フレームワーク構成でも、少しの書き換えで済むと思います。

※`ruffle.js`内で`*.wasm`ファイルを読み込みます。wasmのサイズが大きいため、これを最適化したい人には不向きな方法です。そういう時はRustからコンパイルしたほうが良さそう。

※この方法は[Ruffle の公式ドキュメント](https://ruffle.rs/)に書かれている、Website Self Hosted の延長線上にある手法です。Rust開発環境は不要です。

# 構成


```
┣━public
┃    ┣━ flash
┃    ┃    ┗━ flash
┃    ┃        ┗━ legacy.swf
┃    ┗━ ruffle
┃         ┗━ ruffle-nightly-2021_05_17-web-selfhosted
┃             ┣━ ???.wasm
┃             ┣━ ...
┃             ┗━ ruffle.js
┗━src
    ┣━ components
    ┃    ┣━ fixed_mainnav.tsx
    ┃    ┣━ header.tsx
    ┃    ┣━ main.tsx
    ┃    ┗━ maincontent.tsx
    ┣━ index.html
    ┗━ index.tsx
```
`maincontent.tsx`内の JSX が読み込まれたときに、毎回`legacy.swf`を再生します。

# コード

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
  <title>React App</title>
  <script src="%PUBLIC_URL%/ruffle/ruffle-nightly-2021_05_17-web-selfhosted/ruffle.js"></script>
</head>
<body>
  <div id="root"></div>
</body>
<script>
  var config = {
    // Options affecting the whole page
    "publicPath": "Failed to load the video!",
    "polyfills": true,
    // Options affecting files only
    "autoplay": "true",
    "unmuteOverlay": "hidden",
    "backgroundColor": null,
    "letterbox": "fullscreen",
    "warnOnUnsupportedContent": true,
    "contextMenu": false,
    "upgradeToHttps": window.location.protocol === "https:",
    "logLevel": "error",
  };

  function loadflash(elementId,flashUrl) {
    const ruffle = window.RufflePlayer.newest();
    const player = ruffle.createPlayer();
    const container = document.getElementById(elementId);
    container.appendChild(player);
    player.config = config;
    player.load(flashUrl);
  }
  function loadflash001() {
    loadflash('flash001','%PUBLIC_URL%/flash/legacy.swf');
  }
  window.addEventListener("playflash", loadflash001);
  window.addEventListener("load", loadflash001);
</script>

</html>
```

`head`タグ内で、下記のように、`ruffle.js`を読み込んでいます。

```tsx
  <script src="%PUBLIC_URL%/ruffle/ruffle-nightly-2021_05_17-web-selfhosted/ruffle.js"></script>
```

`loadflash`が Flash を再生させる関数です。

最終的に`load`イベントと`playflash`イベントで Flash を再生させようとしています。
なぜ`load`のほうに登録が必要かと言うと、URL 直打ちで`playflash`イベントが発生できなかったからです。
※この辺り細かく検証できていなくて、分かる方いたらコメントいただけると嬉しいです。

直接`loadflash`を`addEventListener`してみましたが、
`load`イベント時に`playflash`イベントも同時発生することが分かりました。
そのため、`loadflash001`関数を使って、あえて２段編成にしています。

```jsx:index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.sass';
import reportWebVitals from './reportWebVitals';
import MainApp from "./components/main";
import HeaderApp from "./components/header";
import FooterApp from "./components/footer";
import { createBrowserHistory } from "history"
import { Router } from 'react-router';

const history = createBrowserHistory({ basename: '/new_web' });
ReactDOM.render(
  <React.StrictMode>
    <Router history = {history}>
      <HeaderApp></HeaderApp>
      <MainApp></MainApp>
      <FooterApp></FooterApp>
      </Router>
  </React.StrictMode>,
  document.getElementById('root')
);
reportWebVitals(console.log);
```

```tsx:main.tsx
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import OutlineApp from './outline';
import MainContentApp from './maincontent';

class MainApp extends React.Component {
    render() {
        return (
            <main id="wrap_main">
                <div id="sidearea"></div>
                <div id="wrap_contents">
                    <Switch>
                        <Route exact path="/" component={MainContentApp} />
                        <Route exact path="/outline/" component={OutlineApp} />
                        <Route render={() => <p>NotFound</p>} />
                    </Switch>
                </div>
                <div id="main-right-spacer"></div>
            </main >
        );
    }
}
export default MainApp;
```

上記の`main.tsx`の中で、

```tsx
<Route exact path="/" component={MainContentApp} />
<Route exact path="/outline/" component={OutlineApp} />
```

という部分があります。ここで、URL が`/`に遷移した時には、`MainContentApp`が読み込まれます。
そこには、Flash を再生させたいタグ`id='flash001'`があります。

しかし、URL が`/outline/`に遷移した時には、`MainContentApp`の代わりに`OutlineApp`が読み込まれます。
そのため、タグ`id='flash001'`は一度無くなります。

その後、再びURL が`/`に遷移すると、もう一度`MainContentApp`が読み込まれます。
タグ`id='flash001'`が、再度出現することになります。

この時、`window.addEventListener("load", func)`だけだと、Flash は再生されません。
`load`イベントは、初めて DOM を読み込んだ時にしか発生しないからです。

```tsx:maincontent.tsx
import React from 'react';
let FlashEvent = new CustomEvent('playflash', { bubbles: true });

class MainContentApp extends React.Component {
    componentDidMount() {
        window.dispatchEvent(FlashEvent);
    }
    render() {
        return (
            <React.StrictMode>
                <div id="flash001"></div>
            </React.StrictMode>
        );
    }
}
export default MainContentApp;
```

上記`maincontent.tsx`に`<div id="flash001"></div>`があります。ここにFlashを読み込みます。

また、`maincontent.tsx`では２行目で、下記のように`playflash`イベントを宣言しています。

```tsx
let FlashEvent = new CustomEvent('playflash', { bubbles: true });
```

そして、この`MainContentApp`コンポーネントがマウントされると、

```tsx
    componentDidMount() {
        window.dispatchEvent(FlashEvent);
    }
```

によって、`FlashEvent`がディスパッチされます。
これにより、`index.html`で用意した、`playflash`のイベントリスナーがキックされます。

```tsx:header.tsx
import React from 'react';
import { NavLink } from 'react-router-dom';
import FixedMainNavApp from "./fixed_mainnav";

class HeaderApp extends React.Component {
    render() {
        return (
            <React.StrictMode>
                <header id="wrap_header">
                    <div id="wrap_logo">
                        <a href="/">
                            <img id="mainlogo" alt="株式会社logo" />
                        </a>
                    </div>
                    <nav id="wrap_gmenu">
                        <ul>
                            <FixedMainNavApp />
                        </ul>
                    </nav>
                </header>
            </React.StrictMode>
        );
    }
}
export default HeaderApp;
```

上記の`header.tsx`では、`FixedMainNavApp`でナビゲーションのリンクを読み込んでいます。

```tsx:fixed_mainnav.tsx
import React from 'react';
import { Link } from 'react-router-dom';

class FixedMainNavApp extends React.Component {
    render() {
        return (
            <React.StrictMode>
                    <li><Link to="/">ホーム</Link></li>
                    <li><Link to="/outline/">会社概要</Link></li>
                    <li><Link to="/digital/">デジタル事業</Link></li>
                    <li><Link to="/recruit/">リクルート</Link></li>
                    <li><Link to="/sitemap">サイトマップ</Link></li>
            </React.StrictMode>
        );
    }
}
export default FixedMainNavApp;
```

上記の`fixed_mainnav.tsx`内で`Link`タグをユーザーがクリックすると URL 遷移します。

# まとめ

負の遺産は早く捨てたほうが幸せ。
**Excelsior!**