iOS - 创建多个延时实时摄像机预览视图

Posted

技术标签:

【中文标题】iOS - 创建多个延时实时摄像机预览视图【英文标题】:iOS - Creating multiple time-delayed live camera preview views 【发布时间】:2018-07-31 05:34:41 【问题描述】:

我已经进行了大量的研究,但由于多种原因,我还没有找到可行的解决方案,我将在下面概述。


问题

在我的 ios 应用中,我希望三个视图无限期地显示设备相机的延迟实时预览。

例如,视图 1 将显示相机视图,延迟 5 秒,视图 2 将显示相同的相机视图,延迟 20 秒,视图 3 将显示相同的相机视图,延迟 30 秒。

这将用于记录自己进行某种活动,例如锻炼,然后在几秒钟后观察自己,以完善给定锻炼的形式。

尝试的解决方案

我尝试并研究了几种不同的解决方案,但都有问题。

1。使用AVFoundationAVCaptureMovieFileOutput

使用AVCaptureSessionAVCaptureMovieFileOutput 将短片记录到设备存储中。短片是必需的,因为您不能从一个 URL 播放视频,并同时写入同一个 URL。 拥有 3 个 AVPlayerAVPlayerLayer 实例,它们都以所需的时间延迟播放录制的短片段。 问题:
    使用AVPlayer.replaceCurrentItem(_:) 切换剪辑时,剪辑之间存在非常明显的延迟。这需要平稳过渡。 虽然很旧,但由于设备限制,here 的评论建议不要创建多个 AVPlayer 实例。我无法找到确认或否认这一说法的信息。 E:来自 Jake G 的评论 - 10 个AVPlayer 实例适用于 iPhone 5 及更新版本。

2。使用AVFoundationAVCaptureVideoDataOutput

使用AVCaptureSessionAVCaptureVideoDataOutput 使用didOutputSampleBuffer 委托方法流式传输和处理相机的每一帧。 在 OpenGL 视图上绘制每一帧(例如GLKViewWithBounds)。这解决了来自Solution 1. 的多个AVPlayer 实例的问题。 问题:存储每一帧以便以后显示它们需要大量内存(这在 iOS 设备上是不可行的)或磁盘空间。如果我想以每秒 30 帧的速度存储 2 分钟的视频,那就是 3600 帧,如果直接从 didOutputSampleBuffer 复制,总计超过 12GB。也许有一种方法可以在不损失质量的情况下压缩每帧 x1000,让我可以将这些数据保存在内存中。如果有这样的方法,我一直找不到。

可能的第三种解决方案

如果有办法同时读取和写入文件,我相信下面的解决方案会是理想的。

将视频录制为循环流。例如,对于 2 分钟的视频缓冲区,我将创建一个文件输出流,该流将写入两分钟的帧。一旦达到 2 分钟标记,流将从头开始重新开始,覆盖原始帧。 随着这个文件输出流的不断运行,我将在同一个录制的视频文件上有 3 个输入流。每个流将指向流中的不同帧(实际上比写入流晚 X 秒)。然后每个帧将显示在输入流上相应的UIView

当然,这还是有存储空间的问题。如果帧被存储为压缩的 JPEG 图像,我们谈论的是较低质量的 2 分钟视频需要多个 GB 的存储空间。

问题

    有人知道实现我想要的有效方法吗? 如何解决我已经尝试过的解决方案中的一些问题?

【问题讨论】:

关于 AVPlayer 设备限制,在 iPhone 5 和更新版本上,您应该能够同时分配 10 个播放器(实际上是视频通道)而不会出现问题。 @cohenadair 你最后选择了什么? @denfromufa,实际上是所有 3 个解决方案的组合。我最终创建了一个短片段的循环文件存储缓冲区,并使用 OpenGL 依次显示它们。它最终工作得很好。如果您想查看最终结果,该应用在 App Store 上是免费的:apps.apple.com/us/app/xlr8-skill-system/id1353246743 @cohenadair 很酷,使用下面的新 API 查看新答案:***.com/a/66829118/2230844 【参考方案1】:

自从接受答案后,情况发生了变化。现在有一个替代分段 AVCaptureMovieFileOutput 的替代方法,它不会在您创建新分段时在 iOS 上丢帧,而该替代方法是 AVAssetWriter

从 iOS 14 开始,AVAssetWriter 可以创建分段的 MPEG4,它们本质上是内存中的 mpeg 4 文件。它适用于 HLS 流应用程序,但也是一种非常方便的缓存视频和音频内容的方法。

