寫一個(gè)iOS中可以方便實(shí)現(xiàn)自定義相機(jī)(QQ,微信錄小視頻...)的CameraView

前言

為什么寫了一個(gè)這個(gè)自定義的相機(jī)(拍照, 錄視頻), 因?yàn)橐恢笔褂肣Q,微信等發(fā)現(xiàn)這個(gè)錄制小視頻的相機(jī), 還有就是一些相機(jī)軟件的界面和系統(tǒng)的是不一樣的, 明顯是自定義后實(shí)現(xiàn)的, 也想要實(shí)現(xiàn)一個(gè). 不過看了看是AVFoundation里面的東西, 平時(shí)很少用到, 也就沒有怎么去研究這個(gè)東西. 但是最近了, 正好研究直播相關(guān)的東西, 這不是不得不研究了AVFoundation, 然后寫了這么一個(gè)自定義的相機(jī). 初步寫完之后, 其實(shí)發(fā)現(xiàn)基于AVFoundation還是很好實(shí)現(xiàn)自己寫一個(gè)相機(jī)的. Demo地址

最終使用的效果之一

Snip20160710_1.png

一, 使用AVFoundation自定義相機(jī)的時(shí)候, 你需要首先掌握這些東西, 幸運(yùn)的是: 不是很多, 下面依次介紹一下

Snip20160708_11.png
  • 了解AVCaptureSession

AVCaptureSession可以被稱作一個(gè)資源調(diào)度者(自我理解的!!), 他是用來增加或移除輸入和輸出設(shè)備(真實(shí)的物理設(shè)備被抽象成的虛擬設(shè)備)的, 同時(shí)他負(fù)責(zé)開始和停止資源文件的輸入, 同時(shí)需要注意的是, 當(dāng)你需要配置AVCaptureSession的其他的時(shí)候, 你需要使用[session beginConfiguration];, 然后配置完成之后 調(diào)用 [session commitConfiguration];, 只有當(dāng)commit之后配置才會(huì)生效
另外一點(diǎn)是當(dāng)你需要開始的時(shí)候[session startRunning]; 這是一個(gè)耗時(shí)的操作, 你應(yīng)該另開線程來開啟

[session beginConfiguration];

移除原來的輸入/輸出設(shè)備
// 在切換設(shè)備之前, 需要先移除原來的在添加新的
self.session.removeInput(self.videoDeviceInput)
self.session.removeOutput(movieFileOutput)
增加新的輸入/輸出設(shè)備
        // 需要首先判斷是否能夠添加相應(yīng)的設(shè)備
        // 添加輸入設(shè)備
        if self.session.canAddInput(inputDevice) {
            self.session.addInput(inputDevice)
        } else {
            print("can not add the input devices-- \(String(inputDevice))")
        }
        // 添加輸出(圖片/視頻)
        if self.session.canAddOutput(stillImageOutput) {
            self.session.addOutput(stillImageOutput)
        } else {
            print("can not add stillImageOutput !!")
            
        }
設(shè)置畫質(zhì)(low, medium, high, photo,...)
        let preset = AVCaptureSessionPresetMedium
        }
        //判斷是否能夠設(shè)置
        if session.canSetSessionPreset(preset) {
            session.sessionPreset = preset
        }
[session commitConfiguration];
  • 了解 AVCaptureDevice

一個(gè)AVCaptureDevice對(duì)象相應(yīng)的代表一個(gè)輸入物理設(shè)備, 比如iPhone上會(huì)有, 前后攝像頭設(shè)備, 音頻輸入設(shè)備, 都被抽象成一個(gè)AVCaptureDevice, 通常情況下你會(huì)使用AVCaptureDevice提供的一些方法獲取到當(dāng)前支持所有的設(shè)備, 然后通過AVCaptureDevicePositionBack或者AVCaptureDevicePositionFront來區(qū)分前后的攝像頭. 例如, 如下方式獲取到后面的相機(jī)

        let devices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo) as![AVCaptureDevice]
        for device in devices {
            if device.position == AVCaptureDevicePosition.Back {
                 return device
            }
        }
  • 了解AVCaptureDeviceInput

