Remix on Cloudflare Pages で何か動かす
ちょうど業務でも Cloudflare Stream を使おうと考えているので、このサンプルを Remix に移植してみようと思う
$ npx create-remix@latest
で、Just the basics
と Cloudflare Pages
を選択して雛形作成
Remix のバージョンは 1.3.4 だった。
npm run dev
するとなぜか @remix-run/server-runtime
が見つからないと言われるので、npm install @remix-run/server-runtime
してやると動くようになった
Tailwind 使おう
Remix で Tailwind 使う方法は ここ を参考にした
<Outlet> の外側に <header><h1>タイトル</h1></header> がある構造のときに、タイトルをページに合わせて変える方法がわからない。
loader で request.url からパス調べて、パスに応じたタイトルを渡す、でやってるけどもっといい方法ありそう
export const loader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
if (url.pathname === "/") {
return json({title: "Videos"});
} else if (url.pathname === "/upload") {
return json({title: "Upload"});
}
return json({ title: "表示されないはず" });
}
export default function App() {
const { title } = useLoaderData();
return (
<html lang="ja">
<head>
<Meta />
<Links />
</head>
<body>
<header>
<h1>{title}</h1>
</header>
<Outlet />
<LiveReload />
</body>
</html>
);
}
環境変数の設定もちょっと情報少なくて苦労した。
Remix かつ Cloudflare Pages (Wrangler) の場合なので、他の環境の場合は別の方法が取れる。
Remix のバージョンと Wrangler のバージョン次第では、これでもうまく動かなかったこともあるが、Remix 1.3.4 と Wrangler 0.0.24 だととりあえずローカル(npm run dev
)で動いた。
まず、環境変数を .env に書く。ファイルの場所はルートディレクトリ(package.json とかがあるのと同じところで、npm run dev
を実行する場所)。
API_KEY=hogehogehoe
次に Wrangler 実行時のこの環境変数を読み込めるように package.json を修正する
"dev:wrangler": "cross-env NODE_ENV=development wrangler pages dev ./public -b $(cat .env)",
環境変数を使う際には、LoaderFunction
の引数 context
から取得する
export const loader: LoaderFunction = async (context) => {
const env = context.context;
console.log(env.API_KEY); // hogehogehoge が出力される
}
Cloudflare Stream の API の叩き方について
API を叩くためには、アカウントID
と APIトークン
が必要。
アカウントIDは、Cloudflare Stream の管理画面の右上に書いてある。
APIキーはマイプロフィールのAPIトークンから、アカウントの Stream にアクセス許可をした新しくAPIトークンを作成する。
また、動画再生用の署名付きURLを発行するために必要な pem や jwk を取得するにはこちらの手順を参考に。
ページ内の TSX に Script を埋め込みたいときには、root.tsx
に <Scripts />
の記述をしておく必要がある。
{videos.map((video) => (
<div key={video.id} onClick={() => {window.location.href='/videos/' + video.id}} className="...">...</div>
))}
みたいなことをしたいときに、root.tsx
で
import { Scripts } from "remix";
...
export default function App() {
return (
<html>
<body>
...
<Outlet />
<Scripts />
<LiveReload />
</body>
</html>
);
}
みたいにして、 <Scripts /> を記述しておかないと動作しない。
ここまでローカルでやってきたので、そろそろ一回 Cloudflare Pages にデプロイしてみたいと思う。
Cloudflare の管理画面で Pages 選んで、プロジェクト新規作成して、 GitHub のリポジトリと連携する。
フレームワークは Remix が選択肢にあったので選ぶと、Build Command とかはちゃんと Remix 用のになってくれる。
あとは環境変数を設定してデプロイ実行すればOK。
アクセス制限したければ、プロジェクトの設定から Access ポリシーで制限すれば、デフォルトで Cloudflare のアカウントにメールアドレスが登録されてるメンバーのみに限定される(アクセスしたらメール入力欄があり、メールでログインコードが届く)。
特に引っかかるところ無くスムーズにデプロイできて動作確認までできた。
↑で制限されるのはあくまでも プレビュー環境
であり、サブドメイン *.hogehoge.pages.dev
に対して Access ポリシーだった(最初やったときはこれで本番の hogehoge.pages.dev
も制限された気がしたんだけど、勘違いか仕様かわったか分からない)。
本番にもやる場合、Cloudflare Zero Trust の Access の Applications の設定からサブドメインの *
の指定を外して空欄にする。
ただしそうすると、今度はプレビュー環境が見えてしまうので、もう一度↑のとおりに Pages のプロジェクト設定から制限すればOK。
さらにアクセス制限を、会社 Slack のアカウント持ってる人に限定しようと思う。
Cloudflare Zero Trust の Settings の Authentication から Add New で Identity Provider を選べる。
OpenID Connect で Slack とつなげたいので、OpenID Connect
を選ぶ。
入力欄がでてくるが、まずは Slack で新規 App を作成し、OAuth & Permissions の Redirect URLs に Cloudflare のページの右側に載ってるURL https://<your-team-name>.cloudflareaccess.com/cdn-cgi/access/callback
を登録する。<your-team-name>
は Cloudflare Zero Trust の Settings の General で確認できる。
あとは、Slack の App の管理画面の Basic Information に Client ID, Client Secret が載ってるので、それぞれ App ID, Client secret に入力。あとの3つのURLの情報は、ここにある。
Slack の App 管理画面で、Scope も openid, profile, email をつけておしまい。
Remix のバージョンを 1.6.3 にあげた。
あわせて、React のバージョンも 17.0.2 にあげた。
import {hoge} from "remix"
だったのを import {hoge} from "@remix-run/cloudflare"
とか @remix-run/react
とかに直せばだいたいOKだった。
個別の動画のページを作る。埋め込みプレーヤーも使う。
まずはプレーヤー。Cloudflare が用意してくれてる React 用のプレーヤーがあるのでそれを使う。
ただ再生するだけなら、src
にVideoIDか署名付きIDを書けばいいだけ。
今回は、「10秒進む/戻る」の実装がしたかったので、プレーヤーに外から(プレーヤーは iframe の中にあるので)アクセスする必要がある。streamRef
属性を使うことで実現できる。
import { useLoaderData } from "@remix-run/react"
import { Stream, StreamPlayerApi } from "@cloudflare/stream-react";
import { useRef } from "react";
export default function VideoRoute() {
const video = useLoaderData<VideoData>();
const ref = useRef<StreamPlayerApi>();
return (
<main>
<div>
<Stream streamRef={ref} controls src={video.signedId} />
</div>
<div>
<button onClick={() => {
if (ref.current) {
const now = ref.current.currentTime;
ref.current.currentTime = now - 10;
}}>« 10秒戻る</button>
</div>
</main>
)
}
次は動画のアップロード。Cloudflare Stream ではアップロードする方法はいくつかあるんだけど、CGMっぽくユーザーにアップロードしてもらう感じのことをしたかったら Direct creator uploads
を使うべきっぽい。
かつ、ファイルサイズが200MBを超えるなら、普通にアップロードじゃなくて TUS っていうプロトコルでアップロードしないといけない。正直めんどくさい。
Direct Creator Uploads の手順としては、まず、ワンタイムのアップロードURL(endpoint)をゲットする。これはAPIトークンが必要なので、サーバーサイドで実行する。その後、ゲットした endpoint をクライアントサイドに渡して実際のアップロードを実行してもらう。こんな感じ。
ただし、TUSの場合はちょっとややこしくて、最初の endpoint をゲットする部分をAPI化して、そのAPIをクライアント側の tus-js-client
ライブラリを使って呼び出す、って感じにしないといけない。
ファイルサイズとかメタデータとかが Request の header に入ってるので、それを取り出して endpoint を取得する API を叩いて、ゲットした endpoint を Response の Location Header に入れて返す。そんなAPIを作る。
import { json, ActionFunction } from "@remix-run/cloudflare";
export const action: ActionFunction = async ({ context, request }) => {
const env = context;
const size = request.headers.get("Upload-Length");
const metadata = request.headers.get("Upload-Metadata");
if (!size || typeof size !== "string" || size.length === 0 ||
!metadata || typeof metadata !== "string" || metadata.length === 0) {
return json({errors: {title: "File error."}}, {status: 422});
}
return new Response(null, {
headers: {
'Location': await getOneTimeEndpoint(size, metadata)
}
});
async function getOneTimeEndpoint(size: string, metadata: string) {
const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/stream?direct_user=true`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.STREAM_API_TOKEN}`,
'Tus-Resumable': '1.0.0',
'Upload-Length': size,
'Upload-Metadata': metadata,
},
});
const endpoint = response.headers.get('Location');
if (!endpoint) return "";
return endpoint;
}
};
そのAPIを呼び出す側は、<input type="file" />
でファイルを取得して、そのファイルと meta 情報のオプションを添えて tus の Upload を呼び出す感じ。
function upload() {
const options = {
endpoint: '/upload/upload',
chunkSize: 50 * 1024 * 1024,
metadata: {
name: file.name,
maxDurationSeconds: 3600,
requireSignedURLs: true,
},
onError(error: Error) {
console.log(error);
},
onSuccess() {
console.log("Upload Completed!");
},
onProgress(bytesUploaded: number, bytesTotal: number) {
const percentage = ((bytesUploaded / bytesTotal) * 100).toFix(2) + "%";
console.log(percentage);
}
};
const upload = new tus.Upload(file, options);
upload.start();
}
endpoint に指定するのが、最初に取得したワンタイムなアップロードURLなのかと勘違いしがち(僕も勘違いしたし、検索するとみんな勘違いしてた)だけど、そうではなく自分で作った API の URL なので注意。
あと、tus-js-client
は、Remix の tsx ファイル上で import * as tus from "tus-js-client"
をしたら、起動時になんかめっちゃエラーでるので、import はせずに <script src="~~/tus.min.js" />
で読み込んでる。
動くけど、VSCode 上では tus が未定義だってエラーになる。しょうがない。
後はアップロード中にパーセンテージに合わせてプログレスバー作ったりとか、見た目調整したりしておわり。お疲れさまでした。