音視頻采集學(xué)習(xí)筆記(二)

前言

上一篇介紹UIImagePickerController 和AVCaptureSession+AVCaptureMovieFileOutput(音視頻采集學(xué)習(xí)筆記(一)), 這篇介紹AVCaptureSession + AVCaptureVideoDataOutput +AVAssetWriter的實(shí)現(xiàn)盈咳,效果圖如下所示 (采集實(shí)現(xiàn)Demo)

AVCaptureSession + AVCaptureVideoDataOutput+AVAssetWriter

音視頻輸入和輸出都是通過(guò)AVCaptureSession來(lái)管理碰凶,通過(guò)AVCaptureVideoDataOutput可以實(shí)時(shí)的獲取采集的每一幀的數(shù)據(jù)進(jìn)行一系列的操作(增加濾鏡,根據(jù)設(shè)備方向轉(zhuǎn)動(dòng)視圖等) 炕檩,最后通過(guò)AVAssetWriter進(jìn)行編碼處理窖剑,存儲(chǔ)

流程
1.創(chuàng)建捕捉會(huì)話
2.設(shè)置視頻的輸入 
3.設(shè)置音頻的輸入
4.設(shè)置視頻的輸出
5.設(shè)置音頻的輸出
6.添加視頻預(yù)覽層
7.添加濾鏡
8.寫(xiě)入音視頻數(shù)據(jù)
  • 創(chuàng)建捕捉會(huì)話
fileprivate lazy var session : AVCaptureSession = {
        let session = AVCaptureSession()
        session.sessionPreset = .vga640x480
        return session
}()
  • 視頻的輸入
    @available(iOS 10.2, *)
    func setUpVideo(position: AVCaptureDevice.Position) {
        currentDevicePosition = position
        // 視頻輸入設(shè)備
        let videoCaptureDevice = AVCaptureDevice.default(.builtInWideAngleCamera,
                                                         for: .video,
                                                         position: position)

        // 視頻輸入源
        do {
            videoInput = try AVCaptureDeviceInput.init(device: videoCaptureDevice!)
        } catch  {

        }

        if self.session.canAddInput(videoInput) {
            self.session.addInput(videoInput)
        }


        if let dataOutPut = dataOutPut {
            let connection = dataOutPut.connection(with: .video)
            // 相機(jī)前置設(shè)置鏡像
            connection?.isVideoMirrored = currentDevicePosition == .front
        }
    }
  • 音頻的輸入
    func setUpAudio() {
        let audioCaptureDevice = AVCaptureDevice.default(for: .audio)

        do {
            audioInput = try AVCaptureDeviceInput(device: audioCaptureDevice!)
        } catch {

        }

        if session.canAddInput(audioInput) {
            self.session.addInput(audioInput)
        }
    }
  • 視頻的輸出
