【Mastodon】Next+ElectronでLT中のコメントが流れるツール作ってみた！

# 挨拶
どうも，CompositeComputerClubのアドベントカレンダー初日担当()の鳩屋敷です．**初日担当**です．
書きたいことが多すぎて複数記事に分けても良いボリュームを全部載せちゃいました！
興味あるサブタイトルだけでも良いので読んでいただけると幸いです．

# 作った経緯
私が所属しているサークルでは時々Discordで集まってLTを行うことがありました．しかし，ボイスチャットの性質上傍聴している人たちはみんなマイクミュートにしています．そのせいか，LTの盛り上がりをあまり感じられなかったり，登壇者が虚空に話しかけて寂しくなったりしていました．しかしその分，サークル内で運用しているSNS，Mastodonで感想を投げている人たちが多く見られました．
そこで，LTで配信してる画面の上にMastodonに流れるLTの感想が流れると面白いのではないかと思い，今回のデスクトップアプリを作ってみました．
![完成形イメージ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696356/1d95a60c-8b34-4a80-3bfc-9888ee3d2128.png)

# ダウンロードURL(v1.1.5)
Githubのリリースで公開しています．現在WindowsとLinuxに対応しています．
https://github.com/PigeonsHouse/NicoCommeDon/releases/tag/v1.1.5

最新版はこちら！↓
https://github.com/PigeonsHouse/NicoCommeDon/releases/latest

# 使い方(v1.1.5)
起動時の画面はこの様になっています．インスタンスURLに自分が所属しているMastodonのURLを入力して，認証コードを発行してください．ブラウザで認証ページが開くのでそれを許可してあげると認証コードが取得できます．貼り付けたらコメント画面に移動します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696356/ab19bfc2-d401-b0f5-8551-336a26c16292.png)
こちらがコメント画面です．ここからコメントの取得と背景映像の選択ができます．コメントを流してクロマキーで抜いて各自で重ねるも良し，背景映像にLT画面(パワポとか)を選択して上にコメントを流しても良し．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696356/950bed9f-3e57-5cc5-b4b3-ed66d75128bd.png)
コメントの取得は，Ctrl+Alt+P(公開タイムライン), Ctrl+Alt+U(ホームタイムライン), Ctrl+Alt+L(ローカルタイムライン)の3つのショートカットで選べます．Ctrl+Alt+Sで一応監視も停止できます．

↓流れるコメントの実例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696356/0ea3a920-7ef0-6271-338a-425f853c75a7.png)

Ctrl+Alt+Gのショートカットキーで背景映像の選択画面が出現します．Discordの画面共有みたいな感じで選べます．これで選択すると，GBだった部分が選択した画面共有の映像に切り替わります．
![画面共有.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696356/fca931ba-a6d4-690d-2a25-5c1019bda0eb.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696356/5761e67d-3aad-5f85-15ba-226d079ac5c6.png)

# 使用技術
- TypeScript
- Next.js
- Electron
    - Web技術を使用してデスクトップアプリが作成できるフレームワーク
- mastodon-api
    - 認証やストリーミングなどMastodonのAPIを叩くライブラリ
- electron-builder
    - Electronのアプリを.exeや.rpmなどの実行ファイル形式で出力するフレームワーク
- GithubActions
    - Github上のCI/CDサービス．GithubReleaseに自動でファイルをデプロイさせるために使用
- GithubRelease
    - 生成した実行ファイルを公開させるために使用

# 詰まったエピソード
## コメントの再現の話(技術メモ)
このサービスにおいて中核になるコメントの再現を行う際に詰まっていました．ニコニコ動画のコメントを再現するというサービス自体は結構あるので，その中でもブログとして残してあったソースコードを参考に実装していました．しかし色々と知識の浅かった頃の僕はコピペするしかできず，案の定動きませんでした．
その後先輩に相談した結果こういったCSSを提示していただきました．難しく考えていましたが結局`@keyframes`を読めばわかる通り結構簡単な実装となりました．
似たような実装で詰まってる人にこれが届くことを祈ります…

