iOS ReplayKit を使用する際に発生する2つの問題について

先日、弊社から「TDCベアー」というiOSアプリをリリースしました。このアプリは弊社のマスコットキャラクターであるTDCベアーをiOS のFace Tracking を使い操作する人の顔や頭に合わせて動かすことができます。また、キャラクターの動きを音声と一緒に動画データにして他の人と共有することもできます。

このアプリは動画データを作成するために iOS の ReplayKit を使用しています。ReplayKit を使うことで、アプリにあまり負荷を与えずに画面収録を行い動画ファイルとしてデバイス内に保存したり、ライブ配信したりできます。

ただしこの ReplayKit を使用して開発している際にいくつかの問題が生じました。ReplayKit の情報は少なく、問題の解決のために色々と調査をすることになりました。今回は調査の過程で分かったことについて説明します。

-5807 エラーが発生して画面収録が行えなくなる問題

ReplayKit で画面収録を行うには RPScreenRecorder の startRecording や startCapture を使用します。

https://developer.apple.com/documentation/replaykit/rpscreenrecorder

これらの機能を使用して画面収録を開始するときに -5807 というコードのエラーが発生することがあります。エラーのメッセージは Recording interrupted by multitasking and content resizing となっています。このエラーが発生すると画面収録が行えないのはもちろんのこと、アプリを起動しなおして画面収録を開始しても同じエラーが発生し続けるようになります。iOS のコントロールセンターにも画面収録を行う機能が備わっているのですが、ここから画面収録を行ってもエラーが発生するようになります。

iOS デバイスを再起動する以外に、この状態を解消する方法を確認できていません。この問題は執筆時点の iOS 最新バージョンである 12.3.1 で発生することを確認しています。

この問題は以下のWebサイトでも同じ問題について報告されています。

現状、このエラーの発生原因、再現性は突き止められておらず、回避策を講じることもできていません。開発中にこのエラーが発生した際の状況から推察して、以下の条件を全て満たす場合に発生しやすいのではないかと考えています。ただし、十分な回数試行したわけではないので内容は正確ではない可能性もあります。

  • Mac と iPhone を USB 接続した状態で一定時間経過する
  • アプリをインストールした直後、あるいは端末を再起動した直後
  • ReplayKit で画面収録を行う際に許可を求めるアラートが表示される

一度画面収録に成功すると、しばらくはこのエラーが発生する可能性が低くなるようです。またUSBケーブルを外して試行した場合にこのエラーが発生するケースは今の所確認していません。

このエラーの発生原因を調査する上で、iPhone と Mac を USB 接続してターミナルログを確認したところ、エラーが発生する前兆として明らかに異常なログを確認しました。そのエラーログを以下に記載します。なおこのログはアプリが直接出力したものではなく、画面収録時に iOS が出力しているものです。

デフォルト	10:03:34.156138 +0900	replayd	RPRecordingSession: startSession
デフォルト	10:03:34.161505 +0900	replayd	RPConnectionManager: startRecordingWindowLayerContextIDs completed
デフォルト	10:03:34.162741 +0900	replayd	RPMovieWriter: startWritingHandler
デフォルト	10:03:34.162800 +0900	replayd	RPRecordingSession: finishSessionWithHandler
デフォルト	10:03:34.162859 +0900	replayd	RPRecordingSession: stop
デフォルト	10:03:34.162917 +0900	replayd	RPRecordingSession: stopAllCapture
デフォルト	10:03:34.180574 +0900	replayd	RPCaptureManager: stopDeviceCaptureSessionWithFinishHandler
デフォルト	10:03:34.180625 +0900	replayd	Stop mirroring
デフォルト	10:03:34.180688 +0900	replayd	RPReportingAgent: reportingEvent:1 payload:{
    ActiveDuration = "-9223372036854775808";
    AppAudioCaptureRate = 2;
    AppAudioFrameCount = 0;
    BackCameraUsed = 0;
    BroadcastExtensionBundleID = "";
    ClientAppBundleID = "";
    EndReason = 0;
    FrontCameraUsed = 0;
    MicCaptureRate = 43;
    MicFrameCount = 0;
    RecordedFileSize = 0;
    VideoCaptureHeight = 0;
    VideoCaptureRate = 0;
    VideoCaptureWidth = 0;
}

ログのメッセージ、および時刻の流れをよく見ていただければと思います。画面収録を開始すると RPRecordingSession: startSession というログが出力されます。しかし、その直後になぜかすぐに画面収録の停止を試みていると思われる RPRecordingSession: stop というログが出力されています。

画面収録が正常に行われる際に RPRecordingSession: stop というログは、RPScreenRecorder の stopRecording や stopCapture を呼び出して、明示的に画面収録の停止をリクエストしたタイミングで出力されます。しかしエラーが発生する前兆においては、これらを呼び出すことなく、即座に RPRecordingSession: stop が出力されています。

また reportingEvent というログの payload にも注目してください。画面収録の経過時間であろう ActiveDuration の値が異常な値(-9223372036854775808)を示しています。以下のターミナルログの一部は画面収録が正常に完了した場合です。
ActiveDuration の値は収録した秒数が入っていますが、こちらは正常な値であることが分かるかと思います。