func setUpDataOutPut() {
        dataOutPut = AVCaptureVideoDataOutput()
        dataOutPut.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String : Int(kCVPixelFormatType_32BGRA)]
        dataOutPut.alwaysDiscardsLateVideoFrames = true

        if session.canAddOutput(dataOutPut) {
            session.addOutput(dataOutPut)
        }

        dataOutPut.setSampleBufferDelegate(self, queue: DispatchQueue(label: "VideoQueue"))
}
  • 音頻的輸出
    func setUpAudioOutPut() {
        let audioOutPut = AVCaptureAudioDataOutput()

        if session.canAddOutput(audioOutPut) {
            session.addOutput(audioOutPut)
        }

        audioOutPut.setSampleBufferDelegate(self, queue: DispatchQueue(label: "audioQueue"))
     }
  • 視頻預(yù)覽層
    func setUpLayerView() {
        previewLayer = CALayer()
        previewLayer.anchorPoint = CGPoint.zero
        // 默認(rèn)為手機(jī)水平坚洽,home在右邊的方向, 所以previewLayer的寬對(duì)應(yīng)sessionPreset的高西土,高度同理
        previewLayer.frame = CGRect.init(x: 0, y: 75, width: 480, height: 640)
        self.view.contentMode = .scaleAspectFit
        self.view.layer.insertSublayer(previewLayer, at: 0)
    }
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
 // 采集后的圖像是未編碼的CMSampleBuffer形式讶舰,利用給定的接口函數(shù)CMSampleBufferGetImageBuffer從中提取出CVPixelBufferRef
        let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)CMSampleBufferGetImageBuffer(sampleBuffer)
        // 實(shí)例CIImage
        var outImage = CIImage(cvPixelBuffer: imageBuffer!)

        // CIFilter濾鏡并給濾鏡設(shè)置屬性(KVC)
        if filter != nil {
            filter.setValue(outImage, forKey: kCIInputImageKey)
            outImage = filter.outputImage!
        }

        // 根據(jù)設(shè)備方向轉(zhuǎn)動(dòng)視圖
        if orientation == UIDeviceOrientation.portrait {
            let rotationAngle = currentDevicePosition == .front ? CGFloat.pi / 2.0  : -CGFloat.pi / 2.0
            t = CGAffineTransform(rotationAngle: rotationAngle)
        } else if orientation == UIDeviceOrientation.portraitUpsideDown {
            t = CGAffineTransform(rotationAngle: CGFloat.pi / 2.0)
        } else if (orientation == UIDeviceOrientation.landscapeRight) {
            t = CGAffineTransform(rotationAngle: CGFloat.pi)
        } else {
            t = CGAffineTransform(rotationAngle: 0)
        }

        outImage = outImage.transformed(by: t)

        // context 轉(zhuǎn)化為CGImage (非位圖圖片,不能保存到相冊(cè)需了,不能轉(zhuǎn)換為NSData (jpeg png))
        let cgImage = context.createCGImage(outImage, from: outImage.extent)

        // 賦給previewLayer進(jìn)行顯示
        DispatchQueue.main.async {
            self.previewLayer.contents = cgImage
        }
}

CIContext的創(chuàng)建

    lazy var context: CIContext = {
        let eaglContext = EAGLContext(api: EAGLRenderingAPI.openGLES2)
        let options = [CIContextOption.workingColorSpace : NSNull()]
        return CIContext(eaglContext: eaglContext!, options: options)
    }()
處理前置鏡像問(wèn)題(呈像相反):

默認(rèn)系統(tǒng)前置相機(jī)是非鏡像的跳昼,就是呈現(xiàn)的圖像為鏡子相反的,因?yàn)橹髁饔脩舳剂?xí)慣這個(gè)鏡像功能肋乍,所以我們?cè)谝曨l的輸入的時(shí)候設(shè)置了前置isVideoMirrored為true鹅颊,這時(shí)候前置就為鏡像了,由此也有一個(gè)問(wèn)題墓造,這時(shí)候當(dāng)設(shè)備方向?yàn)榇怪保╤ome鍵在底部)堪伍,前置呈像為右锚烦,后置為左涮俄,所以通過(guò)設(shè)備方向轉(zhuǎn)動(dòng)視圖需要根據(jù)相機(jī)方向來(lái)逆時(shí)針轉(zhuǎn)動(dòng)或順時(shí)針轉(zhuǎn)動(dòng)

關(guān)于轉(zhuǎn)動(dòng)視圖的問(wèn)題 :

這里寫(xiě)的是根據(jù)設(shè)備方向從而去轉(zhuǎn)動(dòng)視圖顯示刘离,這樣實(shí)現(xiàn)也可以硫惕,但是有一個(gè)弊端踪旷,錄制中無(wú)法轉(zhuǎn)動(dòng)相機(jī)方向令野,否則其中部分畫(huà)面反的,而且實(shí)現(xiàn)更麻煩餐抢,通過(guò)下一章GpuImage2的學(xué)習(xí)碳锈,發(fā)現(xiàn)通過(guò)AVCaptureConnection的videoOrientation屬性設(shè)置portrait就可以自動(dòng)實(shí)現(xiàn)根據(jù)設(shè)備方向而轉(zhuǎn)動(dòng) (AVCaptureConnection)

        if let dataOutPut = dataOutPut {
            let connection = dataOutPut.connection(with: .video)
            connection?.videoOrientation = .portrait
        }
  • 寫(xiě)入音視頻數(shù)據(jù)