AVCaptureDevice并不是直接添加到AVCaptureSession中的, 而是通過一個(gè)AVCaptureDevice初始化一個(gè)AVCaptureDeviceInput, 這個(gè)時(shí)候才相當(dāng)于是AVCaptureSession中的一個(gè)輸入設(shè)備, 需要注意的是, 這是一個(gè)拋出異常的操作


        let devices = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo) as![AVCaptureDevice]
        var deviceInput: AVCaptureDeviceInput? = nil
        for device in devices {
            if device.position == position {
                deviceInput = try? AVCaptureDeviceInput(device: device)
                break
            }
        }
  • 了解 AVCaptureVideoPreviewLayer

從后綴名previewLayer就可以猜到, 他是一個(gè)CALayer的子類, 適用于你將當(dāng)前的AVCaptureSession輸入設(shè)備采集到的內(nèi)容展示到屏幕上, 所以你可以自定義他的一些屬性, 比如 frame可以自定義位置, videoGravity可以設(shè)置畫面的顯示方式...


    private lazy var previewLayer: AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: self.session)

        // set the resize model
        previewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill
        // add the previewLayer
        layer.addSublayer(previewLayer)
        clipsToBounds = true
  • 了解AVCaptureOutput

這個(gè)適用于我們管理輸出文件的類型, 比如可以選擇是 stillImageOutput, movieFileOutput, 決定我們是要輸出圖片還是視頻, 當(dāng)然同樣需要注意的是, 切換輸出類型的時(shí)候需要先移除之前的再添加新的.


    private func change(mediaType mediaType: OutputMediaType) {
        if mediaType == .stillImage {
            self.session.removeOutput(movieFileOutput)
            self.session.addOutput(stillImageOutput)
        } else {
            self.session.removeOutput(stillImageOutput)
            self.session.addOutput(movieFileOutput)
            
        }
    }

下面介紹一些相機(jī)的配置的操作, 例如閃光燈, 聚焦模式, 曝光模式, 畫面畫質(zhì)... 需要注意的是: 配置之前需要首先申請(qǐng)lockForConfiguration(), 成功之后才開始配置, 配置完成之后應(yīng)該unlockForConfiguration(), 不然可能會(huì)影響到其他的設(shè)備lockForConfiguration()

  • 改變 聚焦模式(自動(dòng)聚焦, 持續(xù)聚焦...), 曝光模式(自動(dòng), 持續(xù)...),
    private func change(focusModel focusModel: AVCaptureFocusMode, exposureModel: AVCaptureExposureMode, at point: CGPoint, isMonitor: Bool) {
        if let device = videoDeviceInput?.device {
            do {
                // must lock it or it may causes crashing
                try device.lockForConfiguration()
                
                if device.focusPointOfInterestSupported && device.isFocusModeSupported(focusModel) {
                    // 設(shè)置聚焦的點(diǎn),注意(0,0)代表屏幕左上角 (1,1)代表屏幕右下角,同時(shí)設(shè)置點(diǎn)之后需要設(shè)置聚焦模式才會(huì)生效 
                    device.focusPointOfInterest = point
                    device.focusMode = focusModel
                }
                
                if device.exposurePointOfInterestSupported && device.isExposureModeSupported(exposureModel) {
                    // only when setting the exposureMode after setting exposurePointOFInterest can be successful
                    // 設(shè)置曝光的點(diǎn),注意(0,0)代表屏幕左上角 (1,1)代表屏幕右下角,同時(shí)設(shè)置點(diǎn)之后需要設(shè)置曝光模式才會(huì)生效 
                    device.exposurePointOfInterest = point
                    device.exposureMode = exposureModel
                }
                // only when set it true can we receive the AVCaptureDeviceSubjectAreaDidChangeNotification
                device.subjectAreaChangeMonitoringEnabled = isMonitor
                device.unlockForConfiguration()
                
                
            } catch {
                print("cannot change the focusModel")
            
            }
        }
    }
  • 改變閃光燈, 注意并不是所有的設(shè)備都有閃光燈, 所以, 設(shè)置之前必須要判斷是否有閃光燈(hasFlash), 并且要判斷是否能支持將要設(shè)置的閃光燈模式(isFlashModeSupported)

    private func change(flashModel flashModel: FlashModel) {
        let avFlashModel = flashModel.changeToAvFlashModel()
        //要判斷是否有閃光燈(hasFlash), 并且要判斷是否能支持將要設(shè)置的閃光燈模式(isFlashModeSupported)
        if let trueVideoDevice = videoDeviceInput?.device where trueVideoDevice.hasFlash && trueVideoDevice.isFlashModeSupported(avFlashModel) {
            
            do {
                try trueVideoDevice.lockForConfiguration()
                trueVideoDevice.flashMode = avFlashModel
                trueVideoDevice.unlockForConfiguration()
            } catch {
                print("can not lock the device for configuration!! ---\(error)")
            }
            
        }
    }
  • 改變畫質(zhì), 當(dāng)然需要注意的是需要判斷當(dāng)前的設(shè)備是否支持所指定的畫質(zhì), 同時(shí)不同的畫質(zhì),是影響當(dāng)前設(shè)備的最大縮放倍數(shù)的device.activeFormat.videoMaxZoomFactor(當(dāng)為1的時(shí)候代表不能縮放, 同時(shí)操作最大的倍數(shù)時(shí)候會(huì)crash)

