Warning: file_get_contents(/data/phpspider/zhask/data//catemap/9/ios/95.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
iOS-创建多个延时实时摄影机预览视图_Ios_Camera_Avfoundation_Video Capture_Video Processing - Fatal编程技术网

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

iOS-创建多个延时实时摄影机预览视图,ios,camera,avfoundation,video-capture,video-processing,Ios,Camera,Avfoundation,Video Capture,Video Processing,我已经做了大量的研究,但由于许多原因,还没有找到一个可行的解决方案,我将在下面概述 问题 在我的iOS应用程序中,我希望有三个视图可以无限期地显示设备摄像头的延迟实时预览 例如,视图1将显示相机视图,延迟5s,视图2将显示相同的相机视图,延迟20s,视图3将显示相同的相机视图,延迟30s 这将被用来记录你自己进行的某种活动,比如锻炼,然后几秒钟后观察你自己,以完善你的锻炼形式 尝试过的解决方案 我尝试并研究了几种不同的解决方案,但都有问题 1.使用AVFoundation和AVCaptureM

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


问题 在我的iOS应用程序中,我希望有三个视图可以无限期地显示设备摄像头的延迟实时预览

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

这将被用来记录你自己进行的某种活动,比如锻炼,然后几秒钟后观察你自己,以完善你的锻炼形式

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

1.使用
AVFoundation
AVCaptureMovieFileOutput
  • 使用
    AVCaptureSession
    AVCaptureMovieFileOutput
    将短片录制到设备存储器中。由于您无法播放URL中的视频并同时写入同一URL,因此需要使用短剪辑
  • 有3个
    AVPlayer
    AVPlayerLayer
    实例,所有实例都以所需的时间延迟播放录制的短片
  • 问题:
  • 使用
    AVPlayer.replaceCurrentItem(:)
    切换剪辑时,剪辑之间存在非常明显的延迟。这需要平稳过渡
  • 尽管很旧,但有一条评论建议,由于设备限制,不要创建多个
    AVPlayer
    实例。我无法找到证实或否认这一说法的信息。E:从Jake G的评论来看-10
    AVPlayer
    instances对于iPhone5和更高版本来说是可以的
2.使用
AVFoundation
AVCaptureVideoDataOutput
  • 使用
    AVCaptureSession
    AVCaptureVideoDataOutput
    使用
    didOutputSampleBuffer
    委托方法流式传输和处理相机提要的每一帧
  • 在OpenGL视图上绘制每个帧(例如
    GLKViewWithBounds
    )。这解决了解决方案1中多个
    AVPlayer
    实例的问题。
  • 问题:存储每个帧以便以后显示需要大量内存(在iOS设备上无法实现),或者磁盘空间。如果我想以每秒30帧的速度存储2分钟的视频,那就是3600帧,如果直接从
    didOutputSampleBuffer
    复制,总共超过12GB。也许有一种方法可以在不损失质量的情况下压缩每一帧x1000,这样我就可以将这些数据保存在内存中。如果存在这样一种方法,我就无法找到它
可能的第三种解决方案 如果有一种同时读取和写入文件的方法,我相信下面的解决方案将是理想的

  • 将视频录制为循环流。例如,对于2分钟的视频缓冲区,我将创建一个文件输出流,该流将写入两分钟的帧。一旦达到2分钟标记,流将从头开始,覆盖原始帧
  • 随着这个文件输出流的不断运行,我将在同一个录制的视频文件上有3个输入流。每个流将指向流中的不同帧(实际上是写流后面的X秒)。然后,每个帧将分别显示在输入流
    UIView
当然,这仍然存在存储空间的问题。事件如果帧存储为压缩的JPEG图像,则我们所说的是低质量2分钟视频所需的多Gb存储

问题:
  • 有人知道一种有效的方法来实现我的目标吗
  • 如何解决我已经尝试过的解决方案中的一些问题
  • 在iOS上,当切换文件时,AVCaptureMovieFileOutput会丢弃帧。在osx上,这不会发生。在头文件中对此进行了讨论,请参见
    captureOutputShouldProvideSampleAccurateRecordingStart
  • 你的2个组合。三,。应该有用。您需要使用
    AVCaptureVideoDataOutput
    AVAssetWriter
    而不是
    AVCaptureMovieFileOutput
    将视频文件分块写入,这样就不会丢弃帧。添加3个具有足够存储空间的环形缓冲区以跟上播放,使用GLES或metal显示缓冲区(使用YUV而不是RGBA,使用4/1.5倍的内存)


    在强大的iPhone4S和iPad2时代,我尝试了一个更温和的版本。它显示了(我想)现在和过去的10秒。我估计这是因为您可以以3倍的实时速度编码30fps,因此我应该能够仅使用硬件容量的2/3对块进行编码并读取以前的块。可悲的是,要么我的想法是错误的,要么硬件是非线性的,要么代码是错误的,编码器一直落后。

    自接受答案以来,情况发生了变化。现在有了分段的
    AVCaptureMovieFileOutput
    的替代方案,当您创建新的分段时,它不会在iOS上删除帧,该替代方案是
    AVAssetWriter

    从iOS 14开始,
    AVAssetWriter
    可以创建片段化的MPEG4,基本上是内存中的MPEG4文件。它适用于HLS流媒体应用,但同时也是一种非常方便的视频和音频内容缓存方法

    水野隆之在WWDC 2020会议上介绍了这一新能力

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

    因此,这将是第四种解决方案:捕获相机流,并使用
    AVAssetWriter
    .mp将其作为片段mp4写入磁盘
    
    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
            }
        }
    }