流程
1.創(chuàng)建AssetWriter
2.創(chuàng)建AssetWriterVideoInput
3.創(chuàng)建AssetWriterPixelBufferInput
4.創(chuàng)建AssetWriterAudioInput
5.添加音視頻寫(xiě)入的輸出到AssetWriter里绞呈,開(kāi)始寫(xiě)入
6.AVCaptureVideoDataOutputSampleBufferDelegate實(shí)現(xiàn)音視頻數(shù)據(jù)寫(xiě)入

(1) 創(chuàng)建AssetWriter

        // 移除之前存儲(chǔ)的數(shù)據(jù)
        if fileManager.fileExists(atPath: self.videoStoragePath()) {
            do {
                try fileManager.removeItem(atPath: self.videoStoragePath())
            } catch {

            }
        }

        let path = self.videoStoragePath()
        videoUrl = URL.init(fileURLWithPath: path)

        do {
           assetWriter = try AVAssetWriter.init(outputURL: videoUrl as URL, fileType: .mov)
        } catch  {

        }

(2) 創(chuàng)建AssetWriterVideoInput (音視頻開(kāi)發(fā)概念篇

        let numPixels = SCREEN_WIDTH * SCREEN_HEIGHT
        let bitsPerPixel = 6.0 as CGFloat
        let bitsPerSecond =  numPixels * bitsPerPixel
        let compressionProperties = [AVVideoAverageBitRateKey : bitsPerSecond,// 比特率(碼率)
                                     AVVideoExpectedSourceFrameRateKey : 30,// 幀率
                                     AVVideoMaxKeyFrameIntervalKey : 30,
                                     AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel
            ] as [String : Any]

        //視頻屬性
        let videoCompressionSettings = [AVVideoCodecKey : AVVideoCodecType.h264, // 編碼格式,一般選h264,硬件編碼
            AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill, // 填充模式
            AVVideoWidthKey : 1920,  // 視頻寬度碗誉,以手機(jī)水平,home 在右邊的方向
            AVVideoHeightKey : 1080, // 視頻高度尝苇,以手機(jī)水平,home 在右邊的方向
            AVVideoCompressionPropertiesKey : compressionProperties] as [String : Any]

        assetWriterVideoInput = AVAssetWriterInput(mediaType: .video,
                                                   outputSettings: videoCompressionSettings)
        //expectsMediaDataInRealTime 必須設(shè)為true非竿,需要從capture session 實(shí)時(shí)獲取數(shù)據(jù)
        assetWriterVideoInput.expectsMediaDataInRealTime = true;

        let rotationAngle = currentDevicePosition == .front ? CGFloat.pi / 2.0  : -CGFloat.pi / 2.0
        // 視頻寫(xiě)入輸出轉(zhuǎn)動(dòng)方向
        assetWriterVideoInput.transform = CGAffineTransform(rotationAngle: rotationAngle);

(3) 創(chuàng)建AssetWriterPixelBufferInput

        let sourcePixelBufferAttributesDictionary = [
            String(kCVPixelBufferPixelFormatTypeKey) : Int(kCVPixelFormatType_32BGRA), // 像素格式類型
            String(kCVPixelBufferWidthKey) : 1920,
            String(kCVPixelBufferHeightKey) : 1080,
            // 允許在 OpenGL 的上下文中直接繪制解碼后的圖像蓖乘,而不是從總線和 CPU 之間復(fù)制數(shù)據(jù)嘉抒。這有時(shí)候被稱為零拷貝通道握牧,因?yàn)樵诶L制過(guò)程中沒(méi)有解碼的圖像被拷貝
            String(kCVPixelFormatOpenGLESCompatibility) : kCFBooleanTrue
            ] as [String : Any]

        assetWriterPixelBufferInput = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: assetWriterVideoInput,
                                                                           sourcePixelBufferAttributes: sourcePixelBufferAttributesDictionary)

(4) 創(chuàng)建AssetWriterAudioInput