public let AVCaptureSessionPresetPhoto: String
public let AVCaptureSessionPresetHigh: String
public let AVCaptureSessionPresetMedium: String
public let AVCaptureSessionPresetLow: String
public let AVCaptureSessionPreset352x288: String
public let AVCaptureSessionPreset640x480: String
public let AVCaptureSessionPreset1280x720: String
public let AVCaptureSessionPreset1920x1080: String
public let AVCaptureSessionPreset3840x2160: String
public let AVCaptureSessionPresetiFrame960x540: String
public let AVCaptureSessionPresetiFrame1280x720: String

    private func change(mediaQuality mediaQuality: MediaQuality) {

        if session.canSetSessionPreset(AVCaptureSessionPresetHigh) {
            session.sessionPreset = AVCaptureSessionPresetHigh
        }
    }

最后當(dāng)然需要知道怎么開始采集數(shù)據(jù)和得到采集的數(shù)據(jù)

  • 拍照 需要使用添加到當(dāng)前AVCaptureSession的stillImageOutput來異步開始拍照并且獲得數(shù)據(jù)(格式為CMSampleBuffer, 需要轉(zhuǎn)換為自己需要的格式
self.stillImageOutput?.captureStillImageAsynchronouslyFromConnection(connection, completionHandler: { (buffer, error) in
                // 這是采集到的數(shù)據(jù)
                if buffer != nil {
                    
                    // 轉(zhuǎn)換為NSData
                    let imageData = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer)
                    //轉(zhuǎn)換為UIImage
                    let image = UIImage(data: imageData)
                }
}

  • 錄視頻, 錄制視頻是需要首先指定視頻的保存位置的, 同時(shí)指定的位置不能是已經(jīng)存在的文件的額位置, 否則將會(huì)失敗(下面示例將文件存到沙盒文件的NSTemporaryDirectory文件夾下面), 同時(shí)需要設(shè)置代理, 代理方法會(huì)有響應(yīng)開始錄制的代理方法和結(jié)束錄制的代理方法(可以判斷是否錄制成功...)

        let tempFileName = "\(NSProcessInfo().globallyUniqueString).mov"
        let tempFilePath = (NSTemporaryDirectory() as NSString).stringByAppendingPathComponent(tempFileName)
        let tempVideoURL = NSURL(fileURLWithPath: tempFilePath)
        // 開始錄制, 這里會(huì)調(diào)用開始錄制的代理
        movieFileOutput?.startRecordingToOutputFileURL(tempVideoURL, recordingDelegate: self)

結(jié)束錄制, 之后會(huì)調(diào)用結(jié)束錄制的代理方法

  movieFileOutput?.stopRecording()

AVCaptureFileOutputRecordingDelegate代理方法

// MARK:- public AVCaptureFileOutputRecordingDelegate
extension CameraView: AVCaptureFileOutputRecordingDelegate {
// 開始錄制
    public func captureOutput(captureOutput: AVCaptureFileOutput!, didStartRecordingToOutputFileAtURL fileURL: NSURL!, fromConnections connections: [AnyObject]!) {
    }
   
// 完成錄制
    public func captureOutput(captureOutput: AVCaptureFileOutput!, didFinishRecordingToOutputFileAtURL outputFileURL: NSURL!, fromConnections connections: [AnyObject]!, error: NSError!) {
        
        var success = true
// 需要注意的是, 有時(shí)候視頻錄制成功, 這個(gè)error仍然不會(huì)為nil, 所以通過如下方式需要判斷是否真的錄制成功
        if error != nil {// sometimes there may be error but the video is caputed successfully
            success = error.userInfo[AVErrorRecordingSuccessfullyFinishedKey] as! Bool
        }
        // 錄制成功
        if (success) {
        // 可以把相應(yīng)的文件轉(zhuǎn)到其他的位置

 }
}

到目前為止, 就將自定義相機(jī)所需要的基本的東西介紹完畢了, 你可以參考官方文檔的介紹了解更多的知識(shí). 這里提供一個(gè)筆者已經(jīng)寫好的一個(gè)自定義相機(jī)的,Demo地址 如果有幫助,歡迎star, 歡迎關(guān)注

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末外傅,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌檀蹋,老刑警劉巖铐伴,帶你破解...
    沈念sama閱讀 224,764評(píng)論 6 522
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件逝薪,死亡現(xiàn)場(chǎng)離奇詭異踱启,居然都是意外死亡报账,警方通過查閱死者的電腦和手機(jī)研底,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 96,235評(píng)論 3 402
  • 文/潘曉璐 我一進(jìn)店門埠偿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人榜晦,你說我怎么就攤上這事冠蒋。” “怎么了乾胶?”我有些...
    開封第一講書人閱讀 171,965評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵抖剿,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我识窿,道長(zhǎng)斩郎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,984評(píng)論 1 300
  • 正文 為了忘掉前任喻频,我火速辦了婚禮缩宜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘甥温。我一直安慰自己锻煌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,984評(píng)論 6 399
  • 文/花漫 我一把揭開白布姻蚓。 她就那樣靜靜地躺著宋梧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪狰挡。 梳的紋絲不亂的頭發(fā)上捂龄,一...
    開封第一講書人閱讀 53,471評(píng)論 1 314
  • 那天释涛,我揣著相機(jī)與錄音,去河邊找鬼跺讯。 笑死枢贿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的刀脏。 我是一名探鬼主播局荚,決...
    沈念sama閱讀 41,844評(píng)論 3 428
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼愈污!你這毒婦竟也來了耀态?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,818評(píng)論 0 279
  • 序言:老撾萬榮一對(duì)情侶失蹤暂雹,失蹤者是張志新(化名)和其女友劉穎首装,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杭跪,經(jīng)...
    沈念sama閱讀 47,359評(píng)論 1 324
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡仙逻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 39,385評(píng)論 3 346
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涧尿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片系奉。...
    茶點(diǎn)故事閱讀 41,515評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖姑廉,靈堂內(nèi)的尸體忽然破棺而出缺亮,到底是詐尸還是另有隱情,我是刑警寧澤桥言,帶...
    沈念sama閱讀 37,114評(píng)論 5 350
  • 正文 年R本政府宣布萌踱,位于F島的核電站,受9級(jí)特大地震影響号阿,放射性物質(zhì)發(fā)生泄漏并鸵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,836評(píng)論 3 338
  • 文/蒙蒙 一扔涧、第九天 我趴在偏房一處隱蔽的房頂上張望园担。 院中可真熱鬧,春花似錦扰柠、人聲如沸粉铐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 33,291評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至迫摔,卻和暖如春劝枣,著一層夾襖步出監(jiān)牢的瞬間汤踏,已是汗流浹背织鲸。 一陣腳步聲響...
    開封第一講書人閱讀 34,422評(píng)論 1 275
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留溪胶,地道東北人搂擦。 一個(gè)月前我還...
    沈念sama閱讀 50,064評(píng)論 3 381
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像哗脖,于是被迫代替她去往敵國(guó)和親瀑踢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 46,581評(píng)論 2 365

推薦閱讀更多精彩內(nèi)容