```css
.comment {
    margin-bottom: 0;
    margin-top: -35px;
    position: absolute;
    font-size: 24px;
    line-height: 1;
    z-index: 2;
    color: #fff;
    text-shadow: -1px -1px #000, 1px -1px #000,	-1px 1px #000, 1px 1px #000;
    overflow: hidden;
    white-space: nowrap;
    animation: 10s linear 0s 1 slide;
    animation-fill-mode: forwards;
}
@keyframes slide {
    0% {
        margin-left: 100%;
        transform: translateX(0%)
    }
    100%{
        margin-left: 0%;
        transform: translateX(-100%)
    }
}
```

## 画面共有の話(技術メモ)
元々NicoCommeDonはグリーンバック上にコメントが流れてGB透過で共有したい画面をOBS等で重ねて配信させるツールでした．使い勝手の悪さが半端ないですね．そこでDiscordのように，PC上に存在するウインドウやモニタ映像を選択して背景に置き，その手前にコメントが流れるようにしようとしました．`getUserMedia`のような関数を使用すれば画像が取得できる，というのは軽く調べてわかっていたのですが，その関数をどうやって呼び出すのか(windowオブジェクトみたいにJavaScriptの元から呼べる関数の一種だと後で知りました…)，WindowIDをどこで用意するのかが一切わからず，検索力もまだなかった頃だったので数ヶ月放置していました．
最終的に，画面選択画面で画面一覧のIDを取得しその配列をmapで選択肢のカードをレンダリングし，選んだ画面のIDを取得して`getUserMedia`に渡して背面に表示しました．
いかがソースコード(の抜粋)です．

```typescript:ウィンドウIDの取得
	const handleStream = (key, stream) => {
		let replaceKey = key.replaceAll(':', '\\:');
		let str = 'video#thumb_' + replaceKey;
		console.log(str);
		const video: HTMLVideoElement = document.querySelector(str);
		video.srcObject = stream
		video.onsuspend = () => {
			handleStream(key, null);
		};
		video.onloadedmetadata = (e) => {
			console.log(e)
			video.play()
		}
	}

	useEffect(() => {
		desktopCapturer.getSources({types: ['screen', 'window']}).then(async sources => {
			setSrcs(sources);
		});
	}, [])
```

```typescript:画面表示
	async function setVideo(message) {
		let windowId = message.data;
		const stream = await (navigator.mediaDevices as any).getUserMedia({
			audio: false,
			video: {
				mandatory: {
					chromeMediaSource: 'desktop',
					chromeMediaSourceId: windowId,
					minWidth: 1280,
					maxWidth: 1280,
					minHeight: 720,
					maxHeight: 720,
				},
			}
		});
		handleStream(stream);
		return;
	}
```

## Electronのビルドの話(布教話)
部内LTで感化されReactとElectronの勉強をするために本ツールを作成しました．しかし，Electronの開発環境自体は，調べれば情報も多く，すぐに導入できました．しかし，Electronで作成したものをデスクトップアプリのような実行ファイルとしてビルドしようとしたのですが，ビルド環境をうまく整えられませんでした…
そもそもReactを使用したElectronの実行ファイル化の情報が少なく，さらにElectronBuilderの導入に失敗していたのか環境が違うのかヒットした情報の手順でもうまく行きませんでした…
最終的にリポジトリを変えついでにNextJSに乗り換えてリメイクを行いました！ここでおすすめしたいのが，ビルド環境まで既に作ってある[Nextron](https://github.com/saltyshiomix/nextron)というフレームワークです！NextJSと若干フォルダ構成が異なりますが，このリポジトリから開始すると最初から`npm`コマンドや`yarn`コマンドで実行ファイルがビルドできます！
1からのElectronの導入がうまく行かなかったのは悔しかったですがこのフレームワークは結構おすすめです．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696356/08e8c394-6069-188d-3369-f699a97677a6.png)

## GithubActionsの構築の話(布教話)
これもまた部内LTで感化されて導入した技術だったのですが，CI/CDを導入しようとGithubActionsに挑戦してみました．CI/CDがよく用いられる用途としてはデプロイやテストが多いと(僕の主観ですが)思います．
が，今回は，Githubのリリース機能を使ってElectronのビルドした実行ファイルをリリースに公開してくれるWorkflowを作成してみました．mainブランチに来たプルリクを閉じるとElectronBuilderを走らせ，リリースを作成し，そこにビルドしたファイルを添付するWorkflowとなっています．

