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
将短片录制到设备存储器中。由于您无法播放URL中的视频并同时写入同一URL,因此需要使用短剪辑AVCaptureMovieFileOutput
- 有3个
和AVPlayer
实例,所有实例都以所需的时间延迟播放录制的短片AVPlayerLayer
- 问题:
- 使用
切换剪辑时,剪辑之间存在非常明显的延迟。这需要平稳过渡AVPlayer.replaceCurrentItem(:)
- 尽管很旧,但有一条评论建议,由于设备限制,不要创建多个
实例。我无法找到证实或否认这一说法的信息。E:从Jake G的评论来看-10AVPlayer
instances对于iPhone5和更高版本来说是可以的AVPlayer
AVFoundation
和AVCaptureVideoDataOutput
:
- 使用
和AVCaptureSession
使用AVCaptureVideoDataOutput
委托方法流式传输和处理相机提要的每一帧didOutputSampleBuffer
- 在OpenGL视图上绘制每个帧(例如
)。这解决了解决方案1中多个GLKViewWithBounds
实例的问题。AVPlayer
- 问题:存储每个帧以便以后显示需要大量内存(在iOS设备上无法实现),或者磁盘空间。如果我想以每秒30帧的速度存储2分钟的视频,那就是3600帧,如果直接从
复制,总共超过12GB。也许有一种方法可以在不损失质量的情况下压缩每一帧x1000,这样我就可以将这些数据保存在内存中。如果存在这样一种方法,我就无法找到它didOutputSampleBuffer
- 将视频录制为循环流。例如,对于2分钟的视频缓冲区,我将创建一个文件输出流,该流将写入两分钟的帧。一旦达到2分钟标记,流将从头开始,覆盖原始帧
- 随着这个文件输出流的不断运行,我将在同一个录制的视频文件上有3个输入流。每个流将指向流中的不同帧(实际上是写流后面的X秒)。然后,每个帧将分别显示在输入流
上李>UIView
captureOutputShouldProvideSampleAccurateRecordingStart
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会议上介绍了这一新能力
有了一个碎片化的mp4AVAssetWriter
,通过将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
}
}
}