iOS - 创建多个延时实时相机预览视图
iOS - Creating multiple time-delayed live camera preview views
我已经进行了 吨 的研究,但由于许多原因仍未找到可行的解决方案,我将在下面概述。
问题
在我的 iOS 应用中,我想要三个无限期显示设备相机延迟实时预览的视图。
例如,视图1显示一个摄像头视图,延迟5秒,视图2显示相同的摄像头视图,延迟20秒,视图3显示相同的摄像头视图,延迟30秒。
这将用于记录你自己进行某种activity,例如锻炼,然后在几秒钟后观察你自己,以完善你在给定练习中的形式。
尝试过的解决方案
我尝试并研究了几个不同的解决方案,但都存在问题。
1。使用 AVFoundation
和 AVCaptureMovieFileOutput
:
- 使用
AVCaptureSession
和AVCaptureMovieFileOutput
将短片录制到设备存储器。需要短片,因为您无法播放来自 URL 的视频并同时写入同一 URL。
- 有 3 个
AVPlayer
和 AVPlayerLayer
实例,所有实例都以所需的时间延迟播放录制的短片。
- 问题:
- 使用
AVPlayer.replaceCurrentItem(_:)
切换剪辑时,剪辑之间有非常明显的延迟。这需要平稳过渡。
- 评论 here 虽然很旧,但由于设备限制,建议不要创建多个
AVPlayer
实例。我无法找到确认或否认此声明的信息。 E:来自 Jake G 的评论 - 10 AVPlayer
个实例对于 iPhone 5 和更新版本是可以的。
2。使用 AVFoundation
和 AVCaptureVideoDataOutput
:
- 使用
AVCaptureSession
和 AVCaptureVideoDataOutput
使用 didOutputSampleBuffer
委托方法流式传输和处理摄像机源的每一帧。
- 在 OpenGL 视图上绘制每一帧(例如
GLKViewWithBounds
)。这解决了来自 Solution 1.
. 的多个 AVPlayer
实例的问题
- 问题:存储每一帧以便稍后显示它们需要大量内存(这在 iOS 设备上不可行)或磁盘space。如果我想以每秒 30 帧的速度存储一个 2 分钟的视频,那就是 3600 帧,如果直接从
didOutputSampleBuffer
复制,总计超过 12GB。也许有一种方法可以将每一帧压缩 x1000 而不会降低质量,这样我就可以将这些数据保存在内存中。如果有这样的方法,我还没找到。
可能的第三种解决方案
如果有办法同时读取和写入文件,我相信下面的解决方案将是理想的。
- 将视频录制为循环流。例如,对于 2 分钟的视频缓冲区,我将创建一个文件输出流来写入两分钟的帧。一旦达到 2 分钟标记,流将从头开始,覆盖原始帧。
- 有了这个文件输出流不断 运行,我会在同一个录制的视频文件上有 3 个输入流。每个流将指向流中的不同帧(有效地落后于写入流 X 秒)。然后每个帧将显示在各自的输入流
UIView
上。
当然,这还有存储的问题space。如果帧存储为压缩的 JPEG 图像,我们谈论的是较低质量的 2 分钟视频所需的多个 GB 存储空间。
问题
- 有谁知道实现我想要的东西的有效方法吗?
- 如何解决我已经尝试过的解决方案中的一些问题?
- on iOS
AVCaptureMovieFileOutput
切换文件时掉帧。在 osx 这不会发生。头文件中对此进行了讨论,请参阅 captureOutputShouldProvideSampleAccurateRecordingStart
。
你的 2. 和 3. 的组合应该有效。您需要使用 AVCaptureVideoDataOutput
和 AVAssetWriter
而不是 AVCaptureMovieFileOutput
以块的形式写入视频文件,这样您就不会丢帧。添加 3 个具有足够存储空间的环形缓冲区来跟上播放,使用 GLES 或金属来显示缓冲区(使用 YUV 而不是 RGBA 使用 4/1.5 倍的内存)。
我在强大的 iPhone 4s 和 iPad 2 的时代尝试了一个更温和的版本。它显示(我认为)现在和过去的 10s。我猜测因为你可以 3x 实时编码 30fps,所以我应该能够编码块并只使用 2/3 的硬件容量读取之前的块。可悲的是,要么是我的想法错了,要么是硬件有问题non-linearity,要么是代码错误,编码器一直落后。
自接受答案以来,情况发生了变化。现在有一个分段 AVCaptureMovieFileOutput
的替代方案,当您创建新分段时,它不会在 iOS 上丢帧,替代方案是 AVAssetWriter
!
从 iOS 14 开始,AVAssetWriter
可以创建碎片化的 MPEG4,它们本质上是内存中的 mpeg 4 文件。专为 HLS 流应用程序而设计,但它也是一种非常方便的缓存视频和音频内容的方法。
这项新功能由 Takayuki Mizuno 在 WWDC 2020 中进行了描述 session Author fragmented MPEG-4 content with AVAssetWriter。
手头有一个碎片化的 mp4 AVAssetWriter
,通过将 mp4
片段写入磁盘并使用所需的时间偏移播放它们来创建解决此问题的方法并不难几个 AVQueuePlayer
s.
因此这将是第四种解决方案:使用 AVAssetWriter
的 .mpeg4AppleHLS
输出配置文件捕获摄像机流并将其作为碎片 mp4 写入磁盘,并使用 [=以不同的延迟播放视频 AVQueuePlayer
s 和 AVPlayerLayers
.
如果您需要支持 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 内存。
我已经进行了 吨 的研究,但由于许多原因仍未找到可行的解决方案,我将在下面概述。
问题
在我的 iOS 应用中,我想要三个无限期显示设备相机延迟实时预览的视图。
例如,视图1显示一个摄像头视图,延迟5秒,视图2显示相同的摄像头视图,延迟20秒,视图3显示相同的摄像头视图,延迟30秒。
这将用于记录你自己进行某种activity,例如锻炼,然后在几秒钟后观察你自己,以完善你在给定练习中的形式。
尝试过的解决方案
我尝试并研究了几个不同的解决方案,但都存在问题。
1。使用 AVFoundation
和 AVCaptureMovieFileOutput
:
- 使用
AVCaptureSession
和AVCaptureMovieFileOutput
将短片录制到设备存储器。需要短片,因为您无法播放来自 URL 的视频并同时写入同一 URL。 - 有 3 个
AVPlayer
和AVPlayerLayer
实例,所有实例都以所需的时间延迟播放录制的短片。 - 问题:
- 使用
AVPlayer.replaceCurrentItem(_:)
切换剪辑时,剪辑之间有非常明显的延迟。这需要平稳过渡。 - 评论 here 虽然很旧,但由于设备限制,建议不要创建多个
AVPlayer
实例。我无法找到确认或否认此声明的信息。 E:来自 Jake G 的评论 - 10AVPlayer
个实例对于 iPhone 5 和更新版本是可以的。
- 使用
2。使用 AVFoundation
和 AVCaptureVideoDataOutput
:
- 使用
AVCaptureSession
和AVCaptureVideoDataOutput
使用didOutputSampleBuffer
委托方法流式传输和处理摄像机源的每一帧。 - 在 OpenGL 视图上绘制每一帧(例如
GLKViewWithBounds
)。这解决了来自Solution 1.
. 的多个 - 问题:存储每一帧以便稍后显示它们需要大量内存(这在 iOS 设备上不可行)或磁盘space。如果我想以每秒 30 帧的速度存储一个 2 分钟的视频,那就是 3600 帧,如果直接从
didOutputSampleBuffer
复制,总计超过 12GB。也许有一种方法可以将每一帧压缩 x1000 而不会降低质量,这样我就可以将这些数据保存在内存中。如果有这样的方法,我还没找到。
AVPlayer
实例的问题
可能的第三种解决方案
如果有办法同时读取和写入文件,我相信下面的解决方案将是理想的。
- 将视频录制为循环流。例如,对于 2 分钟的视频缓冲区,我将创建一个文件输出流来写入两分钟的帧。一旦达到 2 分钟标记,流将从头开始,覆盖原始帧。
- 有了这个文件输出流不断 运行,我会在同一个录制的视频文件上有 3 个输入流。每个流将指向流中的不同帧(有效地落后于写入流 X 秒)。然后每个帧将显示在各自的输入流
UIView
上。
当然,这还有存储的问题space。如果帧存储为压缩的 JPEG 图像,我们谈论的是较低质量的 2 分钟视频所需的多个 GB 存储空间。
问题
- 有谁知道实现我想要的东西的有效方法吗?
- 如何解决我已经尝试过的解决方案中的一些问题?
- on iOS
AVCaptureMovieFileOutput
切换文件时掉帧。在 osx 这不会发生。头文件中对此进行了讨论,请参阅captureOutputShouldProvideSampleAccurateRecordingStart
。
你的 2. 和 3. 的组合应该有效。您需要使用 AVCaptureVideoDataOutput
和 AVAssetWriter
而不是 AVCaptureMovieFileOutput
以块的形式写入视频文件,这样您就不会丢帧。添加 3 个具有足够存储空间的环形缓冲区来跟上播放,使用 GLES 或金属来显示缓冲区(使用 YUV 而不是 RGBA 使用 4/1.5 倍的内存)。
我在强大的 iPhone 4s 和 iPad 2 的时代尝试了一个更温和的版本。它显示(我认为)现在和过去的 10s。我猜测因为你可以 3x 实时编码 30fps,所以我应该能够编码块并只使用 2/3 的硬件容量读取之前的块。可悲的是,要么是我的想法错了,要么是硬件有问题non-linearity,要么是代码错误,编码器一直落后。
自接受答案以来,情况发生了变化。现在有一个分段 AVCaptureMovieFileOutput
的替代方案,当您创建新分段时,它不会在 iOS 上丢帧,替代方案是 AVAssetWriter
!
从 iOS 14 开始,AVAssetWriter
可以创建碎片化的 MPEG4,它们本质上是内存中的 mpeg 4 文件。专为 HLS 流应用程序而设计,但它也是一种非常方便的缓存视频和音频内容的方法。
这项新功能由 Takayuki Mizuno 在 WWDC 2020 中进行了描述 session Author fragmented MPEG-4 content with AVAssetWriter。
手头有一个碎片化的 mp4 AVAssetWriter
,通过将 mp4
片段写入磁盘并使用所需的时间偏移播放它们来创建解决此问题的方法并不难几个 AVQueuePlayer
s.
因此这将是第四种解决方案:使用 AVAssetWriter
的 .mpeg4AppleHLS
输出配置文件捕获摄像机流并将其作为碎片 mp4 写入磁盘,并使用 [=以不同的延迟播放视频 AVQueuePlayer
s 和 AVPlayerLayers
.
如果您需要支持 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 内存。