

# IVS iOS Broadcast SDK의 고급 사용 사례 \$1 저지연 스트리밍
<a name="broadcast-ios-use-cases"></a>

여기서는 몇 가지 고급 사용 사례를 제공합니다. 위의 기본 설정으로 시작하고 여기에서 계속합니다.

## 브로드캐스트 구성 생성
<a name="broadcast-ios-create-configuration"></a>

여기서는 두 개의 비디오 소스를 믹서에 바인딩할 수 있도록 두 개의 믹서 슬롯이 있는 사용자 지정 구성을 생성합니다. 하나(`custom`)는 전체 화면이고 다른 슬롯(`camera`) 뒤에 배치되며, 다른 슬롯은 더 작고 오른쪽 맨 아래에 있습니다. `custom` 슬롯의 경우 위치, 크기 또는 가로 세로 비율 모드를 설정하지 않습니다. 이러한 파라미터를 설정하지 않으므로 슬롯에서는 크기 및 위치에 비디오 설정을 사용합니다.

```
let config = IVSBroadcastConfiguration()
try config.audio.setBitrate(128_000)
try config.video.setMaxBitrate(3_500_000)
try config.video.setMinBitrate(500_000)
try config.video.setInitialBitrate(1_500_000)
try config.video.setSize(CGSize(width: 1280, height: 720))
config.video.defaultAspectMode = .fit
config.mixer.slots = [
    try {
        let slot = IVSMixerSlotConfiguration()
        // Do not automatically bind to a source
        slot.preferredAudioInput = .unknown
        // Bind to user image if unbound
        slot.preferredVideoInput = .userImage
        try slot.setName("custom")
        return slot
    }(),
    try {
        let slot = IVSMixerSlotConfiguration()
        slot.zIndex = 1
        slot.aspect = .fill
        slot.size = CGSize(width: 300, height: 300)
        slot.position = CGPoint(x: config.video.size.width - 400, y: config.video.size.height - 400)
        try slot.setName("camera")
        return slot
    }()
]
```

## 브로드캐스트 세션 생성(고급 버전)
<a name="broadcast-ios-create-session-advanced"></a>

