MTAudioProcessingTap EXC_BAD_ACCESS,并不总是触发finalize回调。如何发布?

时间:2018-12-05 16:30:00

标签: ios swift audio avfoundation core-audio

我正在尝试实现 MTAudioProcessingTap ,效果很好。问题是我完成使用Tap的操作后,我重新实例化了我的课程并创建了一个新的Tap。

我应该如何释放水龙头 1-我在创建水龙头时将其保留为属性,希望可以访问并稍后释放 2-在类的 deinit()方法中,我将混音设置为nil,然后尝试进行self.tap?.release()

问题是..有时它可以工作并调用FINALIZE回调,而一切都很好,有时却不起作用,只是在 tapProcess 回调行中崩溃:

let selfMediaInput = Unmanaged<VideoMediaInput>.fromOpaque(MTAudioProcessingTapGetStorage(tap)).takeUnretainedValue()

完整代码如下:https://gist.github.com/omarojo/03d08165a1a7962cb30c17ec01f809a3

import Foundation
import UIKit
import AVFoundation;
import MediaToolbox

protocol VideoMediaInputDelegate: class {
    func videoFrameRefresh(sampleBuffer: CMSampleBuffer) //could be audio or video
}

class VideoMediaInput: NSObject {
    private let queue = DispatchQueue(label: "com.GenerateMetal.VideoMediaInput")

    var videoURL: URL!

    weak var delegate: VideoMediaInputDelegate?

    private var playerItemObserver: NSKeyValueObservation?
    var displayLink: CADisplayLink!
    var player = AVPlayer()
    var playerItem: AVPlayerItem!
    let videoOutput = AVPlayerItemVideoOutput(pixelBufferAttributes: [String(kCVPixelBufferPixelFormatTypeKey): NSNumber(value: kCVPixelFormatType_32BGRA)])
    var audioProcessingFormat:  AudioStreamBasicDescription?//UnsafePointer<AudioStreamBasicDescription>?
    var tap: Unmanaged<MTAudioProcessingTap>?

    override init(){

    }

