This is my native module code implementation I'm getting base64 encoded string from server and passing this to my native module of pcm player to play audio
App.tsx
PcmPlayer.writeChunk(e.data);
PcmPlayer.swift
import AVFoundation
@objc(PcmPlayer)
class PcmPlayer: RCTEventEmitter {
private var engine: AVAudioEngine?
private var playerNode: AVAudioPlayerNode?
private var format: AVAudioFormat?
private var bufferQueue = [Data]()
private var isPlaying = false
private var hasEnded = false
private var scheduledBufferCount = 0
private let minBufferBytes = 50000
private let pcmQueue = DispatchQueue(label: "pcm.queue")
override init() {
super.init()
}
override func supportedEvents() -> [String]! {
return ["onStatus", "onMessage"]
}
@objc(initPlayer:channels:bitsPerSample:)
func initPlayer(_ sampleRate: NSNumber,
channels: NSNumber,
bitsPerSample: NSNumber) {
pcmQueue.async {
self.stopInternal()
let session = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playback, mode: .default, options: [])
try session.setActive(true, options: .notifyOthersOnDeactivation)
try session.setMode(.default)
print("🔈 Audio session active. Output route:", session.currentRoute.outputs)
} catch {
print("❌ Audio session setup failed:", error)
return
}
self.engine = AVAudioEngine()
self.playerNode = AVAudioPlayerNode()
guard let engine = self.engine, let playerNode = self.playerNode else {
print("❌ Engine or playerNode is nil")
return
}
engine.attach(playerNode)
self.format = AVAudioFormat(commonFormat: .pcmFormatFloat32,
sampleRate: sampleRate.doubleValue,
channels: AVAudioChannelCount(channels.uintValue),
interleaved: false)
guard let format = self.format else {
print("❌ Failed to create AVAudioFormat")
return
}
engine.connect(playerNode, to: engine.mainMixerNode, format: format)
do {
try engine.start()
playerNode.play()
engine.mainMixerNode.outputVolume = 1.0
print("✅ AVAudioEngine started with format:", format)
} catch {
print("❌ Engine start failed:", error)
}
self.hasEnded = false
}
}
@objc(writeChunk:)
func writeChunk(_ base64Pcm: String) {
pcmQueue.async {
guard base64Pcm.count >= 10 else {
print("⚠️ Skipping short base64 string")
return
}
var padded = base64Pcm
let mod4 = base64Pcm.count % 4
if mod4 > 0 {
padded += String(repeating: "=", count: 4 - mod4)
}
guard let data = Data(base64Encoded: padded, options: .ignoreUnknownCharacters) else {
print("❌ Failed to decode base64")
return
}
self.bufferQueue.append(data)
print("📥 Received PCM chunk (\(data.count) bytes)")
print("📥 writeChunk called. isPlaying=\(self.isPlaying), bufferQueue.count=\(self.bufferQueue.count)")
if !self.isPlaying {
self.isPlaying = true
self.waitForBufferAndStartPlayback()
} else if self.scheduledBufferCount == 0 {
self.isPlaying = true
self.waitForBufferAndStartPlayback()
}
}
}
private func waitForBufferAndStartPlayback() {
DispatchQueue.global().async {
while self.queueSize() < self.minBufferBytes && !self.hasEnded {
Thread.sleep(forTimeInterval: 0.01)
}
self.writeLoop()
}
}
private func writeLoop() {
DispatchQueue.global().async {
writeLoop: while self.isPlaying {
if self.bufferQueue.isEmpty {
for _ in 0..<100 {
Thread.sleep(forTimeInterval: 0.01)
if !self.bufferQueue.isEmpty { break }
}
if self.bufferQueue.isEmpty {
print("🔇 No more data to play after waiting")
self.isPlaying = false
break writeLoop
}
}
var data: Data?
self.pcmQueue.sync {
if !self.bufferQueue.isEmpty {
data = self.bufferQueue.removeFirst()
}
}
guard let chunk = data else {
print("⚠️ No data to process")
continue
}
if let buffer = self.pcmBufferFromData(chunk) {
self.scheduledBufferCount += 1
self.playerNode?.scheduleBuffer(buffer, completionHandler: {
self.pcmQueue.async {
self.scheduledBufferCount -= 1
if self.bufferQueue.isEmpty && self.scheduledBufferCount == 0 {
print("ℹ️ Playback idle - waiting for more data")
self.isPlaying = false
}
}
})
}
}
}
}
private func pcmBufferFromData(_ data: Data) -> AVAudioPCMBuffer? {
guard let format = self.format else { return nil }
let frameCount = UInt32(data.count / 2)
guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount) else {
print("❌ Failed to create AVAudioPCMBuffer")
return nil
}
buffer.frameLength = frameCount
guard let floatChannelData = buffer.floatChannelData?[0] else {
print("❌ floatChannelData is nil")
return nil
}
data.withUnsafeBytes { (rawBuffer: UnsafeRawBufferPointer) in
let int16Buffer = rawBuffer.bindMemory(to: Int16.self)
let count = min(int16Buffer.count, Int(frameCount))
for i in 0..<count {
floatChannelData[i] = Float32(int16Buffer[i]) / Float32(Int16.max)
}
}
return buffer
}
@objc(stopPlayer)
func stopPlayer() {
pcmQueue.async {
self.stopInternal()
}
}
private func stopInternal() {
print("🛑 stopInternal called")
self.playerNode?.stop()
self.engine?.stop()
self.engine?.reset()
self.playerNode = nil
self.engine = nil
self.format = nil
self.bufferQueue.removeAll()
self.isPlaying = false
self.hasEnded = true
self.scheduledBufferCount = 0
}
@objc(canWrite:rejecter:)
func canWrite(_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock) {
pcmQueue.async {
resolve(self.bufferQueue.count < 20)
}
}
@objc(flushPlayer:rejecter:)
func flushPlayer(_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: RCTPromiseRejectBlock) {
pcmQueue.async {
self.bufferQueue.removeAll()
resolve(nil)
}
}
@objc
static override func requiresMainQueueSetup() -> Bool {
return false
}
private func queueSize() -> Int {
return pcmQueue.sync {
return self.bufferQueue.reduce(0) { $0 + $1.count }
}
}
}
I couldn't able to hear any audio via my real iOS device also it is working fine on emulator.