[기본 예제](broadcast-ios-getting-started.md#broadcast-ios-create-session)에서 했던 것처럼 `IVSBroadcastSession`을 생성하지만, 여기서는 사용자 지정 구성을 제공합니다. 또한 디바이스를 수동으로 추가할 것이므로 디바이스 배열에 `nil`을 입력합니다.

```
let broadcastSession = try IVSBroadcastSession(
   configuration: config, // The configuration we created above
   descriptors: nil, // We’ll manually attach devices after
   delegate: self)
```

## 카메라 디바이스 반복 및 연결
<a name="broadcast-ios-attach-camera"></a>

여기서는 SDK가 감지한 입력 디바이스를 반복합니다. SDK는 iOS의 기본 제공 디바이스만 반환합니다. Bluetooth 오디오 디바이스가 연결되어 있더라도 기본 제공 디바이스로 표시됩니다. 자세한 정보는 [IVS iOS Broadcast SDK의 알려진 문제 및 해결 방법 \$1 저지연 스트리밍](broadcast-ios-issues.md)을 참조하세요.

사용할 디바이스를 찾으면 `attachDevice`를 호출하여 연결합니다.

```
let frontCamera = IVSBroadcastSession.listAvailableDevices()
    .filter { $0.type == .camera && $0.position == .front }
    .first
if let camera = frontCamera {
    broadcastSession.attach(camera, toSlotWithName: "camera") { device, error in
        // check error
    }
}
```

## 카메라 전환
<a name="broadcast-ios-swap-cameras"></a>

```
// This assumes you’ve kept a reference called `currentCamera` that points to the current camera.
let wants: IVSDevicePosition = (currentCamera.descriptor().position == .front) ? .back : .front
// Remove the current preview view since the device will be changing.
previewView.subviews.forEach { $0.removeFromSuperview() }
let foundCamera = IVSBroadcastSession
        .listAvailableDevices()
        .first { $0.type == .camera && $0.position == wants }
guard let newCamera = foundCamera else { return }
broadcastSession.exchangeOldDevice(currentCamera, withNewDevice: newCamera) { newDevice, _ in
    currentCamera = newDevice
    if let camera = newDevice as? IVSImageDevice {
        do {
            previewView.addSubview(try finalCamera.previewView())
        } catch {
            print("Error creating preview view \(error)")
        }
    }
}
```

## 사용자 지정 입력 소스 생성
<a name="broadcast-ios-create-input-source"></a>

앱에서 생성하는 사운드 또는 이미지 데이터를 입력하려면 `createImageSource` 또는 `createAudioSource`를 사용합니다. 이 두 메서드 모두 가상 디바이스(`IVSCustomImageSource` 및 `IVSCustomAudioSource`)를 생성하며, 해당 디바이스는 다른 디바이스처럼 믹서에 바인딩될 수 있습니다.

이 두 메서드에서 반환되는 디바이스는 `onSampleBuffer` 함수를 통해 `CMSampleBuffer`를 수락합니다.
+ 비디오 소스의 경우 픽셀 형식은 `kCVPixelFormatType_32BGRA`, `420YpCbCr8BiPlanarFullRange` 또는 `420YpCbCr8BiPlanarVideoRange`여야 합니다.
+ 오디오 소스의 경우 버퍼에 선형 PCM 데이터가 포함되어야 합니다.

Broadcast SDK에서 제공하는 카메라 디바이스도 사용하는 중 사용자 지정 이미지 소스를 공급하기 위해 카메라 입력으로 `AVCaptureSession`을 사용할 수 없습니다. 여러 카메라를 동시에 사용하려면 `AVCaptureMultiCamSession`을 사용하여 두 개의 사용자 지정 이미지 소스를 제공합니다.

사용자 지정 이미지 소스는 주로 이미지와 같은 정적 콘텐츠나 비디오 콘텐츠에 사용해야 합니다.

```
let customImageSource = broadcastSession.createImageSource(withName: "video")
try broadcastSession.attach(customImageSource, toSlotWithName: "custom")
```

## 네트워크 연결 모니터링
<a name="broadcast-ios-network-connection"></a>

모바일 디바이스에서는 이동 중에 네트워크 연결을 일시적으로 끊었다가 다시 연결하는 일이 흔합니다. 따라서 앱의 네트워크 연결을 모니터링하고 상황이 바뀌면 적절하게 대응하는 일이 중요합니다.

브로드캐스터의 연결이 끊어지면 Broadcast SDK의 상태가 `error`와 `disconnected`로 차례로 바뀝니다. 이러한 변경 사항에 대한 알림은 `IVSBroadcastSessionDelegate`를 통해 받습니다. 이러한 상태 변경 알림을 받으면 다음을 수행합니다.

1. 브로드캐스트 앱의 연결 상태를 모니터링하고 연결이 복원되면 엔드포인트 및 스트림 키를 사용하여 `start`를 호출합니다.

1. **중요:** 상태 대리자 콜백을 모니터링하고 `start`를 다시 호출한 후 상태가 `connected`로 변경되는지 확인합니다.

## 디바이스 분리
<a name="broadcast-ios-detach-device"></a>

디바이스를 분리만 하고 교체하지 않으려는 경우 `IVSDevice` 또는 `IVSDeviceDescriptor`을(를) 사용하여 디바이스를 분리하세요.

```
broadcastSession.detachDevice(currentCamera)
```

## ReplayKit 통합
<a name="broadcast-ios-replaykit"></a>

iOS에서 디바이스의 화면 및 시스템 오디오를 스트리밍하려면 [ReplayKit](https://developer.apple.com/documentation/replaykit?language=objc)와 통합해야 합니다. Amazon IVS Broadcast SDK에서는 `IVSReplayKitBroadcastSession`을 사용하여 ReplayKit을 쉽게 통합할 수 있습니다. `RPBroadcastSampleHandler` 하위 클래스에서 `IVSReplayKitBroadcastSession`의 인스턴스를 생성하고 다음을 수행합니다.
+ `broadcastStarted`에서 세션 시작
+ `broadcastFinished`에서 세션 중지

세션 객체에 화면 이미지, 앱 오디오 및 마이크 오디오를 위한 세 가지 사용자 지정 소스가 제공됩니다. `processSampleBuffer`에 제공된 `CMSampleBuffers`를 해당 사용자 지정 소스에 전달합니다.

디바이스 방향을 처리하려면 샘플 버퍼에서 ReplayKit 관련 메타데이터를 추출해야 합니다. 다음 코드를 사용합니다.

```
let imageSource = session.systemImageSource;
if let orientationAttachment = CMGetAttachment(sampleBuffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil) as? NSNumber,
    let orientation = CGImagePropertyOrientation(rawValue: orientationAttachment.uint32Value) {
    switch orientation {
    case .up, .upMirrored:
        imageSource.setHandsetRotation(0)
    case .down, .downMirrored:
        imageSource.setHandsetRotation(Float.pi)
    case .right, .rightMirrored:
        imageSource.setHandsetRotation(-(Float.pi / 2))
    case .left, .leftMirrored:
        imageSource.setHandsetRotation((Float.pi / 2))
    }
}
```

`IVSReplayKitBroadcastSession` 대신 `IVSBroadcastSession`을 사용하여 ReplayKit을 통합할 수 있습니다. 그러나 ReplayKit 관련 변형에서는 내부 메모리 사용량을 줄이고 브로드캐스트 확장에 대한 Apple의 메모리 상한 내로 유지하기 위해 여러 항목을 수정합니다.

## 권장 브로드캐스트 설정 가져오기
<a name="broadcast-ios-recommended-settings"></a>

브로드캐스트를 시작하기 전에 사용자 연결을 평가하려면 `IVSBroadcastSession.recommendedVideoSettings`를 사용하여 간단한 테스트를 실행합니다. 테스트가 실행되면 가장 권장되는 것에서 가장 권장되지 않는 것 순서로 여러 권장 사항이 표시됩니다. 이 버전의 SDK에서는 현재 `IVSBroadcastSession`을 다시 구성할 수 없으므로, 해당 세션을 할당 취소한 다음 권장 설정으로 새 세션을 생성해야 합니다. `result.status`가 `Success` 또는 `Error`가 될 때까지 계속 `IVSBroadcastSessionTestResults`가 표시됩니다. `result.progress`를 사용하여 진행률을 확인할 수 있습니다.

Amazon IVS에서는 최대 8.5Mbps의 비트 전송률(`type`이 `STANDARD` 또는 `ADVANCED`인 채널의 경우)을 지원하므로 이 메서드에서 반환되는 `maximumBitrate`는 8.5Mbps를 초과하지 않습니다. 네트워크 성능의 작은 변동을 고려하기 위해 이 메서드에서 반환되는 권장 `initialBitrate`는 테스트에서 측정된 실제 비트 전송률보다 약간 작습니다. (일반적으로 사용 가능한 대역폭의 100%를 사용하는 것은 바람직하지 않습니다.)

```
func runBroadcastTest() {
    self.test = session.recommendedVideoSettings(with: IVS_RTMPS_URL, streamKey: IVS_STREAMKEY) { [weak self] result in
        if result.status == .success {
            self?.recommendation = result.recommendations[0];
        }
    }
}
```

## 자동 재연결 사용
<a name="broadcast-ios-auto-reconnect"></a>

IVS는 `stop` API를 직접적으로 호출하지 않고 브로드캐스트가 예기치 않게 중지되는 경우(예: 네트워크 연결의 일시적 손실) 브로드캐스트에 대한 자동 재연결을 지원합니다. 자동 재연결을 활성화하려면 `enabled` 속성을 `IVSBroadcastConfiguration.autoReconnect`에 대해 `true`로 설정합니다.

무언가가 예기치 않게 스트림을 중지하면 SDK는 선형 백오프 전략에 따라 최대 5회까지 재시도합니다. `IVSBroadcastSessionDelegate.didChangeRetryState` 함수를 통해 애플리케이션에 재시도 상태를 알립니다.

백그라운드에서는 자동 재연결이 제공된 스트림 키 끝에 1로 시작하는 우선순위 번호를 추가하여 IVS [스트림 인수](streaming-config.md#streaming-config-stream-takeover) 기능을 사용합니다. `IVSBroadcastSession` 인스턴스 기간에 재연결을 시도할 때마다 해당 수가 1씩 증가합니다. 즉, 브로드캐스트 중에 디바이스 연결이 4회 손실되고 각 손실에서 1회\$14회의 재시도가 필요한 경우 마지막 스트림의 우선순위는 5\$117일 수 있습니다. 따라서 *동일한 채널에 대한 SDK에서 자동 재연결이 활성화된 동안에는 다른 디바이스에서 IVS 스트림 인수를 사용하지 않는 것이 좋습니다*. 해당 시점에 SDK가 사용하는 우선순위는 보장되지 않으며, 다른 디바이스가 인수하는 경우 SDK는 더 높은 우선순위로 재연결을 시도합니다.

## 백그라운드 비디오 사용
<a name="broadcast-ios-background-video"></a>

백그라운드에서 애플리케이션을 사용하더라도 RelayKit이 아닌 브로드캐스트를 계속할 수 있습니다.

전력을 절약하고 포그라운드 애플리케이션의 반응성을 유지하기 위해 iOS는 한 번에 하나의 애플리케이션만 GPU에 액세스하도록 합니다. Amazon IVS Broadcast SDK는 여러 입력 소스 합성, 이미지 크기 조정, 이미지 인코딩 등 비디오 파이프라인의 여러 단계에서 GPU를 사용합니다. 브로드캐스팅 애플리케이션이 백그라운드에 있는 동안 SDK가 이러한 작업을 수행할 수 있다는 보장은 없습니다.

이것을 해결하려면 `createAppBackgroundImageSource` 메서드를 사용합니다. 메서드를 통해 SDK는 백그라운드에서 비디오와 오디오 모두를 계속 브로드캐스팅할 수 있습니다. `IVSBackgroundImageSource`를 반환하며 이 결과는 추가 `finish` 함수를 가진 정상 `IVSCustomImageSource`입니다. 백그라운드 이미지 소스에 제공된 모든 `CMSampleBuffer`는 원본 `IVSVideoConfiguration`에서 제공하는 프레임 속도로 인코딩됩니다. `CMSampleBuffer`에서 타임스탬프는 무시됩니다.

그다음 SDK는 그런 이미지를 크기 조정하고, 인코딩하고, 캐싱하여 애플리케이션이 백그라운드로 이동할 때 해당 피드를 자동으로 반복합니다. 애플리케이션을 포그라운드로 반환하면 연결된 이미지 디바이스가 다시 활성화되고 미리 인코딩된 스트림이 반복을 중지합니다.

이 프로세스를 실행 취소하려면 `removeImageSourceOnAppBackgrounded`를 사용하세요. SDK의 백그라운드 동작을 명시적으로 되돌리려는 경우가 아니라면 이것을 호출할 필요는 없습니다. 그렇지 않으면 `IVSBroadcastSession`을 할당 해제할 때 자동으로 정리됩니다.

**참고:** *세션이 실행되기 전에 브로드캐스트 세션 구성의 일부로 이 메서드를 호출할 것을 적극 권장합니다.* 해당 메서드는 비디오를 인코딩해 비용이 많이 들기 때문에 메서드가 실행되는 동안 라이브 브로드캐스트 성능이 저하될 수 있습니다.

### 예: 백그라운드 비디오에 대한 정적 이미지 생성
<a name="background-video-example-static-image"></a>

백그라운드 소스에 단일 이미지를 제공하면 해당 정적 이미지의 전체 GOP가 생성됩니다.

다음은 CIImage를 사용하는 예입니다.

```
// Create the background image source
guard let source = session.createAppBackgroundImageSource(withAttemptTrim: true, onComplete: { error in
    print("Background Video Generation Done - Error: \(error.debugDescription)")
}) else {
    return
}

// Create a CIImage of the color red.
let ciImage = CIImage(color: .red)

// Convert the CIImage to a CVPixelBuffer
let attrs = [
    kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue,
    kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue,
    kCVPixelBufferMetalCompatibilityKey: kCFBooleanTrue,
] as CFDictionary

var pixelBuffer: CVPixelBuffer!
CVPixelBufferCreate(kCFAllocatorDefault,
                    videoConfig.width,
                    videoConfig.height,
                    kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
                    attrs,
                    &pixelBuffer)

let context = CIContext()
context.render(ciImage, to: pixelBuffer)

// Submit to CVPixelBuffer and finish the source
source.add(pixelBuffer)
source.finish()
```

또는 단색 CIImage를 만드는 대신 번들 이미지를 사용할 수 있습니다. 여기에 표시된 유일한 코드는 UIImage를 CIImage로 변환하여 이전 샘플과 함께 사용하는 방법입니다.

```
// Load the pre-bundled image and get it’s CGImage
guard let cgImage = UIImage(named: "image")?.cgImage else {
    return
}

// Create a CIImage from the CGImage
let ciImage = CIImage(cgImage: cgImage)
```

### 예: AVASSETimageGenerator를 사용한 비디오
<a name="background-video-example-avassetimagegenerator"></a>

`AVAssetImageGenerator`를 사용하여 HLS 스트림 `AVAsset`을 제외한 `AVAsset`으로부터 `CMSampleBuffers`를 생성할 수 있습니다.

```
// Create the background image source
guard let source = session.createAppBackgroundImageSource(withAttemptTrim: true, onComplete: { error in
    print("Background Video Generation Done - Error: \(error.debugDescription)")
}) else {
    return
}

// Find the URL for the pre-bundled MP4 file
guard let url = Bundle.main.url(forResource: "sample-clip", withExtension: "mp4") else {
    return
}
// Create an image generator from an asset created from the URL.
let generator = AVAssetImageGenerator(asset: AVAsset(url: url))
// It is important to specify a very small time tolerance.
generator.requestedTimeToleranceAfter = .zero
generator.requestedTimeToleranceBefore = .zero

// At 30 fps, this will generate 4 seconds worth of samples.
let times: [NSValue] = (0...120).map { NSValue(time: CMTime(value: $0, timescale: CMTimeScale(config.video.targetFramerate))) }
var completed = 0

let context = CIContext(options: [.workingColorSpace: NSNull()])

// Create a pixel buffer pool to efficiently feed the source
let attrs = [
    kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
    kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue,
    kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue,
    kCVPixelBufferMetalCompatibilityKey: kCFBooleanTrue,
    kCVPixelBufferWidthKey: videoConfig.width,
    kCVPixelBufferHeightKey: videoConfig.height,
] as CFDictionary
var pool: CVPixelBufferPool!
CVPixelBufferPoolCreate(kCFAllocatorDefault, nil, attrs, &pool)

generator.generateCGImagesAsynchronously(forTimes: times) { requestTime, image, actualTime, result, error in
    if let image = image {
        // convert to CIImage then CVpixelBuffer
        let ciImage = CIImage(cgImage: image)
        var pixelBuffer: CVPixelBuffer!
        CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer)
        context.render(ciImage, to: pixelBuffer)
        source.add(pixelBuffer)
    }
    completed += 1
    if completed == times.count {
        // Mark the source finished when all images have been processed
        source.finish()
    }
}
```

`AVPlayer` 및 `AVPlayerItemVideoOutput`을 사용하여 `CVPixelBuffers`를 생성할 수 있습니다. 그러나 이를 위해서 `CADisplayLink`를 사용해야 하고, 실시간에 가깝게 실행하는 반면 `AVAssetImageGenerator`는 프레임을 훨씬 빠르게 처리할 수 있습니다.

### 제한 사항
<a name="background-video-limitations"></a>

애플리케이션이 백그라운드로 이동한 후 일시 중단되는 것을 방지하려면 [백그라운드 오디오 권한 부여](https://developer.apple.com/documentation/xcode/configuring-background-execution-modes)가 필요합니다.

`createAppBackgroundImageSource`를 완료하려면 GPU에 액세스해야 하므로 애플리케이션이 포그라운드에 있는 동안에만 호출할 수 있습니다.

`createAppBackgroundImageSource`는 항상 전체 GOP로 인코딩합니다. 예를 들어 키프레임 간격이 2초(기본값)이고 30fps로 실행 중인 경우 60프레임의 배수를 인코딩합니다.
+ 60프레임 미만이 제공되면 트림 옵션의 값에 관계없이 60프레임에 도달할 때까지 마지막 프레임이 반복됩니다.
+ 60프레임 이상이 제공되고 트림 옵션이 `true`면 마지막 N 프레임이 삭제됩니다. 여기서 N은 제출된 총 프레임 수의 나머지 부분을 60으로 나눈 값입니다.
+ 60프레임 이상이 제공되고 트림 옵션이 `false`면 60프레임의 다음 배수에 도달할 때까지 마지막 프레임이 반복됩니다.