Multipeer Connectivityを使ってVideo Streamingの実装を試みたけど、パラパラ漫画で妥協した話

iOS端末２台間で、Video Streamingを実装した時の戦果です。
前提条件に記載していますが、ネットワーク接続が許されない特殊な環境での挑戦となります。

まず、webRTCで試みましたが、現状、成功していません。
両端末のIPアドレスが取得できないと、webRTCは使えないというのが現状の結論です。(継続挑戦中です。)

また、CoreBluetoothでBLEを使う方法も今回は無しです。

で、残った端末間通信方法がMultipeer Connectivityでした。
[Multipeer Connectivityについてはこちら](https://developer.apple.com/documentation/multipeerconnectivity)

####前提
・iOS間でstreaming
・単方向のみ
・映像のみ、音声はなし
・1対1配信
・端末はSIM、WIFI接続なし(WIFIはON)
####環境
・iOS12
・swift4.2
####完成
[ソースはこちら](https://github.com/highchops1981/fakeStreamingOverBluetooth)
<blockquote class="twitter-tweet" data-lang="ja"><p lang="und" dir="ltr"><a href="https://t.co/seVwjySAy4">https://t.co/seVwjySAy4</a></p>&mdash; mypace (@mypacecoltd) <a href="https://twitter.com/mypacecoltd/status/1048379605684379648?ref_src=twsrc%5Etfw">2018年10月6日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
左のiPadが配信者、右のiPhoneが受信者です。

####手順
1. AVFoundationでvideo capture
2. Multipeer Connectivityで端末間接続
3. Multipeer Connectivityで送信
4. Multipeer Connectivityで受信
5. 映像表示

ソースを交えて説明していきます。

#1. AVFoundationでvideo capture
様々なサイトで紹介されていますので、特記することはありません。実装したソースを掲載しておきます。

AVFoundationの必要なインスタンスを作成します。

```swift
let captureSession = AVCaptureSession()
let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: .front)
let captureOutput = AVCaptureVideoDataOutput()
```
AVCaptureSessionとAVCaptureVideoDataOutputに必要情報をセットします。

```swift
let videoInput = try AVCaptureDeviceInput(device: videoDevice!) as AVCaptureDeviceInput
captureSession.addInput(videoInput)
captureSession.addOutput(captureOutput)
captureSession.sessionPreset = AVCaptureSession.Preset.vga640x480
captureOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as AnyHashable as! String : Int(kCVPixelFormatType_32BGRA)]
captureOutput.setSampleBufferDelegate(self as AVCaptureVideoDataOutputSampleBufferDelegate, queue: DispatchQueue.main)            
captureOutput.alwaysDiscardsLateVideoFrames = true
```
video frameの取得開始と終了はAVCaptureSessionに対して行います。任意のタイミングで実行してください。

```swift
captureSession.startRunning()
captureSession.stopRunning()
```

キャプチャー画像はAVCaptureVideoDataOutputSampleBufferDelegateのcaptureOutputで取得します。このファンクションにはvideo frameが流れてきます。ここは後々重要になります。

```swift
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {}
```

自分のカメラ映像を自分の端末に表示する方法は2つあります。
1つ目はAVCaptureVideoPreviewLayerにAVCaptureSessionを渡す方法です。

```swift

previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspect
previewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation.portrait        
localView.layer.addSublayer(previewLayer!)        
previewLayer?.position = CGPoint(x: self.localView.frame.width/2, y: self.localView.frame.height/2)
previewLayer?.bounds = localView.frame
```

２つ目は前出のAVCaptureVideoDataOutputSampleBufferDelegateのcaptureOutputとAVSampleBufferDisplayLayerを使います。captureOutputにCMSampleBufferが渡されるので、AVSampleBufferDisplayLayerにenqueueしてください。

```swift
self.sampleBufferDisplayLayer.bounds = self.remoteView.frame
self.sampleBufferDisplayLayer.enqueue(sampleBuffer)
self.sampleBufferDisplayLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.remoteView.layer.addSublayer(self.sampleBufferDisplayLayer)
```

ここまでで、cameraの映像を自分の端末に表示することができます。
次からは、映像をどうのようにして相手側に配信するかを記述します。
最終的にすることは、AVCaptureVideoDataOutputSampleBufferDelegateで取得できるCMSampleBufferをなんとかして配信することです。

#2. Multipeer Connectivityで端末間接続
こちらも様々なサイトで紹介されています。接続するところまでは特記することはありません。実装したソースを掲載しておきます。

MCSession、MCAdvertiserAssistant、MCNearbyServiceBrowserのインスタンスを作成します。

```swift
self.displayName = displayName
self.serviceType = serviceType
peerId = MCPeerID(displayName: self.displayName)
session = MCSession(peer: self.peerId!, securityIdentity: nil, encryptionPreference: .optional)
advertiserAssistant = MCAdvertiserAssistant(serviceType: self.serviceType, discoveryInfo: nil, session: self.session!)
serviceType: self.serviceType)
browser = MCNearbyServiceBrowser(peer: self.peerId!, serviceType: self.serviceType)
```

advertiseの実装です。

```swift
session?.delegate = self
advertiserAssistant?.delegate = self
advertiserAssistant?.start()
```
browsingの実装です。

```swift
session?.delegate = self
browser?.delegate = self
browser?.startBrowsingForPeers()
```

1方の端末でadvertise、もう1方の端末でbrowsingを行います。
これで接続できます。
Multipeer Connectivityの注意事項として、Bluetoothのみだと接続できませんでした。WIFI設定をONにすると接続できます。ネットワークに繋がっている必要はありません。

#3. Multipeer Connectivityで配信
どの方法で、どんなデータ形式で配信するかについては試行錯誤が必要でした。
私の使った方法は、AVCaptureVideoDataOutputSampleBufferDelegateのcaptureOutputで取得したCMSampleBufferからUIImageを作成し、Data -> base64EncodedString -> Dataと変換してMultipeer Connectivityのsendメソッドで配信するというものです。

CMSampleBufferから配信用Dataを作ります。

```swift
guard let imageBuffer:CVImageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
        return nil
    }
CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
guard let baseAddress:UnsafeMutableRawPointer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) else {
        return nil
    }
let bytesPerRow:Int = CVPixelBufferGetBytesPerRow(imageBuffer)
let width:Int = CVPixelBufferGetWidth(imageBuffer)
let height:Int = CVPixelBufferGetHeight(imageBuffer)    
let colorSpace:CGColorSpace = CGColorSpaceCreateDeviceRGB()
guard let newContext:CGContext = CGContext(data: baseAddress, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace,  bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue|CGBitmapInfo.byteOrder32Little.rawValue) else {
    return nil
}
guard let imageRef:CGImage = newContext.makeImage() else {
    return nil
}
let image = UIImage(cgImage: imageRef, scale: 0.0)
if let imageData = image.jpegData(compressionQuality: 0.0) {
    let encodeString:String = imageData.base64EncodedString(options: [])
    let data = encodeString.data(using: .utf8) // --> これを配信する       
}
```

いよいよ配信です。Multipeer ConnectivityのMCSessionのsendに配信用Dataを渡してあげます。

```swift
try? session?.send(data, toPeers: (session?.connectedPeers)!, with: .reliable)
```
ここまでで、AVFoudationでcaputureしたvideo frameをMultipeer Connectivityで接続している端末には配信することができます。

#4. Multipeer Connectivityで受信

配信されてvideo frameを受信します。ここも試行錯誤が必要でした。Data型から人が見える形に変換する必要があるからです。私はUIImageに変換する方法をとりました。

```swift
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
    let responseString = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
    if let imageData = Data(base64Encoded: responseString, options: []) {
        let image = UIImage(data: imageData) // --> これを表示する  
    }
}
```

#5. 映像表示
最後は人に見えるようにしてあげましょう。UIImageをUIImageViewにセットするだけです。

```swift
func setRemoteView(image:UIImage) {
    DispatchQueue.main.async {
        self.remoteImageVIew.image = image
    }
}
```
長い道のりでしたが、これで、AVFoudationでcaputureしたvideo frameをMultipeer Connectivityで接続している端末には配信し表示することができます。

#結論
UIImageを高頻度で差し替えているだけの実装ですが、映像品質はgoodでした。
遅延、映像乱れ、メモリリークなどは起きていません。

#できなかったこと
今回の実装では、配信側、受信側でデータ形式の変換をなんども行なっています。
#####配信側のデータ形式変換
CMSampleBuffer -> CVImageBuffer -> CGContext -> CGImage -> UIImage -> Data -> String -> Data -> 配信

#####受信側のデータ形式変換
受信 -> Data -> NSString -> Data -> UIImage

video caputureで一番最初に取得できるCMSampleBufferを配信できれば、実装をシンプルにできます。
なぜならAVSampleBufferDisplayLayerを使えば、enqueueしてaddSublayerするだけで映像を表示できるからです。
また、Multipeer ConnectivityにはstartStreamとdidReceive streamなるものが存在し、これを使ってCMSampleBufferを配信できなかと思い実装してみました。

#####配信側

```swift

func sendImageBuffer(buffer:CMSampleBuffer) {
    guard let imageBuffer:CVImageBuffer = CMSampleBufferGetImageBuffer(buffer) else {
        return
    }
    
    CVPixelBufferLockBaseAddress(imageBuffer, CVPixelBufferLockFlags(rawValue: 0))
    guard let baseAddress:UnsafeMutableRawPointer = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0) else {
        return
    }
    
    let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer)
    let height = CVPixelBufferGetHeight(imageBuffer)
    let data = NSData(bytes: baseAddress, length: bytesPerRow * height)
    let unsafePointer = data.bytes.bindMemory(to: UInt8.self, capacity: 1)
    
    do {

        outputStream = try session?.startStream(withName: "stream", toPeer: (session?.connectedPeers.first)!)
        outputStream?.delegate = self
        outputStream?.write(unsafePointer, maxLength: data.length)
        outputStream?.schedule(in: .main, forMode: RunLoop.Mode.default)
        outputStream?.open()

    } catch {

    }

}
```

#####受信側

```swift
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {

    stream.delegate = self
    stream.schedule(in: .main, forMode: RunLoop.Mode.default)
    stream.open()
    
    var data:Data?
    let bufferSize = 1024
    let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
    while stream.hasBytesAvailable {
        let read = stream.read(buffer, maxLength: bufferSize)
        data?.append(buffer, count: read)
    }
    buffer.deallocate()
    
}
```

InputStreamをDataにするとこまではできたのですが、この先人が見える形にする方法がわかりませんでした。
どうやらAudioのInputStreamにはparserがあり、この方法で配信が可能のようです。
[Streaming Audio to Multiple Listeners via iOS' Multipeer Connectivity](https://robots.thoughtbot.com/streaming-audio-to-multiple-listeners-via-ios-multipeer-connectivity)