Skip to content

Instantly share code, notes, and snippets.

@yogurtsake
Created March 14, 2020 23:54
Show Gist options
  • Save yogurtsake/5309b7422ad1ab4314da5d4948167614 to your computer and use it in GitHub Desktop.
Save yogurtsake/5309b7422ad1ab4314da5d4948167614 to your computer and use it in GitHub Desktop.
Post for StackOverflow
protocol AudioStreamingDelegate {
func connectedDevicesChanged(manager: MultipeerHandler, connectedDevices: [String])
}
class MultipeerHandler: NSObject {
private let peerType = "stream-audio"
private var peerId: MCPeerID!
private var serviceAdvertiser: MCNearbyServiceAdvertiser?
private var serviceBrowser: MCNearbyServiceBrowser?
lazy var session: MCSession = {
let session = MCSession(peer: self.peerId)
session.delegate = self
return session
}()
var delegate: AudioStreamingDelegate?
var viewController: ViewController!
init(sender: ViewController) {
super.init()
peerId = MCPeerID(displayName: UIDevice.current.name)
viewController = sender
}
deinit {
self.serviceAdvertiser?.stopAdvertisingPeer()
self.serviceBrowser?.stopBrowsingForPeers()
}
func startHosting() {
serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerId!, discoveryInfo: nil, serviceType: peerType)
serviceAdvertiser?.delegate = self
serviceAdvertiser?.startAdvertisingPeer()
}
func joinSession() {
serviceBrowser = MCNearbyServiceBrowser(peer: peerId!, serviceType: peerType)
serviceBrowser?.delegate = self
serviceBrowser?.startBrowsingForPeers()
}
}
extension MultipeerHandler: MCNearbyServiceAdvertiserDelegate {
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
print("\(error)")
}
func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
print("Accepting invitation from \(peerID.displayName)")
invitationHandler(true, session)
}
}
extension MultipeerHandler: MCNearbyServiceBrowserDelegate {
func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
let remotePeerName = peerID.displayName
let myPeerID = session.myPeerID
let shouldInvite = (myPeerID.displayName.compare(remotePeerName) == .orderedDescending)
if shouldInvite {
print("Inviting \(remotePeerName)")
browser.invitePeer(peerID, to: session, withContext: nil, timeout: 30.0)
} else {
print("Not inviting \(remotePeerName)")
}
}
func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
print("\(peerID.displayName)]")
}
func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) {
print("\(error)")
}
}
extension MultipeerHandler: MCSessionDelegate {
func session(_ session: MCSession, didReceiveCertificate certificate: [Any]?, fromPeer peerID: MCPeerID, certificateHandler: @escaping (Bool) -> Void) {
certificateHandler(true)
}
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
print("peer \(peerID) didChangeState: \(state.rawValue)")
let displayName = peerID.displayName
switch state {
case MCSessionState.connected:
print("Connected \(displayName)")
DispatchQueue.main.async {
self.viewController.view.backgroundColor = .systemGreen
}
case MCSessionState.connecting:
print("Connecting \(displayName)")
case MCSessionState.notConnected:
print("Not connected\(displayName)")
DispatchQueue.main.async {
self.viewController.view.backgroundColor = .systemRed
}
startHosting()
joinSession()
@unknown default:
fatalError()
}
self.delegate?.connectedDevicesChanged(manager: self, connectedDevices:
session.connectedPeers.map{$0.displayName})
}
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
print("didReceiveStream")
if streamName == "voice" {
print(#line)
viewController.inputStream = stream
viewController.inputStream.delegate = viewController
viewController.inputStream.schedule(in: RunLoop.main, forMode: .default)
viewController.inputStream.open()
}
}
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
print("didStartReceivingResourceWithName")
}
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
print("didFinishReceivingResourceWithName")
}
}
class StreamHelper: NSObject {
static func copyAudioBufferBytes(_ audioBuffer: AVAudioPCMBuffer) -> [UInt8] {
let srcLeft = audioBuffer.floatChannelData![0]
let bytesPerFrame = audioBuffer.format.streamDescription.pointee.mBytesPerFrame
let numBytes = Int(bytesPerFrame * audioBuffer.frameLength)
var audioByteArray = [UInt8](repeating: 0, count: numBytes)
srcLeft.withMemoryRebound(to: UInt8.self, capacity: numBytes) { srcByteData in
audioByteArray.withUnsafeMutableBufferPointer {
$0.baseAddress!.initialize(from: srcByteData, count: numBytes)
}
}
return audioByteArray
}
static func bytesToAudioBuffer(_ buf: [UInt8]) -> AVAudioPCMBuffer {
let audioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 8000, channels: 1, interleaved: false)
let frameLength = UInt32(buf.count) / audioFormat!.streamDescription.pointee.mBytesPerFrame
let audioBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat!, frameCapacity: frameLength)
audioBuffer!.frameLength = frameLength
let dstLeft = audioBuffer!.floatChannelData![0]
buf.withUnsafeBufferPointer {
let src = UnsafeRawPointer($0.baseAddress!).bindMemory(to: Float.self, capacity: Int(frameLength))
dstLeft.initialize(from: src, count: Int(frameLength))
}
return audioBuffer!
}
}
import UIKit
import AVFoundation
import MultipeerConnectivity
final class ViewController: UIViewController, StreamDelegate {
var audioSession: AVAudioSession = AVAudioSession.sharedInstance()
var audioPlayer: AVAudioPlayerNode = AVAudioPlayerNode()
var audioEngine: AVAudioEngine!
var inputNode: AVAudioInputNode!
var audioFormat: AVAudioFormat!
var mainMixer: AVAudioMixerNode!
var inputStream: InputStream!
var outputStream: OutputStream!
var bytesArrayArray: [[UInt8]?] = []
var multipeerHandler: MultipeerHandler!
var isRecording = false
let button: UIButton = {
let button = UIButton(type: .custom)
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(startRecording), for: .touchUpInside)
button.setTitle("Stream", for: .normal)
button.setTitle("Streaming", for: .selected)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
configureAudioSession()
audioEngine = AVAudioEngine()
inputNode = audioEngine.inputNode
audioFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 8000, channels: 1, interleaved: false)
multipeerSetup()
viewSetup()
}
private func configureAudioSession() {
do {
try audioSession.setCategory(.playAndRecord, options: .defaultToSpeaker)
try audioSession.setMode(.voiceChat)
try audioSession.setActive(true)
audioSession.requestRecordPermission() { [unowned self] (allowed: Bool) -> Void in
DispatchQueue.main.async {
if allowed {
print("allowed")
}
}
}
} catch { }
}
func multipeerSetup() {
multipeerHandler = MultipeerHandler(sender: self)
multipeerHandler.delegate = self
multipeerHandler.joinSession()
multipeerHandler.startHosting()
mainMixer = audioEngine.mainMixerNode
self.title = "Connections: \(multipeerHandler.session.connectedPeers)"
}
func viewSetup() {
view.backgroundColor = .white
self.view.addSubview(button)
let constraints = [
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
]
NSLayoutConstraint.activate(constraints)
}
}
extension ViewController {
@objc func startRecording() throws {
// start streaming
if !(self.isRecording) {
audioEngine.stop()
let inputFormat = inputNode.inputFormat(forBus: 0)
audioEngine.attach(audioPlayer)
audioEngine.connect(audioPlayer, to: mainMixer, format: audioFormat)
// audioEngine.connect(audioPlayer, to: audioPlayer.outputNode, format: inputFormat)
do {
if multipeerHandler.session.connectedPeers.count > 0 {
if outputStream != nil {
outputStream = nil
}
outputStream = try multipeerHandler.session.startStream(withName: "voice", toPeer: multipeerHandler.session.connectedPeers[0])
outputStream.schedule(in: RunLoop.main, forMode: .default)
outputStream.delegate = self
outputStream.open()
inputNode.installTap(onBus: 0, bufferSize: AVAudioFrameCount(inputFormat.sampleRate/10), format: inputFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
let convertedFrameCount = AVAudioFrameCount((Double(buffer.frameLength) / inputFormat.sampleRate) * inputFormat.sampleRate)
guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: inputFormat, frameCapacity: convertedFrameCount) else {
print("cannot make pcm buffer")
return
}
print("\(#line)")
let bytes = StreamHelper.copyAudioBufferBytes(pcmBuffer)
if(self.outputStream.hasSpaceAvailable){
self.outputStream.write(bytes, maxLength: bytes.count)
print("\(#line)")
}
}
audioEngine.prepare()
try audioEngine.start()
} else {
print("no peers to connect to")
}
} catch let error {
print(error.localizedDescription)
}
self.isRecording = true
} else {
// stop streaming
inputNode.removeTap(onBus: 0)
self.isRecording = false
}
}
}
extension ViewController {
func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
if aStream == inputStream {
switch eventCode {
case .errorOccurred:
break
case .endEncountered:
print("closed")
inputNode.removeTap(onBus: 0)
audioEngine.stop()
audioPlayer.stop()
inputStream.close()
inputStream.remove(from: RunLoop.main, forMode: .default)
break
case .hasBytesAvailable:
print("has byte available")
do {
try audioEngine.start()
audioPlayer.play()
} catch let error {
print(error.localizedDescription)
}
var bytes = [UInt8](repeating: 0, count: 4)
let stream = aStream as! InputStream
stream.read(&bytes, maxLength: bytes.count)
if bytesArrayArray.count < 1024 {
bytesArrayArray.append(bytes)
} else {
var resultBuffer = [UInt8](repeating: 0, count: 4096)
var index = 0
for byteArrayElement in bytesArrayArray {
for byte in byteArrayElement! {
resultBuffer[index] = byte
index = index + 1
}
}
let audioBuffer = StreamHelper.bytesToAudioBuffer(resultBuffer)
bytesArrayArray = []
audioPlayer.scheduleBuffer(audioBuffer, completionHandler: nil)
}
break
case .hasSpaceAvailable:
break
case .openCompleted:
break
default:
print("default")
}
}
}
}
extension ViewController: AudioStreamingDelegate {
func connectedDevicesChanged(manager: MultipeerHandler, connectedDevices: [String]) {
OperationQueue.main.addOperation {
self.title = "Connections: \(connectedDevices)"
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment