# スクリーンキャスト機能

## 概要

iOS クライアントアプリの画面をキャプチャして配信する機能です。
iOS クライアントアプリのゲームなどの動画実況配信や、アプリの使い方を説明する配信に利用できます。

また、画面キャプチャには ReplayKit フレームワークを利用しています。ReplayKit の詳細については  をご確認ください。

## 利用方法

スクリーンキャストによる画面キャプチャを有効にするには `MediaChannel.startScreenCapture` を利用します。
引数に `ScreenCaptureSettings` 構造体でパラメーターを指定することができます。

画面キャプチャを停止するには `MediaChannel.stopScreenCapture` を実行します。

### ScreenCaptureSettings で指定できるパラメーター

- targetFPS: 送信する映像フレームレートの目標値です。固定レートではなく上限値となります。入力フレームレートが高い場合、この値に応じてフレームが間引きされます- 高負荷時は目標レートを下回る可能性があります
  - 有効値は `1` ~ `120` となります。範囲外の値を指定した場合はクリップされます。デフォルト値は `15` です。大きな値を指定するほどクライアントは高負荷となります
  - フレーム送信間隔は PTS (Presentation Time Stamp) に応じて変動します
- videoSampleBufferTransformer: 映像フレーム送信前に `CMSampleBuffer` を加工するためのクロージャーです。指定しない場合は加工せずにフレームをそのまま送信します。重い加工処理はフレームのドロップや送信遅延に繋がるため避けるべきです。詳細については ReplayKit のドキュメントをご確認ください [](https://developer.apple.com/documentation/replaykit/rpscreenrecorder/startcapture\(handler:completionhandler:\))
- onRuntimeError: 画面キャプチャ中に発生したエラーを通知するためのコールバックです。通知専用のため、このコールバック呼び出しではキャプチャ停止は行われません。利用側で `MediaChannel.stopScreenCapture()` によるキャプチャ停止を実行する必要があります


### カメラ配信との同時使用について

Sora iOS SDK では複数の映像入力経路での同時配信に対応していません。このためカメラデバイスによる映像送信と同時に使用することはできません。
カメラデバイスによる映像配信中に画面キャプチャを開始しようとした場合、 `SoraError.mediaChannelError` エラーとなります。

配信においてカメラを利用しないのであれば `cameraSettings.isEnabled` を `false` に設定することでカメラを無効にして接続できます。

接続中にカメラと画面キャプチャを切り替えるのであれば、初期カメラ有効設定の `Configuration.initialCameraEnabled` や、
映像ハードミュート機能 `MediaChannel.setVideoHardMute` を利用することができます。

詳細は [ミュート機能](mute.html) をご確認ください。

### 利用例

```swift
// MediaChannel.startScreenCapture / MediaChannel.stopScreenCapture の利用例です。
// 接続処理や UI のうち、スクリーンキャスト開始 / 停止に不要な部分は省略しています。

import CoreMedia
import Sora
import UIKit

@MainActor
final class ScreencastViewController: UIViewController {
  // 接続完了後に設定される MediaChannel です。
  var mediaChannel: MediaChannel?

  // 画面キャプチャ中かどうかを保持します。
  private var isScreenCapturing = false

  // 画面キャプチャ切り替え処理の多重実行を防ぐため、実行中 Task を保持します。
  private var screenCaptureTask: Task<Void, Never>?

  // スクリーンキャストの開始 / 停止ボタンです。
  @IBOutlet weak var screencastButton: UIBarButtonItem?

  // スクリーンキャスト開始 / 停止をトグルします。
  @IBAction func onScreencastButton(_ sender: UIBarButtonItem) {
    guard let mediaChannel else {
      return
    }
    guard screenCaptureTask == nil else {
      return
    }

    screencastButton?.isEnabled = false

    if !isScreenCapturing {
      // runtime error は画面キャプチャ開始後に発生するため、ここで別途コールバックを設定します。
      let settings = ScreenCaptureSettings(
        // 目標フレームレートを指定したい場合は 1 ~ 120 の範囲で指定できます。
        targetFPS: 30,
        // videoSampleBufferTransformer を指定すると、送信前にフレームを加工できます。
        // 重い変換処理はフレームのドロップや送信遅延となる可能性があります。
        videoSampleBufferTransformer: { [weak self] sampleBuffer in
          guard let self else {
            return sampleBuffer
          }

          // 例として、H.264 利用時に画面のリサイズを行います
          guard mediaChannel.configuration.videoCodec == .h264 else {
            return sampleBuffer
          }
          return self.resizeSampleBuffer(sampleBuffer, scale: 0.5) ?? sampleBuffer
        },
        onRuntimeError: { error in
          // ログ出力など任意のエラーハンドリングを実装してください。
          _ = error
          // キャプチャ停止する場合は stopScreenCapture() を実行します。
          Task {
            await mediaChannel.stopScreenCapture()
          }
        }
      )

      screenCaptureTask = Task { [weak self] in
        defer {
          self?.screenCaptureTask = nil
          self?.screencastButton?.isEnabled = true
        }

        do {
          // 事前条件:
          // - MediaChannel が接続済みであること
          // - sender 側で映像送信が有効であること
          // - カメラキャプチャ実行中でないこと
          try await mediaChannel.startScreenCapture(settings: settings)

          guard let self else { return }
          self.isScreenCapturing = true
          self.updateScreencastButton(isCapturing: true)
        } catch {
          // 例: ユーザーが画面収録を拒否した場合、接続状態が不正な場合など
          _ = error
        }
      }
      return
    }

    screenCaptureTask = Task { [weak self] in
      defer {
        self?.screenCaptureTask = nil
        self?.screencastButton?.isEnabled = true
      }

      await mediaChannel.stopScreenCapture()

      guard let self else { return }
      self.isScreenCapturing = false
      self.updateScreencastButton(isCapturing: false)
    }
  }

  // 画面キャプチャ状態に応じてボタン表示を更新します。
  private func updateScreencastButton(isCapturing: Bool) {
    screencastButton?.image = UIImage(
      systemName: isCapturing ? "rectangle.on.rectangle.slash" : "rectangle.on.rectangle")
  }

  // 画面リサイズする例のためのダミーメソッドです
  private func resizeSampleBuffer(_ sampleBuffer: CMSampleBuffer, scale _: CGFloat)
    -> CMSampleBuffer?
  {
    _ = sampleBuffer
    return nil
  }
}
```
