動画の描画
useVideoフックで動画のフレームをSkiaの画像として取得し、Image・ImageShader・Atlasに描画する方法と、再生・シーク・ループなどの制御を解説します。
React Native Skiaは、動画のフレームを1枚のSkia画像として取得する仕組みを持っています。取得したフレームはImage・ImageShader・Atlasなど、通常のSkia画像が使える場所ならどこにでも渡せます。動画にカラーフィルターをかけたり、円形にトリミングしたり、他の図形と合成したりといった表現が可能です。
前提として、Reanimated 3以降が必要です。Androidではフレーム取得の実装上、API level 26以上が必要になります。
useVideoの基本
useVideoフックに動画のURL(リモートのhttp://...、ローカルのfile://、またはバンドル内のアセット)を渡すと、現在のフレームを表す共有値(Shared Value)が得られます。
import { Canvas, ColorMatrix, Fill, ImageShader, useVideo } from "@shopify/react-native-skia";
import { Pressable, useWindowDimensions } from "react-native";
import { useSharedValue } from "react-native-reanimated";
const VideoExample = () => {
const paused = useSharedValue(false);
const { width, height } = useWindowDimensions();
const { currentFrame } = useVideo("https://example.com/video.mp4", { paused });
return (
<Pressable style={{ flex: 1 }} onPress={() => (paused.value = !paused.value)}>
<Canvas style={{ flex: 1 }}>
<Fill>
<ImageShader
image={currentFrame}
x={0}
y={0}
width={width}
height={height}
fit="cover"
/>
<ColorMatrix
matrix={[
0.95, 0, 0, 0, 0.05, 0.65, 0, 0, 0, 0.15, 0.15, 0, 0, 0, 0.5, 0,
0, 0, 1, 0,
]}
/>
</Fill>
</Canvas>
</Pressable>
);
};
currentFrameは共有値なので、これまでの章と同じくJavaScriptの再レンダリングを介さずにUIスレッド上でフレームが更新されます。タップでpausedを反転させているのも、指のタップというユーザーイベントをイベントハンドラーで処理しているだけで、Effectは登場しません。
このデモでは自作のグラデーション動画を再生しています。プレビュー部分をタップすると再生・一時停止が切り替わり、下のコントロールで反転カラーマトリクスのON/OFFを切り替えられます。
戻り値
useVideoが返すオブジェクトの内容です。
| 名前 | 型 | 説明 |
|---|---|---|
| currentFrame | SharedValue<SkImage | null> |
現在のフレーム画像。読み込み前はnull |
| currentTime | SharedValue<number> |
現在の再生位置(ミリ秒) |
| duration | number |
動画の長さ(ミリ秒) |
| framerate | number |
フレームレート |
| rotation | 0 | 90 | 180 | 270 |
動画の回転角度 |
| size | { width: number; height: number } |
動画の解像度 |
再生オプション
第2引数のオプションは、いずれも共有値またはプレーンな値として渡せます。
| 名前 | 型 | 説明 |
|---|---|---|
| paused | boolean |
一時停止するかどうか |
| looping | boolean |
ループ再生するかどうか |
| volume | number |
音量(0〜1) |
| seek | number | null |
指定ミリ秒へシーク。デフォルトはnull |
タップのたびに2秒地点へシークする例です。
const seek = useSharedValue<null | number>(null);
const { currentFrame, currentTime } = useVideo(videoURL, { seek, looping: true });
<Pressable style={{ flex: 1 }} onPress={() => (seek.value = 2000)}>
{/* ... */}
</Pressable>
seekは「ミリ秒を書き込むと1度だけシークが起き、実行後は自動的にnullへ戻る」という一回きりの共有値です。連続的な値ではなくトリガーとして使う点が、pausedやloopingのような状態を表す共有値と異なります。
このデモではスライダーの値をそのままseek.valueに書き込み、動画の任意の位置へジャンプできることを確認できます。durationはuseVideoの戻り値で、動画のメタデータ読み込みが完了するまでは0のままです。
回転した動画への対応
スマートフォンで縦向きに撮影した動画などはrotationが0以外になることがあります。fitbox関数を使うと、回転とフィット計算をまとめてtransformpropに変換できます。
import { Canvas, Image, useVideo, fitbox, rect } from "@shopify/react-native-skia";
const VideoExample = () => {
const { currentFrame, rotation, size } = useVideo(videoURL);
const src = rect(0, 0, size.width, size.height);
const dst = rect(0, 0, width, height);
const transform = fitbox("cover", src, dst, rotation);
return (
<Canvas style={{ flex: 1 }}>
<Image image={currentFrame} x={0} y={0} width={width} height={height} fit="none" transform={transform} />
</Canvas>
);
};
バンドル内の動画を使う
expo-assetを使うと、アプリにバンドルした動画ファイルをuseVideoに渡せます。
import { useVideo } from "@shopify/react-native-skia";
import { useAssets } from "expo-asset";
export const useVideoFromAsset = (
mod: number,
options?: Parameters<typeof useVideo>[1]
) => {
const [assets, error] = useAssets([mod]);
if (error) {
throw error;
}
return useVideo(assets ? assets[0].localUri : null, options);
};
注意点
Web上での動画対応には、ネイティブとは異なる制約があります。
内部的にはブラウザの<video>要素を使ってフレームをデコードしているため、動画はブラウザが再生できるコーデック(H.264のMP4など)である必要があります。加えて、<video>要素はデフォルトでcrossOrigin="anonymous"を要求するため、動画の配信元がCORSヘッダーを返さないと読み込みに失敗します。多くの動画CDNや汎用のファイルストレージはCORSを許可していないため、Web版では同一オリジンで配信できるローカルの動画ファイルを使うのが安全です(この章のデモの動画も本書と同一オリジンで配信しています)。実際、React Native Skia公式のサンプルアプリでも「Web版はCORSの制約があるためローカルの動画を使う」という分岐がコード中に明記されています。
ネイティブ(iOS/Android)ではCORSの制約は受けませんが、代わりにAndroidのAPIレベル制約があります。動画を多用する画面は、実機での動作確認を特に意識してください。
動画のエンコード
React Native Skiaは動画のデコード(読み込み・フレーム取得)には対応していますが、Skiaの描画結果を動画としてエンコードする機能は持っていません。エンコードが必要な場合はffmpegを使うか、react-native-skia-videoのようなサードパーティライブラリの利用を検討してください。
公式ドキュメントのVideoも参照してください。