録画機能を持つアプリで、短い動画はギャラリーに保存できるのに、長く撮ったものだけが保存されません。エラーには「容量が足りません」と出ます。端末の空きは数十 GB 残っているのに、という報告を、Expo の公開 Issue で読みました。
私は個人開発で、画像を端末に保存する機能を何度も書いてきました。エラー文を信じて空き容量の表示ばかり見て、半日を使った経験があります。この Issue を読んだとき、その半日が頭をよぎりました。
最初にお伝えしたいのは、「容量不足」という文言が、容量の話とは限らないということです。 ここからは、報告されている内容を整理し、境目の数字をこの手元で実際に動かして確かめ、アプリ側で先に確かめておく数行を書き残します。
報告されている現象
Expo の Issue(expo/expo #47767)に、expo-media-library の createAssetAsync が Android で大きなファイルを保存できない、という報告があります。環境は expo ~55.0.27、expo-media-library ~55.0.18、react-native 0.83.6 です。iOS では起きないと書かれています。
エラー文は「Unable to copy file into external storage.」で、原因として「Not enough space」が添えられています。報告者がアプリのキャッシュにダミーの mp4 を作って試した結果は、次のとおりです。
| ファイルの大きさ | 保存の結果 |
|---|---|
| 1.00 GB | 保存できた |
| 2.10 GB | 保存できた |
| 2.20 GB | 失敗した |
| 4.00 GB | 失敗した |
空き容量は足りている状態での結果だと、報告者は書いています。つまり、境目は端末の空きではなく、ファイル自体の大きさにあります。Issue の時点では未修正で、メンテナの方が main で原因を確認した、という状態でした(2026-10-22 に確認)。
境目の数字は、どこから来るのか
報告者の見立ては、保存処理の中で FileChannel.transferTo を 1 回だけ呼んでいて、ループしていない、というものです。Linux の sendfile は 1 回の呼び出しで運べる量に上限があり、その値は 2,147,479,552 バイトです。1 回で運び切れないと、残りが「書けなかった」と受け取られ、作りかけの項目が消されて、容量不足の文言になる——という流れです。
ここは Android の内部の話で、私の手元では再現できません。そこで、同じ上限が本当にあるのかだけを、Linux の同じ仕組み(sendfile)で確かめました。2.2GB(2,306,867,200 バイト)の空ファイルを作り、1 回だけ呼んだ結果です。
import os
size = os.path.getsize("src") # 2,306,867,200 バイト
s = os.open("src", os.O_RDONLY)
d = os.open("dst1", os.O_WRONLY | os.O_CREAT)
n = os.sendfile(d, s, 0, size) # 1 回だけ呼ぶ
print(n, size - n)
# 2147479552 1593876481 回で運ばれたのは 2,147,479,552 バイトちょうどで、159,387,648 バイトが残りました。Int32 の最大値(2,147,483,647)より、4,095 バイト手前です。数字の境目が、Issue の「2.10GB は通り、2.20GB は通らない」と同じ場所にあることが分かります。
続けて、残りがなくなるまで繰り返す形に変えました。
d = os.open("dst2", os.O_WRONLY | os.O_CREAT)
off = 0
while off < size:
n = os.sendfile(d, s, off, size - off)
if n == 0: # 進まないときは本当の失敗として止める
raise RuntimeError("no progress")
off += n
print(off == size) # True(2 回の呼び出しで完了)2 回の呼び出しで 2.2GB すべてが運ばれました。Issue で提案されている直し方も、同じ考え方です。「1 回で全部運べる」と決めつけず、運べた分だけ進めて続けること。 そして、まったく進まないときだけを本当の失敗として扱います。
アプリ側で、先にできること
ライブラリ側の修正を待つあいだ、アプリ側でできることは二つあります。どちらも、原因を切り分けるための手がかりを増やすものです。
一つめは、保存の前にファイルの大きさを見て、上限を超えていたら、別の文言で知らせることです。
import * as FileSystem from "expo-file-system";
import * as MediaLibrary from "expo-media-library";
// 約 2GB。Issue で報告されている境目の少し手前に置いています
const SAFE_LIMIT_BYTES = 2_000_000_000;
export async function saveVideoToGallery(uri: string) {
const info = await FileSystem.getInfoAsync(uri);
if (!info.exists) {
throw new Error("保存する動画が見つかりません");
}
if (info.size > SAFE_LIMIT_BYTES) {
// 空き容量の話ではない、と利用者に伝わる文言にします
throw new Error(
"この動画は大きすぎて、端末のギャラリーに保存できません。短く分けて保存してください。"
);
}
return MediaLibrary.createAssetAsync(uri);
}この断片は、型と流れを示すためのもので、私の手元の端末で createAssetAsync の 2GB 超を再現して確かめたものではありません。上限の値は、Issue の報告に合わせて少し手前に置いた目安です。お使いの SDK の版で、実機の確認をお願いいたします。
二つめは、録画の側で、そもそも 1 本が大きくなりすぎないようにすることです。長時間の録画を、一定の長さで区切って保存する設計にしておけば、この境目には近づきません。
切り分けの順番
「容量不足」と出たときに、私はいま次の順で見ています。
- 失敗したファイルの大きさを、バイトで確かめる
- 同じ中身の小さなファイルが保存できるかを試す
- 小さいものは通り、大きいものだけ落ちるなら、容量ではなくサイズの境目を疑う
空き容量を調べるのは、そのあとです。エラー文は、書いた人の想定した原因を指しているだけで、実際の原因とは限らないのだと、いまは思っています。当時の私は、そこに気づくまでに半日かかりました。私自身、この順番を決めてから、同じ場所で迷う時間が短くなりました。
なお、この記事で動かして確かめたのは、手元の Linux での sendfile の挙動までです。Android 端末での再現は、Issue の報告を引用した範囲にとどまることを、あらためてお断りしておきます。大きな動画を扱うアプリでは、保存の前後で何を記録しておくと原因を追いやすいか、という点も一緒に決めておくと安心です。たとえば、失敗した瞬間のファイルサイズ、保存先の種類、OS の版の三つを、ログに一行で残しておきます。
関連して、保存の権限やアルバムの扱いで詰まったときは、メディア保存の権限とアルバム設計や、端末への保存の不具合の直し方もあわせてご覧ください。
エラー文は読んだうえで、数字は自分で確かめます。 次に保存が落ちたときは、まずファイルの大きさをバイトで一行出力するところから始めてみていただければと思います。私もそこから見るようにしています。