Three.js VideoTexture SP対応
2026/03/27
three.jsのvideoTextureが、モバイル端末だと表示されず、ネットワークタブを見ると「読み込み中にエラーが起きました」とだけ書かれていた。
Three.js VideoTexture SP対応
dreiのvideoTextureを参考にするとSPでも動くようになったのでメモ。よくわからないが、ポイントは多分以下
loadedmetadataで「準備完了」にした
以前はcanplaythroughイベントを待っていた。最後まで再生できそうなタイミングで発火するので、モバイルだと遅い・発火しないことがある。
crossOrigin="anonymous"
動画をWebGLのテクスチャとしてGPUに乗せるには、ブラウザが「クロスオリジンでも安全に読める」と判断する必要あり
texture.colorSpaceをレンダラーにあわせる
texture.colorSpace = renderer.outpuColorSpace にすると、レンダラーとテクスチャの色空間の解釈が一致し、安定?
スマホは初回のユーザー操作がないと自動再生ポリシーで失敗する
最初からvideoTextureが見えているようなシーンだと注意。
ユーザーの操作をlistenし、play()する関数を作る。
ほぼ全文
export type CreateVideoTextureOptions = {
/** このイベントで Promise を解決する(drei 既定: loadedmetadata) */
unsuspend?: keyof HTMLVideoElementEventMap;
crossOrigin?: HTMLVideoElement["crossOrigin"];
muted?: boolean;
loop?: boolean;
playsInline?: boolean;
preload?: HTMLVideoElement["preload"];
/** WebGLRenderer.outputColorSpace に合わせる(drei: gl.outputColorSpace) */
outputColorSpace?: THREE.ColorSpace;
minFilter?: THREE.MinificationTextureFilter;
magFilter?: THREE.MagnificationTextureFilter;
};
export type VideoTextureResult = {
texture: THREE.VideoTexture<HTMLVideoElement>;
video: HTMLVideoElement;
};
function isUnsuspendReady(
video: HTMLVideoElement,
unsuspend: keyof HTMLVideoElementEventMap
): boolean {
switch (unsuspend) {
case "loadedmetadata":
return video.readyState >= HTMLMediaElement.HAVE_METADATA;
case "loadeddata":
case "canplay":
return video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA;
case "canplaythrough":
return video.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA;
default:
return false;
}
}
export function createVideoTextureFromSrc(
src: string,
options: CreateVideoTextureOptions = {}
): Promise<VideoTextureResult> {
const {
unsuspend = "loadedmetadata",
crossOrigin = "anonymous",
muted = true,
loop = true,
playsInline = true,
preload = "auto",
outputColorSpace = THREE.SRGBColorSpace,
minFilter = THREE.NearestFilter,
magFilter = THREE.NearestFilter,
} = options;
return new Promise((resolve, reject) => {
const video = document.createElement("video");
video.crossOrigin = crossOrigin;
video.muted = muted;
video.loop = loop;
video.playsInline = playsInline;
video.preload = preload;
video.src = src;
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
video.removeEventListener(unsuspend, onUnsuspend);
video.removeEventListener("error", onError);
const texture = new THREE.VideoTexture(video);
texture.colorSpace = outputColorSpace;
texture.minFilter = minFilter;
texture.magFilter = magFilter;
resolve({ texture, video });
};
const onUnsuspend = () => {
finish();
};
const onError = () => {
if (settled) return;
settled = true;
video.removeEventListener(unsuspend, onUnsuspend);
video.removeEventListener("error", onError);
reject(new Error(`Video load failed: ${src}`));
};
video.addEventListener("error", onError);
if (isUnsuspendReady(video, unsuspend)) {
queueMicrotask(finish);
} else {
video.addEventListener(unsuspend, onUnsuspend, { once: true });
}
});
}最後のisUnsuspendReady()箇所は、「loadedmetadataなどのunsuspendイベントがもう済んでいるか、これから来るか」で処理を分けている。
isUnsuspendReadyがtrueになるときは、ブラウザのキャッシュで動画が即座にメタデータまで読めていて、イベントを登録する前に readyState がもう十分な値になっている場合。
このときloadedmetadataを待っても来ない事があるので、queueMicrotasc(finish)で解決。
falseのときは普通にイベント登録。