    convenience init(url: URL){
        self.init()
        self.videoURL = url

        self.playerItem = AVPlayerItem(url: url)

        playerItemObserver = playerItem.observe(\.status) { [weak self] item, _ in
            guard item.status == .readyToPlay else { return }
            self?.playerItemObserver = nil
            self?.player.play()
        }

        setupProcessingTap()


        player.replaceCurrentItem(with: playerItem)
        player.currentItem!.add(videoOutput)

        NotificationCenter.default.removeObserver(self)
        NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: nil, queue: nil) {[weak self] notification in

            if let weakSelf = self {
                /*
                 Setting actionAtItemEnd to None prevents the movie from getting paused at item end. A very simplistic, and not gapless, looped playback.
                 */
                weakSelf.player.actionAtItemEnd = .none
                weakSelf.player.seek(to: CMTime.zero)
                weakSelf.player.play()
            }

        }
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(applicationDidBecomeActive(_:)),
            name: UIApplication.didBecomeActiveNotification,
            object: nil)

    }

    func stopAllProcesses(){
        self.queue.sync {
            self.player.pause()
            self.player.isMuted = true
            self.player.currentItem?.audioMix = nil
            self.playerItem.audioMix = nil
            self.playerItem = nil
            self.tap?.release()
        }
    }


    deinit{
        print(">> VideoInput deinited !!!! ")
        if let link = self.displayLink {
            link.invalidate()
        }
        NotificationCenter.default.removeObserver(self)

        stopAllProcesses()

    }
    public func playVideo(){
        if (player.currentItem != nil) {
            print("Starting playback!")
            player.play()
        }
    }
    public func pauseVideo(){
        if (player.currentItem != nil) {
            print("Pausing playback!")
            player.pause()
        }
    }

    @objc func applicationDidBecomeActive(_ notification: NSNotification) {
        playVideo()
    }




    //MARK: GET AUDIO BUFFERS
    func setupProcessingTap(){

        var callbacks = MTAudioProcessingTapCallbacks(
            version: kMTAudioProcessingTapCallbacksVersion_0,
            clientInfo: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
            init: tapInit,
            finalize: tapFinalize,
            prepare: tapPrepare,
            unprepare: tapUnprepare,
            process: tapProcess)

        var tap: Unmanaged<MTAudioProcessingTap>?
        let err = MTAudioProcessingTapCreate(kCFAllocatorDefault, &callbacks, kMTAudioProcessingTapCreationFlag_PostEffects, &tap)
        self.tap = tap


        print("err: \(err)\n")
        if err == noErr {
        }

        print("tracks? \(playerItem.asset.tracks)\n")

        let audioTrack = playerItem.asset.tracks(withMediaType: AVMediaType.audio).first!
        let inputParams = AVMutableAudioMixInputParameters(track: audioTrack)
        inputParams.audioTapProcessor = tap?.takeRetainedValue()//tap?.takeUnretainedValue()
//        tap?.release()

        // print("inputParms: \(inputParams), \(inputParams.audioTapProcessor)\n")
        let audioMix = AVMutableAudioMix()
        audioMix.inputParameters = [inputParams]

        playerItem.audioMix = audioMix
    }

    //MARK: TAP CALLBACKS

    let tapInit: MTAudioProcessingTapInitCallback = {
        (tap, clientInfo, tapStorageOut) in
        tapStorageOut.pointee = clientInfo

        print("init \(tap, clientInfo, tapStorageOut)\n")

    }

    let tapFinalize: MTAudioProcessingTapFinalizeCallback = {
        (tap) in
        print("finalize \(tap)\n")
    }

    let tapPrepare: MTAudioProcessingTapPrepareCallback = {
        (tap, itemCount, basicDescription) in
        print("prepare: \(tap, itemCount, basicDescription)\n")
        let selfMediaInput = Unmanaged<VideoMediaInput>.fromOpaque(MTAudioProcessingTapGetStorage(tap)).takeUnretainedValue()
        selfMediaInput.audioProcessingFormat = AudioStreamBasicDescription(mSampleRate: basicDescription.pointee.mSampleRate,
                                                                           mFormatID: basicDescription.pointee.mFormatID, mFormatFlags: basicDescription.pointee.mFormatFlags, mBytesPerPacket: basicDescription.pointee.mBytesPerPacket, mFramesPerPacket: basicDescription.pointee.mFramesPerPacket, mBytesPerFrame: basicDescription.pointee.mBytesPerFrame, mChannelsPerFrame: basicDescription.pointee.mChannelsPerFrame, mBitsPerChannel: basicDescription.pointee.mBitsPerChannel, mReserved: basicDescription.pointee.mReserved)
    }

    let tapUnprepare: MTAudioProcessingTapUnprepareCallback = {
        (tap) in
        print("unprepare \(tap)\n")
    }

    let tapProcess: MTAudioProcessingTapProcessCallback = {
        (tap, numberFrames, flags, bufferListInOut, numberFramesOut, flagsOut) in
        print("callback \(bufferListInOut)\n")

        let selfMediaInput = Unmanaged<VideoMediaInput>.fromOpaque(MTAudioProcessingTapGetStorage(tap)).takeUnretainedValue()

        let status = MTAudioProcessingTapGetSourceAudio(tap, numberFrames, bufferListInOut, flagsOut, nil, numberFramesOut)
        //print("get audio: \(status)\n")
        if status != noErr {
            print("Error TAPGetSourceAudio :\(String(describing: status.description))")
            return
        }

        selfMediaInput.processAudioData(audioData: bufferListInOut, framesNumber: UInt32(numberFrames))
    }
    func processAudioData(audioData: UnsafeMutablePointer<AudioBufferList>, framesNumber: UInt32) {
        var sbuf: CMSampleBuffer?
        var status : OSStatus?
        var format: CMFormatDescription?

        //FORMAT
//        var audioFormat = self.audioProcessingFormat//self.audioProcessingFormat?.pointee
        guard var audioFormat = self.audioProcessingFormat else {
            return
        }
        status = CMAudioFormatDescriptionCreate(allocator: kCFAllocatorDefault, asbd: &audioFormat, layoutSize: 0, layout: nil, magicCookieSize: 0, magicCookie: nil, extensions: nil, formatDescriptionOut: &format)
        if status != noErr {
            print("Error CMAudioFormatDescriptionCreater :\(String(describing: status?.description))")
            return
        }


        print(">> Audio Buffer mSampleRate:\(Int32(audioFormat.mSampleRate))")
        var timing = CMSampleTimingInfo(duration: CMTimeMake(value: 1, timescale: Int32(audioFormat.mSampleRate)), presentationTimeStamp: self.player.currentTime(), decodeTimeStamp: CMTime.invalid)


        status = CMSampleBufferCreate(allocator: kCFAllocatorDefault,
                                      dataBuffer: nil,
                                      dataReady: Bool(truncating: 0),
                                      makeDataReadyCallback: nil,
                                      refcon: nil,
                                      formatDescription: format,
                                      sampleCount: CMItemCount(framesNumber),
                                      sampleTimingEntryCount: 1,
                                      sampleTimingArray: &timing,
                                      sampleSizeEntryCount: 0, sampleSizeArray: nil,
                                      sampleBufferOut: &sbuf);
        if status != noErr {
            print("Error CMSampleBufferCreate :\(String(describing: status?.description))")
            return
        }
        status =   CMSampleBufferSetDataBufferFromAudioBufferList(sbuf!,
                                                                  blockBufferAllocator: kCFAllocatorDefault ,
                                                                  blockBufferMemoryAllocator: kCFAllocatorDefault,
                                                                  flags: 0,
                                                                  bufferList: audioData)
        if status != noErr {
            print("Error cCMSampleBufferSetDataBufferFromAudioBufferList :\(String(describing: status?.description))")
            return
        }

        let currentSampleTime = CMSampleBufferGetOutputPresentationTimeStamp(sbuf!);
        print(" audio buffer at time: \(currentSampleTime)")
        self.delegate?.videoFrameRefresh(sampleBuffer: sbuf!)

    }


}

