OpenAIExpo2025/10/28 13:15

Faster, more reliable video uploads with Expo Modules

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

openaijamodel: gpt-5-mini-2025-08-07

Expo Modulesによる高速で信頼性の高い動画アップロード

Key Points

  • ネイティブでバックグラウンド実行
  • S3マルチパートで高速化
  • Expo Modulesで保守負担軽減

Summary

Boomでは動画アップロードがコア機能のため、モバイルでの大容量アップロードを確実に完了させる必要がありました。既存のJavaScript版アップローダはアプリのバックグラウンド中に停止したり、ネットワークの一時的な切断で失敗したりとスケールしませんでした。対策として、Expo Modulesでネイティブの長寿命タスク(UploadTask)を実装し、AWS S3のマルチパートアップロードを組み合わせることで、バックグラウンド実行、並列アップロード、リトライをネイティブ側で処理するパイプラインを構築しました。

Key Points

  • 課題
    • JavaScript実行はアプリライフサイクルに依存し、バックグラウンドで中断される。
    • 単一のネットワーク障害でアップロードが破棄される。
  • ソリューション
    • Expo ModulesでUploadTask(SharedObject)をネイティブに実装し長寿命タスクを保持。
    • AWS S3マルチパートを利用して大きなファイルを並列かつ再試行可能にアップロード。
    • ネイティブ側でチャンク分割、並列実行、リトライ、バックグラウンド実行を担当させる。
  • 実装の実務ポイント(React Native側の流れ)
    • new BackgroundUpload.UploadTask(videoUri, thumbnailUri)でタスク生成。
    • addListener('onProgress', ...)で進捗監視。
    • uploadTask.preProcess()でパーツ分割(uploadTask.parts.lengthを参照)。
    • バックエンドにpartsCountを送って事前署名(presigned URLs)を取得。
    • uploadTask.clipUrls = ... / uploadTask.completionUrl = ... / uploadTask.coverUrl = ... を設定して await uploadTask.upload() を呼ぶ。
    • 完了後にリスナーを削除、エラーはキャッチしてログ/再試行ロジックへ。
  • 結果
    • 100–300MBのクリップで中央値のエンドツーエンド時間が約20%短縮。
    • テストでは「詰まる」アップロードは観測されず、信頼性が向上。

Why this approach

  • Expo ModulesはTurboModulesよりボイラープレートが少なくExpoプロジェクトに統合しやすい。
  • ハイブリッド(JS + ネイティブ)実装により、ユーザ体験を損なわずにネイティブ性能と耐障害性を得られる。

Practical tips for engineers

  • 事前署名URLはパーツ数(uploadTask.parts.length)に基づいて発行する。
  • ネイティブでの並列度・リトライ戦略はアプリのネットワーク特性に合わせて調整する。
  • 進捗はonProgressで定期的に伝播し、UIはバックグラウンド→フォアグラウンド復帰時の状態再描画に備える。

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

openaijamodel: gpt-5-mini-2025-08-07

Expo Modulesを使った、より高速で信頼性の高い動画アップロード

Expo Modulesを使った、より高速で信頼性の高い動画アップロード

Users • React Native • Development • 2025-10-28 • 3分で読めます

ゲスト投稿:Petr Chalupa(SRTVのソフトウェアエンジニア、React Pragueオーガナイザー)

Boomは動画を軸としたコンペティションプラットフォームです(App Store、Google Play)。クリエイターは毎月11のカテゴリに映像を投稿し、ユーザー投票や審査員による選出で各カテゴリの勝者が決まり、賞金が授与されます。Boomにおいては、アップロードは単なる副機能ではなく、クリエイターがコンテストに参加するための主要な操作です。そのため、信頼できてストレスのないアップロード体験が非常に重要です。

課題:モバイルでの大容量動画アップロード

初期実装はシンプルなJavaScript製のアップローダーでした。MVPには十分でしたが、スケールさせるといくつかの重大な欠点が露呈しました:

  • バックグラウンド実行の欠如:JavaScriptの実行はアプリのライフサイクルに紐づいており、アプリがバックグラウンドになるとOSがアップロードを停止する可能性があります。
  • ネットワーク中断の取り扱い:一度のネットワークの途切れでアップロード全体が失敗し、ユーザーに再実行を強いることがありました。

目標:高速で信頼できるモバイル動画アップロード

目標は次のとおりです:

  • 速さ:ユーザーの端末からサーバーへできるだけ速く動画を転送する。
  • 信頼性:アプリの切り替え、ネットワークの断続、その他の中断が発生してもアップロードが継続する。

解決策:Expo Modulesを用いたネイティブのバックグラウンドアップロード

我々はExpo Modulesを使って全く新しいアップロードパイプラインを構築しました。SharedObjectを用いることで長時間生存する状態を持ったアップロードタスクを実現し、AWS S3のmultipart uploadに切り替えることで大容量ビデオのアップロードをより高速かつ信頼性の高いものにしました。

以下は我々のUploadTaskのTypeScriptインターフェースです。

export type UploadTaskEvents = {
  onProgress : ( params : { progress : number } ) => void
}
export declare class UploadTask extends SharedObject<UploadTaskEvents> {
  constructor ( clipPath : string , coverPath : string )
  readonly parts : URL[]
  clipUrls? : string[]
  completionUrl? : string
  coverUrl? : string
  preProcess() : void
  upload() : Promise<void>
}

次に、iOSネイティブモジュールがUploadTaskクラスをJavaScript側に公開している例です。

import ExpoModulesCore

public class BackgroundUploadModule : Module {
  public func definition() -> ModuleDefinition {
    Name("BackgroundUpload")
    Class(UploadTask.self) {
      Constructor { (clip: URL, cover: URL) -> UploadTask in
        return UploadTask(clip: clip, cover: cover)
      }

      Property("parts") { uploadTask in uploadTask.clip.parts }

      Property("clipUrls") { uploadTask in uploadTask.clip.uploadUrls }
        .set { (uploadTask: UploadTask, uploadUrls: [URL]) in
          uploadTask.clip.uploadUrls = uploadUrls
        }

      Property("completionUrl") { uploadTask in uploadTask.clip.completionUrl }
        .set { (uploadTask: UploadTask, completionUrl: URL) in
          uploadTask.clip.completionUrl = completionUrl
        }

      Property("coverUrl") { uploadTask in uploadTask.cover.uploadUrl }
        .set { (uploadTask: UploadTask, uploadUrl: URL) in
          uploadTask.cover.uploadUrl = uploadUrl
        }

      Function("preProcess") { uploadTask in try uploadTask.preProcessAssets() }

      AsyncFunction("upload") { uploadTask in try await uploadTask.upload() }
    }
  }
}

React Nativeとの統合例

下はReact Nativeでの統合を簡略化したコード例です。ネイティブモジュール側でチャンク分割、並列度、リトライ、バックグラウンド実行を扱います。

// instantiate an UploadTask that owns the parts, progress, and URLs
const uploadTask = new BackgroundUpload.UploadTask(videoUri, thumbnailUri)

// listen for progress
const progressListener = uploadTask.addListener('onProgress', ({ progress }) => {
  updateProgress({ uploadProgress: progress })
})

// split the file into independent parts
uploadTask.preProcess()

// request the backend for presigned URLs
const { data: uploadInfo } = await createUploadUrls({
  variables: { input: { partsCount: uploadTask.parts.length } },
})

// configure task
uploadTask.clipUrls = uploadInfo.clip.uploadUrls.map(({ uploadUrl }) => uploadUrl)
uploadTask.completionUrl = uploadInfo.clip.completionUrl
uploadTask.coverUrl = uploadInfo.cover.uploadUrl

// begin upload parts in parallel with retries
try {
  await uploadTask.upload()
} catch (error) {
  logError(error)
} finally {
  progressListener.remove()
}

結果:より高速で小さく、信頼できるアップロード

  • 速度:100〜300MBのクリップにおけるエンドツーエンドの中央値が約20%改善しました。
  • 信頼性:最近のテスト実行では“停止した”アップロードは観測されませんでした。
  • 測定方法:end‑to‑end = tap Agree and Post → backend confirms completion

結論:ネイティブアップロードを追加するならExpo Modulesが最適

大容量メディアのアップロードのような高性能で回復力が必要な機能には、JavaScriptとネイティブコードを組み合わせたハイブリッドアプローチが最適です。Expo Modulesはこれらの機能を構築するのに非常に適しており、TurboModulesと比べて保守が容易でボイラープレートが少なく、Expoプロジェクトとの統合もスムーズです。長期的な保守コストを下げつつ、ネイティブの性能を必要な箇所で享受できるため、投資の効果としてより滑らかなユーザー体験と堅牢なアプリが得られました。