let audioCompressionSettings = [AVEncoderBitRatePerChannelKey : 28000,// 編碼時(shí)每個(gè)通道的比特率
            AVFormatIDKey : kAudioFormatMPEG4AAC,// 音頻格式
            AVNumberOfChannelsKey : 1,// 通道數(shù)1為單通道2為立體通道
            AVSampleRateKey : 22050]// 采樣率輸入的模擬音頻信號(hào)每一秒的采樣數(shù)是影響音頻質(zhì)量和音頻文件大小非常重要的一個(gè)因素采樣率越小文件越小質(zhì)量越低如44.1kHz

        assetWriterAudioInput = AVAssetWriterInput.init(mediaType: .audio,
                                                        outputSettings: audioCompressionSettings)

(5) 添加音視頻寫(xiě)入輸出到AssetWriter

        if assetWriter.canAdd(assetWriterVideoInput) {
            assetWriter.add(assetWriterVideoInput)
        }


        if assetWriter.canAdd(assetWriterAudioInput) {
            assetWriter.add(assetWriterAudioInput)
        }

        assetWriter.startWriting()
        self.assetWriter.startSession(atSourceTime: self.currentSampleTime)
        self.isStart = true

(6) AVCaptureVideoDataOutputSampleBufferDelegate實(shí)現(xiàn)音視頻數(shù)據(jù)寫(xiě)入, 通過(guò)媒體類型判斷分別寫(xiě)入

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        // 寫(xiě)入音頻
        if mediaType == kCMMediaType_Audio {

            guard isStart else {
                return
            }

            self.assetWriterAudioInput.append(sampleBuffer)
            return
        }
        // 寫(xiě)入視頻數(shù)據(jù)
        if isStart {
            // 緩沖區(qū)中的數(shù)據(jù)是否已經(jīng)處理完成
            if (self.assetWriterPixelBufferInput?.assetWriterInput.isReadyForMoreMediaData)! {
                var newPixelBuffer: CVPixelBuffer? = nil

                CVPixelBufferPoolCreatePixelBuffer(nil, self.assetWriterPixelBufferInput!.pixelBufferPool!, &newPixelBuffer)

                self.context.render(outImage, to: newPixelBuffer!, bounds: outImage.extent, colorSpace: nil)

                let success = self.assetWriterPixelBufferInput?.append(newPixelBuffer!, withPresentationTime: self.currentSampleTime!)

                if success == false {
                    print("Pixel Buffer沒(méi)有附加成功")
                }
            }
        }
}
擴(kuò)展:

AssetWriterPixelBufferInput (用法及理解)
H264視頻硬件編解碼說(shuō)明 (CMSampleBuffer結(jié)構(gòu)以及編解碼使用)

特別感謝以上分享文章的朋友

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纽什,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子浪规,更是在濱河造成了極大的恐慌笋婿,老刑警劉巖缸濒,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異滔驶,居然都是意外死亡锻霎,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)蜀细,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)戈盈,“玉大人奠衔,你說(shuō)我怎么就攤上這事√寥ⅲ” “怎么了归斤?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)刁岸。 經(jīng)常有香客問(wèn)我脏里,道長(zhǎng),這世上最難降的妖魔是什么虹曙? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任迫横,我火速辦了婚禮鸦难,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘员淫。我一直安慰自己合蔽,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布介返。 她就那樣靜靜地躺著拴事,像睡著了一般。 火紅的嫁衣襯著肌膚如雪圣蝎。 梳的紋絲不亂的頭發(fā)上刃宵,一...
    開(kāi)封第一講書(shū)人閱讀 49,816評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音徘公,去河邊找鬼牲证。 笑死,一個(gè)胖子當(dāng)著我的面吹牛关面,可吹牛的內(nèi)容都是我干的坦袍。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼等太,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼捂齐!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起缩抡,我...
    開(kāi)封第一講書(shū)人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤奠宜,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后瞻想,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體压真,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年蘑险,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滴肿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡漠其,死狀恐怖嘴高,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情和屎,我是刑警寧澤拴驮,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站柴信,受9級(jí)特大地震影響套啤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一潜沦、第九天 我趴在偏房一處隱蔽的房頂上張望萄涯。 院中可真熱鬧,春花似錦唆鸡、人聲如沸涝影。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)燃逻。三九已至,卻和暖如春臂痕,著一層夾襖步出監(jiān)牢的瞬間伯襟,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工握童, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留姆怪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓澡绩,卻偏偏與公主長(zhǎng)得像稽揭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子英古,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348