Takayuki Mizuno 在 WWDC 2020 会议Author fragmented MPEG-4 content with AVAssetWriter 中描述了这项新功能。

有了碎片化的 mp4 AVAssetWriter,通过将 mp4 片段写入磁盘并使用多个 AVQueuePlayers 以所需的时间偏移量播放它们来创建解决此问题的方法并不难。

所以这将是第四个解决方案:使用AVAssetWriter.mpeg4AppleHLS 输出配置文件捕获摄像机流并将其作为碎片mp4 写入磁盘,并使用AVQueuePlayers 和@987654333 以不同的延迟播放视频@。

如果您需要支持 iOS 13 及更低版本,则必须替换分段的AVAssetWriter,这会很快获得技术支持,尤其是如果您也想编写音频。谢谢,水野孝之!

import UIKit
import AVFoundation
import UniformTypeIdentifiers

class ViewController: UIViewController 
    let playbackDelays:[Int] = [5, 20, 30]
    let segmentDuration = CMTime(value: 2, timescale: 1)

    var assetWriter: AVAssetWriter!
    var videoInput: AVAssetWriterInput!
    var startTime: CMTime!

    var writerStarted = false
    
    let session = AVCaptureSession()
    
    var segment = 0
    var outputDir: URL!
    var initializationData = Data()
    
    var layers: [AVPlayerLayer] = []
    var players: [AVQueuePlayer] = []

    override func viewDidLoad() 
        super.viewDidLoad()
        
        for _ in 0..<playbackDelays.count 
            let player = AVQueuePlayer()
            player.automaticallyWaitsToMinimizeStalling = false
            let layer = AVPlayerLayer(player: player)
            layer.videoGravity = .resizeAspectFill
            layers.append(layer)
            players.append(player)
            view.layer.addSublayer(layer)
        
        
        outputDir = FileManager.default.urls(for: .documentDirectory, in:.userDomainMask).first!
    
        assetWriter = AVAssetWriter(contentType: UTType.mpeg4Movie)
        assetWriter.outputFileTypeProfile = .mpeg4AppleHLS // fragmented mp4 output!
        assetWriter.preferredOutputSegmentInterval = segmentDuration
        assetWriter.initialSegmentStartTime = .zero
        assetWriter.delegate = self
        
        let videoOutputSettings: [String : Any] = [
            AVVideoCodecKey: AVVideoCodecType.h264,
            AVVideoWidthKey: 1024,
            AVVideoHeightKey: 720
        ]
        videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: videoOutputSettings)
        videoInput.expectsMediaDataInRealTime = true

        assetWriter.add(videoInput)

        // capture session
        let videoDevice = AVCaptureDevice.default(for: .video)!
        let videoInput = try! AVCaptureDeviceInput(device: videoDevice)
        session.addInput(videoInput)
        
        let videoOutput = AVCaptureVideoDataOutput()
        videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
        session.addOutput(videoOutput)
        
        session.startRunning()
    
    
    override func viewDidLayoutSubviews() 
        let size = view.bounds.size
        let layerWidth = size.width / CGFloat(layers.count)
        for i in 0..<layers.count 
            let layer = layers[i]
            layer.frame = CGRect(x: CGFloat(i)*layerWidth, y: 0, width: layerWidth, height: size.height)
        
    
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask 
        return .landscape
    


extension ViewController: AVCaptureVideoDataOutputSampleBufferDelegate 
    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) 
        
        if startTime == nil 
            let success = assetWriter.startWriting()
            assert(success)
            startTime = sampleBuffer.presentationTimeStamp
            assetWriter.startSession(atSourceTime: startTime)
        
        
        if videoInput.isReadyForMoreMediaData 
            videoInput.append(sampleBuffer)
        
    


extension ViewController: AVAssetWriterDelegate 
    func assetWriter(_ writer: AVAssetWriter, didOutputSegmentData segmentData: Data, segmentType: AVAssetSegmentType) 
        print("segmentType: \(segmentType.rawValue) - size: \(segmentData.count)")
        
        switch segmentType 
        case .initialization:
            initializationData = segmentData
        case .separable:
            let fileURL = outputDir.appendingPathComponent(String(format: "%.4i.mp4", segment))
            segment += 1

            let mp4Data = initializationData + segmentData
            try! mp4Data.write(to: fileURL)

            let asset = AVAsset(url: fileURL)

            for i in 0..<players.count 
                let player = players[i]
                let playerItem = AVPlayerItem(asset: asset)
                player.insert(playerItem, after: nil)
                
                if player.rate == 0 && player.status == .readyToPlay 
                    let hostStartTime: CMTime = startTime + CMTime(value: CMTimeValue(playbackDelays[i]), timescale: 1)

                    player.preroll(atRate: 1)  prerolled in
                        guard prerolled else  return 
                        player.setRate(1, time: .invalid, atHostTime: hostStartTime)
                    
                
            
            
        @unknown default:
            break
        
    

