首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >iOS -创建多个延迟的实时摄像机预览视图

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

Stack Overflow用户
提问于 2018-02-20 18:53:52
回答 2查看 1.1K关注 0票数 4

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

问题

在我的iOS应用程序中,我想要三个视图,无限期地显示设备摄像头的延迟直播预览。

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

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

尝试解决方案

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

1.使用AVFoundationAVCaptureMovieFileOutput

  • 使用AVCaptureSessionAVCaptureMovieFileOutput将短片记录到设备存储中。需要简短的剪辑,因为您不能播放来自URL的视频,并且不能同时写入同一URL。
  • 有3个AVPlayerAVPlayerLayer实例,它们都在它们想要的时间延迟播放简短的录音片段.
  • 问题:
    1. 当使用AVPlayer.replaceCurrentItem(_:)切换剪辑时,剪辑之间存在非常明显的延迟。这需要平稳过渡。
    2. 虽然这里很旧,但由于设备限制,它建议不要创建多个AVPlayer实例。我还没有找到确认或否认这份声明的信息。E:从Jake的评论来看,10个AVPlayer实例可以用于iPhone 5和更高版本。

2.使用AVFoundationAVCaptureVideoDataOutput

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

可能的第三种解决方案

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

  • 将视频录制为圆形流。例如,对于2分钟的视频缓冲区,我将创建一个文件输出流,该流将编写两分钟的帧。一旦2分钟的标记被击中,流将从开始重新启动,覆盖原始帧。
  • 有了这个文件输出流的不断运行,我将有3个输入流在同一个录制的视频文件。每个流将指向流中的不同帧(实际上是写流后面的X秒)。然后,每个帧将显示在相应的UIView输入流上。

当然,这仍然存在存储空间的问题。如果帧被存储为压缩的JPEG图像,我们将讨论一个更低质量、2分钟视频所需的多个GBs存储。

问题

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

回答 2

Stack Overflow用户

回答已采纳

发布于 2018-02-22 23:44:57

  1. 在iOS上,AVCaptureMovieFileOutput在切换文件时删除帧。在osx上,这种情况不会发生。在头文件中对此进行了讨论,请参阅captureOutputShouldProvideSampleAccurateRecordingStart

你的2和3的组合应该有效。您需要使用AVCaptureVideoDataOutputAVAssetWriter而不是AVCaptureMovieFileOutput来编写视频文件块,这样就不会丢弃帧。添加3个具有足够存储空间的环形缓冲区,以跟上回放,使用GLES或金属元素显示缓冲区(使用YUV代替RGBA使用内存减少4/1.5倍)。

在强大的iPhone 4s和iPad 2时代,我尝试了一个更温和的版本。它展示了(我认为)现在和过去的10s。我估计,因为您可以在3倍实时编码30 you,所以我应该能够使用硬件容量的2/3对块进行编码并读取前面的块。可悲的是,要么我的想法错了,要么硬件出现了非线性,要么代码错了,编码器一直落后。

票数 2
EN

Stack Overflow用户

发布于 2021-03-27 07:45:35

自从被接受的答案以来,情况发生了变化。现在有一种替代分段AVCaptureMovieFileOutput的方法,当您创建新段时,它不会在iOS上丢弃框架,而该选项是AVAssetWriter

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

这种新的功能是由Takayuki在2020年作者用AVAssetWriter分割MPEG-4内容会议上描述的。

有了一个支离破碎的mp4 AVAssetWriter,通过将mp4段写入磁盘并使用几个AVQueuePlayer的所需的时间偏移来重放,创建解决此问题的解决方案并不困难。

因此,这将是第四种解决方案:使用AVAssetWriter.mpeg4AppleHLS输出配置文件,捕获摄像机流并将其作为分段的.mpeg4AppleHLS写入磁盘,并使用AVQueuePlayerAVPlayerLayers以不同的延迟播放视频。

如果您需要支持iOS 13或更低的版本,您将不得不替换分段的AVAssetWriter,这将获得快速的技术,特别是如果您也想编写音频。谢谢你,竹之三郎!

代码语言:javascript
复制
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和38 my的内存。

票数 6
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/48892328

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档