iOS端末2台間で、Video Streamingを実装した時の戦果です。
前提条件に記載していますが、ネットワーク接続が許されない特殊な環境での挑戦となります。
まず、webRTCで試みましたが、現状、成功していません。
両端末のIPアドレスが取得できないと、webRTCは使えないというのが現状の結論です。(継続挑戦中です。)
また、CoreBluetoothでBLEを使う方法も今回は無しです。
で、残った端末間通信方法がMultipeer Connectivityでした。
Multipeer Connectivityについてはこちら
####前提
・iOS間でstreaming
・単方向のみ
・映像のみ、音声はなし
・1対1配信
・端末はSIM、WIFI接続なし(WIFIはON)
####環境
・iOS12
・swift4.2
####完成
ソースはこちら
— mypace (@mypacecoltd) 2018年10月6日左のiPadが配信者、右のiPhoneが受信者です。
####手順
- AVFoundationでvideo capture
- Multipeer Connectivityで端末間接続
- Multipeer Connectivityで送信
- Multipeer Connectivityで受信
- 映像表示
ソースを交えて説明していきます。
#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