结果是这样的

而且性能还算合理:我的 2019 年 iPod 占用 10-14% 的 cpu 和 38MB 的内存。

【讨论】:

当然可以,只需在prerollsetRate 中传递不同的速率。当然,比率 1 比率会消耗您的延迟并超越“现在”。我现在意识到屏幕截图应该是两个设备和 四个 时钟。我看看能不能做得更好。 这太棒了!我使用了一个非常古老(但可以工作)的设置,我自己用大量代码将图像写入文档目录,这非常干净和最新。您的代码会立即运行,但是,它显示的最终视频会旋转 90 度。我想知道为什么您的屏幕截图没有显示...您有什么想法吗? 不知道,Bob - 我可能破解了肖像测试代码,你尝试过吗? 可能 - UI 代码在此答案中退居媒体处理之后。 嗯,我真的无法让它工作,我已经尝试了一整天。我也有相机反馈太宽的问题。我一直在仔细查看您的屏幕截图,但我认为那里也一样。如果你仔细观察现实生活时钟中的数字,你会发现那里的数字更小,你可以看到数字 8 是最好的。我想我要问一个新的问题,这让我发疯;)【参考方案2】:
    在 iOS 上 AVCaptureMovieFileOutput 在切换文件时丢帧。在 osx 上不会发生这种情况。头文件中有关于这个的讨论,见captureOutputShouldProvideSampleAccurateRecordingStart

您的 2. 和 3. 的组合应该可以工作。您需要使用AVCaptureVideoDataOutputAVAssetWriter 而不是AVCaptureMovieFileOutput 将视频文件分块写入,这样您就不会丢帧。添加 3 个具有足够存储空间的环形缓冲区以跟上播放,使用 GLES 或金属来显示缓冲区(使用 YUV 而不是 RGBA 使用的内存少 4/1.5 倍)。

在强大的 iPhone 4s 和 iPad 2 时代,我尝试了一个更温和的版本。它显示(我认为)现在和过去 10 年代。我猜测因为你可以以 3x 实时编码 30fps,所以我应该能够对块进行编码并仅使用 2/3 的硬件容量来读取之前的块。可悲的是,要么我的想法错误,要么硬件存在非线性,要么代码错误,编码器一直落后。

【讨论】:

这里的问题是没有足够的内存来拥有 1 个环形缓冲区(更不用说 3 个),即使帧以较小的格式存储,例如 YUV(我通过将帧转换为 YUV 图像和存储在内存中;这可能是错误的方法)。我已经设法将我的视频分块保存,并使用多个 AVPlayer 实例播放它们,但该方法最终落后于录制(因此播放开始时延迟 5 秒,但 10 分钟后它以 20 秒播放延迟)。 你如何选择你的环形缓冲区大小? 我没有选择任何尺寸。据我所知,您不能像在 C 中那样在 Swift 中分配内存。在应用程序崩溃之前,我无法以 20 fps 的速度存储超过几秒钟的帧。问题可能源于AVAssetReader 无法生成压缩帧,因此读取的所有内容都是未压缩的。 还可以播放视频!你需要弄清楚你需要多少秒的解压缩帧来维持播放,有多少内存可供你使用,以及这意味着什么帧分辨率。使用 YUV 会降低你的内存需求,提高你的分辨率。我不会担心 C 和 Swift 的差异。 帧有显示时间戳,即它们应该出现的时间。如果您有帧 f0f1,那么您知道应该显示 f0 的时间间隔。同样,您的环形缓冲区也代表时间间隔,对于每个延迟或实时视图,您查找该时间的帧并在其绘制回调中在 GL/Metal 中绘制它,这以固定速率发生 - 通常是屏幕刷新率。

以上是关于iOS - 创建多个延时实时摄像机预览视图的主要内容,如果未能解决你的问题,请参考以下文章

恢复活动时,在表面视图中捕获的静止图像被实时摄像机重置

有没有人能够在 iOS 上的单独视图中同时播放视频文件和显示实时摄像机源?

opencv for android 如何实现后台启动摄像头,不显示预览界面

海康威视录像机实时预览怎么不显示画面

vs+qt+opencv海康摄像头实时预览

海康威视7816局域网络监控延时啥原因?怎么解决?