Files
simvision/KSPlayer-main/Sources/KSPlayer/MEPlayer/AudioUnitPlayer.swift
Michael Simard 872354b834 Initial commit: SimVision tvOS streaming app
Features:
- VOD library with movie grouping and version detection
- TV show library with season/episode organization
- TMDB integration for trending shows and recently aired episodes
- Recent releases section with TMDB release date sorting
- Watch history tracking with continue watching
- Playlist caching (12-hour TTL) for offline support
- M3U playlist parsing with XStream API support
- Authentication with credential storage

Technical:
- SwiftUI for tvOS
- Actor-based services for thread safety
- Persistent caching for playlists, TMDB data, and watch history
- KSPlayer integration for video playback

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 22:12:08 -06:00

198 lines
7.9 KiB
Swift

//
// AudioUnitPlayer.swift
// KSPlayer
//
// Created by kintan on 2018/3/16.
//
import AudioToolbox
import AVFAudio
import CoreAudio
public final class AudioUnitPlayer: AudioOutput {
private var audioUnitForOutput: AudioUnit!
private var currentRenderReadOffset = UInt32(0)
private var sourceNodeAudioFormat: AVAudioFormat?
private var sampleSize = UInt32(MemoryLayout<Float>.size)
public weak var renderSource: OutputRenderSourceDelegate?
private var currentRender: AudioFrame? {
didSet {
if currentRender == nil {
currentRenderReadOffset = 0
}
}
}
private var isPlaying = false
public func play() {
if !isPlaying {
isPlaying = true
AudioOutputUnitStart(audioUnitForOutput)
}
}
public func pause() {
if isPlaying {
isPlaying = false
AudioOutputUnitStop(audioUnitForOutput)
}
}
public var playbackRate: Float = 1
public var volume: Float = 1
public var isMuted: Bool = false
private var outputLatency = TimeInterval(0)
public init() {
var descriptionForOutput = AudioComponentDescription()
descriptionForOutput.componentType = kAudioUnitType_Output
descriptionForOutput.componentManufacturer = kAudioUnitManufacturer_Apple
#if os(macOS)
descriptionForOutput.componentSubType = kAudioUnitSubType_HALOutput
#else
descriptionForOutput.componentSubType = kAudioUnitSubType_RemoteIO
outputLatency = AVAudioSession.sharedInstance().outputLatency
#endif
let nodeForOutput = AudioComponentFindNext(nil, &descriptionForOutput)
AudioComponentInstanceNew(nodeForOutput!, &audioUnitForOutput)
var value = UInt32(1)
AudioUnitSetProperty(audioUnitForOutput,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output, 0,
&value,
UInt32(MemoryLayout<UInt32>.size))
}
public func prepare(audioFormat: AVAudioFormat) {
if sourceNodeAudioFormat == audioFormat {
return
}
sourceNodeAudioFormat = audioFormat
#if !os(macOS)
try? AVAudioSession.sharedInstance().setPreferredOutputNumberOfChannels(Int(audioFormat.channelCount))
KSLog("[audio] set preferredOutputNumberOfChannels: \(audioFormat.channelCount)")
#endif
sampleSize = audioFormat.sampleSize
var audioStreamBasicDescription = audioFormat.formatDescription.audioStreamBasicDescription
AudioUnitSetProperty(audioUnitForOutput,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input, 0,
&audioStreamBasicDescription,
UInt32(MemoryLayout<AudioStreamBasicDescription>.size))
let channelLayout = audioFormat.channelLayout?.layout
AudioUnitSetProperty(audioUnitForOutput,
kAudioUnitProperty_AudioChannelLayout,
kAudioUnitScope_Input, 0,
channelLayout,
UInt32(MemoryLayout<AudioChannelLayout>.size))
var inputCallbackStruct = renderCallbackStruct()
AudioUnitSetProperty(audioUnitForOutput,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input, 0,
&inputCallbackStruct,
UInt32(MemoryLayout<AURenderCallbackStruct>.size))
addRenderNotify(audioUnit: audioUnitForOutput)
AudioUnitInitialize(audioUnitForOutput)
}
public func flush() {
currentRender = nil
#if !os(macOS)
outputLatency = AVAudioSession.sharedInstance().outputLatency
#endif
}
deinit {
AudioUnitUninitialize(audioUnitForOutput)
}
}
extension AudioUnitPlayer {
private func renderCallbackStruct() -> AURenderCallbackStruct {
var inputCallbackStruct = AURenderCallbackStruct()
inputCallbackStruct.inputProcRefCon = Unmanaged.passUnretained(self).toOpaque()
inputCallbackStruct.inputProc = { refCon, _, _, _, inNumberFrames, ioData in
guard let ioData else {
return noErr
}
let `self` = Unmanaged<AudioUnitPlayer>.fromOpaque(refCon).takeUnretainedValue()
self.audioPlayerShouldInputData(ioData: UnsafeMutableAudioBufferListPointer(ioData), numberOfFrames: inNumberFrames)
return noErr
}
return inputCallbackStruct
}
private func addRenderNotify(audioUnit: AudioUnit) {
AudioUnitAddRenderNotify(audioUnit, { refCon, ioActionFlags, inTimeStamp, _, _, _ in
let `self` = Unmanaged<AudioUnitPlayer>.fromOpaque(refCon).takeUnretainedValue()
autoreleasepool {
if ioActionFlags.pointee.contains(.unitRenderAction_PostRender) {
self.audioPlayerDidRenderSample(sampleTimestamp: inTimeStamp.pointee)
}
}
return noErr
}, Unmanaged.passUnretained(self).toOpaque())
}
private func audioPlayerShouldInputData(ioData: UnsafeMutableAudioBufferListPointer, numberOfFrames: UInt32) {
var ioDataWriteOffset = 0
var numberOfSamples = numberOfFrames
while numberOfSamples > 0 {
if currentRender == nil {
currentRender = renderSource?.getAudioOutputRender()
}
guard let currentRender else {
break
}
let residueLinesize = currentRender.numberOfSamples - currentRenderReadOffset
guard residueLinesize > 0 else {
self.currentRender = nil
continue
}
if sourceNodeAudioFormat != currentRender.audioFormat {
runOnMainThread { [weak self] in
guard let self else {
return
}
self.prepare(audioFormat: currentRender.audioFormat)
}
return
}
let framesToCopy = min(numberOfSamples, residueLinesize)
let bytesToCopy = Int(framesToCopy * sampleSize)
let offset = Int(currentRenderReadOffset * sampleSize)
for i in 0 ..< min(ioData.count, currentRender.data.count) {
if let source = currentRender.data[i], let destination = ioData[i].mData {
if isMuted {
memset(destination + ioDataWriteOffset, 0, bytesToCopy)
} else {
(destination + ioDataWriteOffset).copyMemory(from: source + offset, byteCount: bytesToCopy)
}
}
}
numberOfSamples -= framesToCopy
ioDataWriteOffset += bytesToCopy
currentRenderReadOffset += framesToCopy
}
let sizeCopied = (numberOfFrames - numberOfSamples) * sampleSize
for i in 0 ..< ioData.count {
let sizeLeft = Int(ioData[i].mDataByteSize - sizeCopied)
if sizeLeft > 0 {
memset(ioData[i].mData! + Int(sizeCopied), 0, sizeLeft)
}
}
}
private func audioPlayerDidRenderSample(sampleTimestamp _: AudioTimeStamp) {
if let currentRender {
let currentPreparePosition = currentRender.timestamp + currentRender.duration * Int64(currentRenderReadOffset) / Int64(currentRender.numberOfSamples)
if currentPreparePosition > 0 {
var time = currentRender.timebase.cmtime(for: currentPreparePosition)
if outputLatency != 0 {
time = time - CMTime(seconds: outputLatency, preferredTimescale: time.timescale)
}
renderSource?.setAudio(time: time, position: currentRender.position)
}
}
}
}