Created
March 14, 2020 23:54
-
-
Save yogurtsake/5309b7422ad1ab4314da5d4948167614 to your computer and use it in GitHub Desktop.
Post for StackOverflow
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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