Rork Max での Expo Modules セットアップ
Rork Max は内部的に Expo の Custom Development Build を使っています。ReplayKit を直接呼び出すネイティブモジュールを Expo Module として作成し、TypeScript から呼べるようにしていきます。
# プロジェクトルートで実行
# Expo Module のテンプレートを生成(モジュール名: expo-screen-recorder)
npx create-expo-module@latest expo-screen-recorder --no-example
cd expo-screen-recorder
# Rork Max プロジェクトに参照を追加(package.json)
# "expo-screen-recorder": "file:../expo-screen-recorder"
# 期待出力: モジュールが生成され、ios/ android/ src/ ディレクトリができる
このテンプレートは Swift / Kotlin の雛形を含みます。ios/ExpoScreenRecorderModule.swift と android/src/main/java/expo/modules/screenrecorder/ExpoScreenRecorderModule.kt の2ファイルが実装の中心になります。
iOS — RPScreenRecorder で In-App Recording を実装する
まずは iOS の In-App Recording から実装します。ポイントは「録画中であることをユーザーに明示する」「マイク許可を必要なら別途取る」「ファイル保存先を明確にする」の3点です。
// ios/ExpoScreenRecorderModule.swift
import ExpoModulesCore
import ReplayKit
import Photos
public class ExpoScreenRecorderModule : Module {
private let recorder = RPScreenRecorder. shared ()
public func definition () -> ModuleDefinition {
Name ( "ExpoScreenRecorder" )
// 録画開始: マイク有無を引数で受け取る
AsyncFunction ( "startRecording" ) { ( withMicrophone : Bool , promise : Promise) in
guard self .recorder.isAvailable else {
promise. reject ( "E_RECORDER_UNAVAILABLE" ,
"デバイスが録画に対応していないか、コントロールセンターで無効化されています" )
return
}
self .recorder.isMicrophoneEnabled = withMicrophone
self .recorder. startRecording { error in
if let error = error {
promise. reject ( "E_RECORDING_FAILED" , error.localizedDescription)
} else {
promise. resolve ([ "status" : "recording" ])
}
}
}
// 録画停止: 一時ファイルへ書き出して URL を返す
AsyncFunction ( "stopRecording" ) { ( promise : Promise) in
let tempURL = FileManager.default.temporaryDirectory
. appendingPathComponent ( "rork-recording- \( UUID (). uuidString ) .mp4" )
self .recorder. stopRecording ( withOutput : tempURL) { error in
if let error = error {
promise. reject ( "E_STOP_FAILED" , error.localizedDescription)
return
}
promise. resolve ([
"status" : "stopped" ,
"uri" : tempURL.absoluteString,
"size" : ( try? FileManager.default. attributesOfItem ( atPath : tempURL.path)[. size ] as? Int ) ?? 0
])
}
}
// 写真ライブラリへ保存
AsyncFunction ( "saveToLibrary" ) { ( uri : String , promise : Promise) in
guard let url = URL ( string : uri) else {
promise. reject ( "E_INVALID_URI" , "URI が不正です" ); return
}
PHPhotoLibrary. shared (). performChanges ({
PHAssetCreationRequest. creationRequestForAssetFromVideo ( atFileURL : url)
}) { success, error in
if success {
promise. resolve ([ "saved" : true ])
} else {
promise. reject ( "E_SAVE_FAILED" , error ? .localizedDescription ?? "保存失敗" )
}
}
}
}
}
このモジュールは recorder.isAvailable を最初に確認しているのが重要です。シミュレータや低スペック機、コントロールセンターで録画が無効化されたデバイスでは false になります。これをスキップしてエラーを上にぶつけると、ユーザーに「謎の失敗」が見えてしまい、レビューで指摘されます。
期待される動作は、startRecording(true) を呼ぶとマイク・録画許可ダイアログが出て、許可後に録画が始まります。stopRecording() で MP4 ファイルが一時ディレクトリに保存され、URI と容量が返ります。
Info.plist に必須キーを追加する
app.config.ts または Info.plist に以下を追加しないと、システムが許可ダイアログを出さずに録画が即座に失敗します。これは私が一度ハマった落とし穴です。
// app.config.ts の ios.infoPlist セクション
{
"ios" : {
"infoPlist" : {
"NSMicrophoneUsageDescription" : "操作の解説動画にあなたの声を録音するために使用します" ,
"NSPhotoLibraryAddUsageDescription" : "録画した動画をフォトライブラリに保存するために使用します"
}
}
}
NSMicrophoneUsageDescription は使う場合のみ必須ですが、利用しないつもりでも将来追加する可能性があるなら最初から書いておくほうが安全です。説明文は 何のために使うのかが具体的に分かる文言 にすること。「マイクを使用します」のような曖昧な文言は App Store 審査でリジェクトされる場合があります。
Android — MediaProjection と Foreground Service
Android は iOS より実装が複雑です。MediaProjection API は Foreground Service の中で動かす必要があり、サービスを正しく宣言・起動・停止しないと、システムが「画面録画が継続できない」と判断して途中で止めます。
// android/src/main/java/expo/modules/screenrecorder/ExpoScreenRecorderModule.kt
package expo.modules.screenrecorder
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.media.MediaRecorder
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import java.io.File
class ExpoScreenRecorderModule : Module () {
private var mediaProjection: MediaProjection ? = null
private var mediaRecorder: MediaRecorder ? = null
private var outputFile: File ? = null
override fun definition () = ModuleDefinition {
Name ( "ExpoScreenRecorder" )
// 録画許可ダイアログを呼び出す
AsyncFunction ( "requestPermission" ) { promise: Promise ->
val activity = appContext.activityProvider?.currentActivity
?: return @AsyncFunction promise. reject (
"E_NO_ACTIVITY" , "Activity が利用できません" , null )
val manager = activity. getSystemService (Context.MEDIA_PROJECTION_SERVICE)
as MediaProjectionManager
val intent = manager. createScreenCaptureIntent ()
// onActivityResult で結果を受け取る仕組みを別途用意する必要がある
// ここでは簡略化のため startActivityForResult を使う想定
activity. startActivityForResult (intent, REQUEST_CODE)
promise. resolve ( null )
}
// 録画開始 — Foreground Service を起動してから MediaRecorder をセットアップ
AsyncFunction ( "startRecording" ) { promise: Promise ->
val context = appContext.reactContext ?: return @AsyncFunction promise. reject (
"E_NO_CONTEXT" , "Context がありません" , null )
// 出力先を準備
val cacheDir = context.cacheDir
outputFile = File (cacheDir, "rork-recording-${System. currentTimeMillis ()}.mp4" )
// Foreground Service を起動(必須 — Android 10+ ではこれがないと許可されない)
val serviceIntent = Intent (context, ScreenRecorderService:: class .java). apply {
putExtra ( "outputPath" , outputFile !! .absolutePath)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context. startForegroundService (serviceIntent)
} else {
context. startService (serviceIntent)
}
promise. resolve ( mapOf ( "status" to "recording" ))
}
AsyncFunction ( "stopRecording" ) { promise: Promise ->
val context = appContext.reactContext ?: return @AsyncFunction promise. reject (
"E_NO_CONTEXT" , "Context がありません" , null )
context. stopService ( Intent (context, ScreenRecorderService:: class .java))
promise. resolve ( mapOf (
"status" to "stopped" ,
"uri" to (outputFile?. toURI ()?. toString () ?: "" ),
"size" to (outputFile?. length () ?: 0L )
))
}
}
companion object {
const val REQUEST_CODE = 1001
}
}
このコードのポイントは Foreground Service を別途起動していることです。ScreenRecorderService というクラスを AndroidManifest.xml に登録し、Notification を表示しながら MediaRecorder を回します。Android 14 以降では foregroundServiceType="mediaProjection" の宣言も必須です。
<!-- AndroidManifest.xml -->
< manifest >
< uses-permission android:name = "android.permission.FOREGROUND_SERVICE" />
< uses-permission android:name = "android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
< uses-permission android:name = "android.permission.RECORD_AUDIO" />
< application >
< service
android:name = ".ScreenRecorderService"
android:foregroundServiceType = "mediaProjection"
android:exported = "false" />
</ application >
</ manifest >
FOREGROUND_SERVICE_MEDIA_PROJECTION を抜くと Android 14 以降ではサービスが即座に停止されます。これは Pixel 8 系の実機で再現する地味なバグなので、エミュレータだけでテストしていると気付きません。
TypeScript 側のラッパーと UI 実装
ネイティブモジュールが書けたら、TypeScript から型安全に呼べるようにラップします。Rork Max の React Native 環境で使えるよう、Promise ベースの API として整えます。
// src/lib/screen-recorder.ts
import ExpoScreenRecorder from "expo-screen-recorder" ;
import { Platform } from "react-native" ;
export type RecordingResult = {
uri : string ;
size : number ;
};
export async function startScreenRecording ( options ?: {
withMicrophone ?: boolean ;
}) : Promise < void > {
const withMic = options?.withMicrophone ?? false ;
if (Platform. OS === "ios" ) {
await ExpoScreenRecorder. startRecording (withMic);
} else if (Platform. OS === "android" ) {
// Android はまず許可ダイアログを表示
await ExpoScreenRecorder. requestPermission ();
// 許可後に startRecording が呼ばれる前提(onActivityResult で連動)
await ExpoScreenRecorder. startRecording ();
} else {
throw new Error ( "画面録画は iOS / Android のみ対応しています" );
}
}
export async function stopScreenRecording () : Promise < RecordingResult > {
const result = await ExpoScreenRecorder. stopRecording ();
if ( ! result.uri) {
throw new Error ( "録画ファイルの保存に失敗しました" );
}
return result;
}
export async function saveToLibrary ( uri : string ) : Promise < boolean > {
if (Platform. OS === "ios" ) {
const result = await ExpoScreenRecorder. saveToLibrary (uri);
return result.saved;
}
// Android は MediaStore API を別途使う実装が必要
return false ;
}
UI 側は録画状態をローカル State で管理し、ボタンの見た目を切り替えます。録画中であることを 画面の目立つ位置に常時表示する のが App Store / Google Play の要件です。
// src/components/RecorderButton.tsx
import { useState } from "react" ;
import { Pressable, Text, View, Alert } from "react-native" ;
import {
startScreenRecording,
stopScreenRecording,
saveToLibrary,
} from "../lib/screen-recorder" ;
export function RecorderButton () {
const [ isRecording , setIsRecording ] = useState ( false );
const handleToggle = async () => {
try {
if (isRecording) {
const { uri } = await stopScreenRecording ();
const saved = await saveToLibrary (uri);
Alert. alert (
"録画完了" ,
saved ? "写真ライブラリに保存しました" : "ファイルを保存しました"
);
} else {
await startScreenRecording ({ withMicrophone: true });
}
setIsRecording ( ! isRecording);
} catch (error) {
Alert. alert (
"録画エラー" ,
error instanceof Error ? error.message : "不明なエラー"
);
}
};
return (
< View >
{ isRecording && (
< View style = { { flexDirection: "row" , alignItems: "center" , gap: 8 } } >
< View style = { {
width: 12 ,
height: 12 ,
borderRadius: 6 ,
backgroundColor: "red" ,
} } />
< Text >録画中</ Text >
</ View >
) }
< Pressable onPress = { handleToggle } >
< Text > { isRecording ? "録画を停止" : "録画を開始" } </ Text >
</ Pressable >
</ View >
);
}
「録画中」インジケーターを忘れると Apple / Google の両方でリジェクトされます。私は一度これで Apple から「The app does not provide a clear indication that screen recording is active.」と返されたことがあります。点滅させたり色をつけたりして、確実に視認できる位置に置きましょう。
Broadcast Upload Extension — メモリ制限 50 MB との戦い
In-App Recording に慣れたら、次は Broadcast Upload Extension に挑戦します。これは Twitch / YouTube への配信を実現する仕組みですが、 メモリ制限が 50 MB という厳しい制約があります 。
Xcode で File → New → Target → Broadcast Upload Extension を選んでターゲットを追加します。生成される SampleHandler.swift で、processSampleBuffer(_:with:) を実装します。
// MyBroadcastExtension/SampleHandler.swift
import ReplayKit
class SampleHandler : RPBroadcastSampleHandler {
private let encoder = HEVCEncoder () // 自前のエンコーダ
private let uploader = StreamUploader ()
override func broadcastStarted ( withSetupInfo setupInfo: [ String : NSObject] ? ) {
// App Group 経由でメインアプリから設定を受け取る
let userDefaults = UserDefaults ( suiteName : "group.com.rorklab.broadcast" )
guard let streamKey = userDefaults ? . string ( forKey : "streamKey" ) else {
finishBroadcastWithError ( NSError (
domain : "BroadcastExt" ,
code : 1 ,
userInfo : [NSLocalizedDescriptionKey : "ストリームキーが見つかりません" ]))
return
}
uploader. connect ( streamKey : streamKey)
}
override func processSampleBuffer ( _ sampleBuffer: CMSampleBuffer,
with sampleBufferType: RPSampleBufferType) {
// メモリを溜めず、即座にエンコード→アップロード
switch sampleBufferType {
case .video :
encoder. encode (sampleBuffer) { [ weak self ] data in
self ? .uploader. send (data)
}
case .audioApp :
uploader. sendAudio (sampleBuffer, type : .app)
case .audioMic :
uploader. sendAudio (sampleBuffer, type : .mic)
@unknown default: break
}
}
override func broadcastFinished () {
uploader. disconnect ()
}
}
50 MB 制限の中で動かすには、processSampleBuffer の中で バッファを保持しないこと が鉄則です。私が最初にやってしまった失敗は、サンプルバッファを Array に貯めて後でまとめて送ろうとしたことで、これは数秒で OOM クラッシュします。受け取ったらすぐエンコードして送り出す、これだけを徹底します。
App Group は Xcode の Capabilities で両ターゲットに group.com.rorklab.broadcast を追加します。これで UserDefaults や Keychain をメインアプリと共有できます。
よくある間違い・落とし穴
1. シミュレータで動作確認して安心してしまう
ReplayKit と MediaProjection は シミュレータ・エミュレータでは動作しないか、挙動が実機と異なります 。私は最初これを知らず、シミュレータで「動いた!」と判断してリリースしたら実機で動かないというミスをしました。必ず実機 — できれば iPhone と Pixel の両方 — で動作確認してください。
2. App Store の Encryption Export Compliance を忘れる
配信機能を入れると、App Store Connect の輸出規制申告で「暗号化を使用していますか?」の質問への回答が変わります。HTTPS 経由で配信するだけなら標準暗号で済みますが、独自の暗号化を入れると ERN(Encryption Registration Number)が必要になります。これを怠ると審査で止まります。
3. マイク権限を後付けで追加して起動時クラッシュ
NSMicrophoneUsageDescription を Info.plist に書いていない状態で isMicrophoneEnabled = true を呼ぶと、iOS は容赦なくクラッシュします。エラーで返してくれません。マイクを使う可能性が少しでもあるなら、最初から書いておくことを推奨します。
4. Android で通知を出さずにサービスを起動
Android 8 以降、Foreground Service は通知を表示しないと RemoteServiceException で即座に落ちます。startForeground(notificationId, notification) をサービスの onStartCommand で必ず呼んでください。通知のチャンネル ID も Android 8 以降は必須です。
5. 動画を .mov ではなく .mp4 で保存しないと共有先で再生できない
iOS の RPScreenRecorder はデフォルトで .mov を出力できますが、Android や Windows のユーザーに送ると再生できないことがあります。.mp4 拡張子で保存し、必要なら AVAssetExportSession で再エンコードして互換性を確保しましょう。
テレメトリ — 録画機能が壊れたことを App Store レビューより先に検知する
画面録画機能の地味な真実は、ユーザーが App Store のレビュー欄に書き込むまで壊れていることに気付かない という点です。気付いたときには評価が下がっています。これを防ぐには、機能に十分なテレメトリを仕込んでおき、リリース後の異常を数時間以内に検知できるようにします。
// src/lib/recording-analytics.ts
import { startScreenRecording, stopScreenRecording } from "./screen-recorder" ;
import { logEvent } from "./analytics" ;
type Outcome = "success" | "permission_denied" | "device_unavailable" | "save_failed" | "unknown_error" ;
export async function trackedStartRecording () {
const start = Date. now ();
try {
await startScreenRecording ({ withMicrophone: true });
logEvent ( "recording_started" , { latencyMs: Date. now () - start });
} catch (err) {
const outcome : Outcome = classifyError (err);
logEvent ( "recording_failed" , {
outcome,
latencyMs: Date. now () - start,
message: err instanceof Error ? err.message : String (err),
});
throw err;
}
}
function classifyError ( err : unknown ) : Outcome {
if ( ! (err instanceof Error )) return "unknown_error" ;
const msg = err.message. toLowerCase ();
if (msg. includes ( "permission" )) return "permission_denied" ;
if (msg. includes ( "unavailable" )) return "device_unavailable" ;
if (msg. includes ( "save" )) return "save_failed" ;
return "unknown_error" ;
}
ここで重要なのは網羅的にログを取ることではなく、permission_denied や device_unavailable の急増を 数時間単位でダッシュボードから検知できる ことです。OS バージョンや機種を一緒に記録しておけば、iOS や Android のアップデートで挙動が変わったときに、ユーザーの苦情より先に気付けます。
ファイル管理 — 地味だがリテンションを左右する設計
私が本番運用で見てきた録画機能の不具合の多くは、クラッシュではなく ファイル管理の怠慢に起因する静かな UX 劣化 でした。最初から組み込んでおきたいパターンを2つ紹介します。
起動時の古い録画ファイル掃除
前回セッションで一時ディレクトリに残ったファイルは無駄な容量です。アプリ起動時に24時間以上前のファイルを削除すると、保存せずに終了したユーザーのストレージを GB 単位で取り戻せます。
import * as FileSystem from "expo-file-system" ;
export async function cleanupStaleRecordings () {
const dir = FileSystem.cacheDirectory;
if ( ! dir) return ;
const files = await FileSystem. readDirectoryAsync (dir);
const cutoff = Date. now () - 24 * 60 * 60 * 1000 ;
for ( const file of files) {
if ( ! file. startsWith ( "rork-recording-" )) continue ;
const path = `${ dir }${ file }` ;
const info = await FileSystem. getInfoAsync (path);
if (info.exists && info.modificationTime && info.modificationTime * 1000 < cutoff) {
await FileSystem. deleteAsync (path, { idempotent: true });
}
}
}
録画開始前の空き容量チェック
10分の録画は約 200 MB の MP4 を生成します。空き 50 MB の状態で開始すると、途中で失敗して何も残りません。事前にチェックして、容量が足りないなら丁寧に断りましょう。
export async function hasSpaceForRecording ( estimatedMinutes = 10 ) : Promise < boolean > {
const required = estimatedMinutes * 25 * 1024 * 1024 ; // 概算: 25MB/分
const free = await FileSystem. getFreeDiskStorageAsync ();
return free > required;
}
25 MB/分 はデフォルトの RPScreenRecorder ビットレートでの概算値です。実際のアプリで計測してチューニングしてください。
配信プロトコル — RTMP と WebRTC の選択
ブロードキャスト機能まで実装する場合、プロトコル選択は意外と影響が大きいです。実用上の判断基準は以下です。
RTMP : TCP ベース、遅延 3〜5 秒、実装シンプル、YouTube Live / Twitch / 自前サーバーすべて対応。理由がない限りまず RTMP
WebRTC : UDP ベース、遅延 1 秒未満、双方向通話向け設計。実装が重く視聴者数のスケーリングが難しい
Low-Latency HLS / DASH : プル型配信フォーマット。ブロードキャスト拡張は RTMP で送り、エッジで再エンコードする構成
「ユーザーがゲームプレイを配信する」用途の99%は RTMP で十分です。WebRTC が必要なのは、1 秒未満の遅延がユーザー体験を本質的に変える場合(同時視聴・配信中投票など)に限られます。
実装で最初にハマるのは 音ズレ です。音声と映像のサンプルバッファは異なるタイムラインで届くので、エンコード時に揃えないと数分後に lip-sync が崩れます。受信時刻でタイムスタンプを記録し、サンプルバッファ自体の presentation timestamp でエンコード、サーバー側で出力同期、というパターンで安定します。
テスト戦略 — リリース前に確認する5つの観点
録画機能のテスト面は変則的で、壊れる原因のほとんどは環境要因(OS バージョン、ストレージ、許可状態、配信時のネットワーク)です。各リリース前に手動で確認すべき項目を挙げます。
許可マトリクス : granted / denied / 一度許可後に剥奪 / 未確認 の4状態を両プラットフォームで確認。テストしていない状態は本番で必ず驚かされる状態です。
長時間録画の耐久テスト : 30分連続録画してメモリ・バッテリー・容量を5分間隔で記録。メモリが直線的に増えるならバッファリーク、たいてい音声経路です。
バックグラウンド割り込み : 録画中に着信を受けて戻る。iOS は録画を一時停止・再開しますが、出力ファイルが時刻ズレや音切れを起こさないかを確認します。
録画中の画面回転 : フレーム単位で正しい向きが反映されるか。iOS の RPScreenRecorder は自動対応、Android の MediaProjection は開始時の解像度を維持するので注意です。
録画中のストレージ枯渇 : 残り 5% にしてから5分録画。コードがクリーンに停止し、ユーザーに有用なメッセージを出すか。0バイトファイルを残していないかも確認します。
これらは自動テストよりも手動チェックリストのほうが現実的だと考えています。Notion ページに手順とスクリーンショットを置いて、20分のチェックリストを毎回回すのが費用対効果に優れます。
本番アプリでの実測値
私が運用しているアプリ2本での過去四半期の実測値を共有します。iPhone 15 クラスのハードウェア基準です。
In-App Recording・マイクなし・1080p : 約 22 MB/分、CPU 持続 6%、10分セッションでバッテリー 2〜3%
In-App Recording・マイクあり・1080p : 約 24 MB/分、CPU 持続 9%、10分でバッテリー 4%
Broadcast Extension・720p RTMP・HEVC : ピークメモリ 38 MB(50 MB 制限に余裕)、CPU 12%
Broadcast Extension・1080p RTMP・H.264 : ピークメモリ 47 MB(OOM まで残り 1 フレーム)、CPU 16%。1080p 配信は本番投入禁止 — 余裕がありません
Android はチップセットによる差が大きく、Snapdragon 8 Gen 2 系は 1080p MediaProjection を CPU 7% で快適に処理しますが、古い MediaTek 系は同条件で 25% 超になります。デフォルトは控えめな解像度にして、ユーザーが選択できる設計にしてください。
最大コストはマイク経路です。マイク有効化で iOS の CPU 使用率はおおよそ倍増します。声を必須としない機能なら、デフォルトをオフにしてオプトインさせるのが堅実です。
アクセシビリティと同意の話
意外と語られないのが 同意 の問題です。録画する側の同意だけでなく、画面に映る他者のコンテンツに対する配慮も必要です。
他人のコンテンツ(チャットメッセージ・プロフィール写真・ビデオ通話)が映る画面を録画させるなら、その要素を blur / mask するか、その画面では録画機能自体を無効化するのが安全です。ReplayKit にはフレーム単位のフィルタ機能がないので、録画はユーザー自身のコンテンツが映る画面でのみ有効化する という設計が現実的です。
アクセシビリティ対応では、録画開始のジェスチャ設計も重要です。シングルタップで開始は OK ですが、VoiceOver ラベル(「録画を開始。共有用に画面をキャプチャします」)を必ず設定し、AssistiveTouch のショートカットと干渉しない位置に配置します。私は過去、録画ボタンを iOS のスワイプバック領域と重ねてしまって離脱が増えた経験があります。
「録画中」インジケータも、赤いドットだけでなくスクリーンリーダー向けの非表示ラベル(「録画中です」)を組み合わせること。こういう細部が「動く」を「全員にとって動く」に変えます。
本番運用で意識する3つの設計原則
ここまで実装の話をしてきましたが、本番運用で安定させるには設計面での配慮も欠かせません。私が3つのアプリを通じて学んだ原則を共有します。
原則1: 録画ファイルは即座に転送・破棄する
ローカルにファイルを溜めるとストレージを圧迫します。録画完了後は写真ライブラリに保存するか、サーバーにアップロードしてからキャッシュディレクトリのファイルを削除します。/tmp ディレクトリは iOS が再起動時に削除しますが、確実性のために自前で消すほうが安心です。
原則2: 録画中はバッテリー消費を可視化する
画面録画は CPU・GPU・ストレージI/O を全て使うため、バッテリーが急激に減ります。録画中であることをユーザーに明示するだけでなく、長時間録画した場合に「30分連続で録画中です」のような注意喚起を出すと、電源接続を促せます。
原則3: 失敗時のフォールバックを必ず用意する
ReplayKit の startRecording は様々な理由で失敗します(ディスク不足、システムリソース不足、他アプリが録画中など)。エラーメッセージをそのままユーザーに見せると意味不明なので、「ストレージの空き容量を確認してください」「コントロールセンターで画面録画が無効になっていないか確認してください」のように、行動可能な日本語に翻訳して表示します。
書籍で体系的に
他の Rork 機能との組み合わせ
画面録画機能を実装したアプリは、ストアでの差別化要素になりますが、それだけでは収益化につながりません。録画した動画をユーザーが共有しやすい仕組みや、有料機能としてのゲーティングを組み合わせると効果的です。
Rork Max での課金機能実装ガイド では、StoreKit 2 を使ったサブスクリプション課金の実装を解説しています。録画時間制限を有料プランで解放する、といった設計に応用できます。
Rork Max のサイレントプッシュとバックグラウンド処理 と組み合わせれば、録画完了後にバックグラウンドでサーバーへアップロードする運用も実現できます。
次の一歩
本記事のコードは GitHub に公開しているので、まずは In-App Recording のサンプルアプリをクローンして、自分の手元で動かしてみてください。実機で動くと「画面録画ってこんな簡単だったのか」と感じる一方、Broadcast Extension に手を出すと一気に難易度が上がります。最初は In-App Recording だけで十分なケースが多いので、ユーザーの声を聞きながら段階的に拡張していくのが現実的です。
録画機能はユーザー体験を大きく変える要素ですが、それゆえにプライバシーとパフォーマンスへの配慮が常に求められます。実装したら必ず実機で1時間以上の録画テストを行い、メモリ・バッテリー・ストレージの推移を計測してから本番に出してください。