デフォルト	10:09:26.500057 +0900	replayd	RPReportingAgent: reportingEvent:1 payload:{
    ActiveDuration = 4;
    AppAudioCaptureRate = 2;
    AppAudioFrameCount = 7;
    BackCameraUsed = 0;
    BroadcastExtensionBundleID = "";
    ClientAppBundleID = "";
    EndReason = 0;
    FrontCameraUsed = 0;
    MicCaptureRate = 43;
    MicFrameCount = 0;
    RecordedFileSize = 0;
    VideoCaptureHeight = 1920;
    VideoCaptureRate = 60;
    VideoCaptureWidth = 887;
    VideoFrameCount = 207;
}

この前兆が現れると、アプリから画面収録を停止したりアプリそのものを終了したとしても、ターミナルログには以下のようなログが繰り返し出力され続けており、バックグラウンドでは画面の収録処理が継続されているようです。

デフォルト	10:03:34.639957 +0900	replayd	Screen sampled at w:886 h:1920
デフォルト	10:03:34.641118 +0900	replayd	RPRecordingManager:broadcastSessionDidUpdateDuration:
デフォルト	10:03:34.641166 +0900	replayd	RPRecordingSession:screenCaptureController:didReceiveSampleBuffer:transformFlags:
デフォルト	10:03:34.641818 +0900	replayd	RPClientProxy:captureHandlerWithSample:

この前兆が現れた後、再度アプリから画面収録を行うと -5807 というコードのエラーが発生することを確認しています。

全て ReplayKit のブラックボックス内で起きている事であるため、アプリのプログラムコード側で解消する方法が見つかっていません。唯一判明している解消方法であるデバイスの再起動ですが、デバイスの再起動をユーザに指示することはアプリのガイドラインに違反するためそのようなメッセージを表示することはできません。今は何らかの回避策が見つかるか、あるいは iOS のアップデートにより解消されることを期待しています。

「TDCベアー」アプリはこの問題を解消できていませんが、iPhone を普通に利用する上では問題が発生する可能性が低いのは幸いです。

アプリを強制終了すると画面収録の機能から録音できなくなる問題

ReplayKit は画面収録を行うのと同時に、マイクから音声を録音する機能が備わっています。しかし ReplayKit のマイク録音を有効にして収録を行った後にアプリを強制終了すると、その後はマイクの録音が行えなくなる問題を確認しています。この問題は iOS 12.3.1 で発生することを確認しています。

この問題を再現する手順を整理すると以下のようになります。

  1. RPScreenRecorder の isMicrophoneEnabled を true にした状態で一度画面収録を行う
  2. アプリを強制終了する
  3. 再度アプリを起動して isMicrophoneEnabled を true にした状態で一度画面収録を行う

この問題を再現するために使用したサンプルコードは以下の通りです。

import UIKit
import ReplayKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func startRecording(_ sender: Any) {
        RPScreenRecorder.shared().isMicrophoneEnabled = true
        RPScreenRecorder.shared().startRecording { error in
            if let error = error {
                print(error.localizedDescription)
            }
        }
    }
    
    @IBAction func stopRecording(_ sender: Any) {
        RPScreenRecorder.shared().stopRecording { viewController, error in
            if let error = error {
                print(error.localizedDescription)
            }
            guard let viewController = viewController else {
                return
            }
            viewController.previewControllerDelegate = self
            self.present(viewController, animated: true, completion: nil)
        }
    }
}

extension ViewController: RPPreviewViewControllerDelegate {
    func previewControllerDidFinish(_ previewController: RPPreviewViewController) {
        DispatchQueue.main.async {
            previewController.dismiss(animated: true, completion: nil)
        }
    }
}

この問題は「TDCベアー」アプリにとっては致命的なものでした。キャラクターの動きに合わせて音声をつける機能があるにも関わらず、この問題により録音が行えなくなるためです。

この問題の回避策として、マイクからの録音は AVCaptureDevice を使用する方法を取りました。RPScreenRecorder の isMicrophoneEnabled は false にして startCapture を使い画面を収録しています。startCapture は startRecording と違い、AVAssetWriter を使用して動画にエンコードするための処理を実装する必要がありますが、作成する動画データの形式を自由に変えられるというメリットがあります。動画の音声部分を自由に変えられるため AVCaptureDevice から収録した音声データを動画に含めることができます。今の所この回避策であれば、アプリを強制終了した場合であっても音声の収録を行うことができています。

startCapture を使えば画面の一部を切り抜くといった加工をリアルタイムに行うことも可能です。startCapture の自由度とは反対に startRecording は収録後は基本的に組み込みのプレビュー画面(RPPreviewViewController)を表示する必要があるという制約があります。実装の手間はかかりますが App Store で公開するようなアプリについては startRecording ではなく startCapture で録画することをおすすめします。

まとめ

ReplayKit はアプリにさほど負荷をあたえることなく画面収録を行えるという素晴らしい機能を提供してくれるのですが、上記にあげるような問題が存在します。ReplayKit に関する情報はとても少なく、これからこの機能を利用される方は苦労されるかもしれませんが今回掲載した情報が少しでも役に立てば幸いです。

山田 泰広 について

社内の本業とは別にCSIRTという組織に所属してセキュリティのことをいろいろとお手伝いしています。漫画やゲーム、映画が好きで、記事にもネタを混ぜていけたらと思います。最近CISSPというセキュリティの資格を取りました!