LoginSignup
10
6

More than 5 years have passed since last update.

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

Posted at

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

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

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

で、残った端末間通信方法がMultipeer Connectivityでした。
Multipeer Connectivityについてはこちら

前提

・iOS間でstreaming
・単方向のみ
・映像のみ、音声はなし
・1対1配信
・端末はSIM、WIFI接続なし(WIFIはON)

環境

・iOS12
・swift4.2

完成

ソースはこちら



左のiPadが配信者、右のiPhoneが受信者です。

手順

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

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

1. AVFoundationでvideo capture

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

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

let captureSession = AVCaptureSession()
let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: AVMediaType.video, position: .front)
let captureOutput = AVCaptureVideoDataOutput()

AVCaptureSessionとAVCaptureVideoDataOutputに必要情報をセットします。

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に対して行います。任意のタイミングで実行してください。

captureSession.startRunning()
captureSession.stopRunning()

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

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

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


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

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

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のインスタンスを作成します。

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の実装です。

session?.delegate = self
advertiserAssistant?.delegate = self
advertiserAssistant?.start()

browsingの実装です。

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を作ります。

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を渡してあげます。

try? session?.send(data, toPeers: (session?.connectedPeers)!, with: .reliable)

ここまでで、AVFoudationでcaputureしたvideo frameをMultipeer Connectivityで接続している端末には配信することができます。

4. Multipeer Connectivityで受信

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

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にセットするだけです。

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を配信できなかと思い実装してみました。

配信側

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 {

    }

}
受信側
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

10
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6