我如何使用课程

self.inputVideoMedia = nil
self.inputVideoMedia = VideoMediaInput(url: videoURL)
self.inputVideoMedia!.delegate = self

我第二次这样做..它崩溃了(但不总是如此)。它不会崩溃的时间我可以在控制台中看到FINALIZE打印。

2 个答案:

答案 0 :(得分:1)

如果VideoMediaInput在抽头被重新分配之前 (这可能发生,因为似乎无法同步停止抽头),则抽头回调可能会阻塞引用到您已取消分配的课程。

您可以通过将弱引用传递给您的类来解决此问题。您可以这样做:

首先删除您的tap实例变量以及对其的任何引用-不需要它。然后进行以下更改:

class VideoMediaInput: NSObject {

    class TapCookie {
        weak var input: VideoMediaInput?

        deinit {
            print("TapCookie deinit")
        }
    }
...

    func setupProcessingTap(){
        let cookie = TapCookie()
        cookie.input = self

        var callbacks = MTAudioProcessingTapCallbacks(
            version: kMTAudioProcessingTapCallbacksVersion_0,
            clientInfo: UnsafeMutableRawPointer(Unmanaged.passRetained(cookie).toOpaque()),
            init: tapInit,
            finalize: tapFinalize,
            prepare: tapPrepare,
            unprepare: tapUnprepare,
            process: tapProcess)
...


    let tapFinalize: MTAudioProcessingTapFinalizeCallback = {
        (tap) in
        print("finalize \(tap)\n")

       // release cookie
        Unmanaged<TapCookie>.fromOpaque(MTAudioProcessingTapGetStorage(tap)).release()
    }


    let tapPrepare: MTAudioProcessingTapPrepareCallback = {
        (tap, itemCount, basicDescription) in
        print("prepare: \(tap, itemCount, basicDescription)\n")
        let cookie = Unmanaged<TapCookie>.fromOpaque(MTAudioProcessingTapGetStorage(tap)).takeUnretainedValue()
        let selfMediaInput = cookie.input!
...

    let tapProcess: MTAudioProcessingTapProcessCallback = {
        (tap, numberFrames, flags, bufferListInOut, numberFramesOut, flagsOut) in
        print("callback \(bufferListInOut)\n")

        let cookie = Unmanaged<TapCookie>.fromOpaque(MTAudioProcessingTapGetStorage(tap)).takeUnretainedValue()

        guard let selfMediaInput = cookie.input else {
            print("Tap callback: VideoMediaInput was deallocated!")
            return
        }
...

我不确定cookie类是否是必需的,它仅用于包装weak引用。先进的Swift专家可能会知道如何通过所有青少年突变忍者原始指针消除弱点,但我不知道。

答案 1 :(得分:1)

音频上下文在其自己的实时线程中运行。因此,音频进程不会与停止或取消函数调用同步停止,而是在实时线程耗尽之后的某个未知时间之后(以某些内部音频缓冲区中一些音频样本的持续时间为顺序)停止。 / p>

因此,在停止任何实时音频流之后的一段时间(未知,但少于几秒钟)之前,不应释放(或重新分配)音频缓冲区,对象和回调。

据报道,在Swift中,取决于实时线程之间的释放对象消息或实例变量状态(包括弱引用),当前在Swift中是不安全的(有关音频,请参阅WWDC 2018会话)。因此,我建议使用信号量(在实时上下文之外,例如音频)或posix内存屏障(在对C函数的桥接调用中)。 (...直到Swift的将来某个版本都可以找到实时并发机制。)(...尤其是在可以对内存写入进行重新排序的iOS设备上。)