```yaml:deploy.yml
on:
  pull_request:
    types: 
      - closed
    branches:
      - main

jobs:
  create_release:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v2
    - name: Get package version
      id: get_package_version
      run: |
        package_version=`npx -c 'echo "$npm_package_version"'`
        echo "::set-output name=package_version::${package_version}"
    - name: Create release
      uses: actions/create-release@v1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag_name: v${{ steps.get_package_version.outputs.package_version }}
        release_name: NicoCommeDon_v${{ steps.get_package_version.outputs.package_version }}
        body: NicoCommeDon v${{ steps.get_package_version.outputs.package_version }}
        draft: false
        prerelease: false

  release_linux:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout
      uses: actions/checkout@v2
    - name: Use Node.js 14.x
      uses: actions/setup-node@v1
      with:
        node-version: 14.x
    - name: Build package
      run: |
        npm install -g yarn
        yarn install
        yarn build:linux
    - name: Get package version
      id: get_package_version
      run: |
        package_version=`npx -c 'echo "$npm_package_version"'`
        echo "::set-output name=package_version::${package_version}"
    - name: Get release
      id: get_release
      uses: cardinalby/git-get-release-action@v1.1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag: v${{ steps.get_package_version.outputs.package_version }}
    - name: Upload release .deb Asset
      uses: actions/upload-release-asset@v1.0.2
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.get_release.outputs.upload_url }}
        asset_path: ./dist/nico-comme-don_${{ steps.get_package_version.outputs.package_version }}_amd64.deb
        asset_name: nico-comme-don_${{ steps.get_package_version.outputs.package_version }}_amd64.deb
        asset_content_type: application/vnd.debian.binary-package
    - name: Upload release .rpm Asset
      uses: actions/upload-release-asset@v1.0.2
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.get_release.outputs.upload_url }}
        asset_path: ./dist/nico-comme-don-${{ steps.get_package_version.outputs.package_version }}.x86_64.rpm
        asset_name: nico-comme-don-${{ steps.get_package_version.outputs.package_version }}.x86_64.rpm
        asset_content_type: application/x-rpm

  release_windows:
    runs-on: windows-latest
    
    steps:
    - name: Checkout
      uses: actions/checkout@v2
    - name: Use Node.js 14.x
      uses: actions/setup-node@v1
      with:
        node-version: 14.x
    - name: Build package
      run: |
        npm install -g yarn
        yarn install
        yarn build:win64
    - name: Get package version
      id: get_package_version
      run: |
        $package_json = Get-Content -Path "./package.json" | ConvertFrom-Json
        $version = $package_json.version
        echo "::set-output name=package_version::$version"
    - name: Get release
      id: get_release
      uses: cardinalby/git-get-release-action@v1.1
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        tag: v${{ steps.get_package_version.outputs.package_version }}
    - name: Upload release .exe Asset
      uses: actions/upload-release-asset@v1.0.2
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      with:
        upload_url: ${{ steps.get_release.outputs.upload_url }}
        asset_path: ./dist/NicoCommeDon Setup ${{ steps.get_package_version.outputs.package_version }}.exe
        asset_name: NicoCommeDon Setup ${{ steps.get_package_version.outputs.package_version }}.exe
        asset_content_type: application/octet-stream
```

# 今後の展望
- Mastodon以外のサービスもコメントとして流せるようにしたい(TwitterとかDiscordとか)
- テストケースを作成したい
- 汎用性を少し高めたい
- バグがまだまだあるので潰して行きたい…etc

一応NicoCommeDonに関してやりたいことは[issue](https://github.com/PigeonsHouse/NicoCommeDon/issues)にメモってあります．
あと共同開発もしたいのですが環境整備がまだまだ… ~~(てか顔知らん人からコミットやMR来たら嬉しいけど怖い…)~~

# 終わりに
まだまだ発展途上ですが，初めて一人で形にしたアプリになるので一人でも多くの人に使っていただけると嬉しいです！！