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。使用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 设备上是不可行的)或磁盘空间。如果我想以每秒 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
片段写入磁盘并使用多个 AVQueuePlayer
s 以所需的时间偏移量播放它们来创建解决此问题的方法并不难。
所以这将是第四个解决方案:使用AVAssetWriter
的.mpeg4AppleHLS
输出配置文件捕获摄像机流并将其作为碎片mp4 写入磁盘,并使用AVQueuePlayer
s 和@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 的内存。
【讨论】:
当然可以,只需在preroll
和setRate
中传递不同的速率。当然,比率 1 比率会消耗您的延迟并超越“现在”。我现在意识到屏幕截图应该是两个设备和 四个 时钟。我看看能不能做得更好。
这太棒了!我使用了一个非常古老(但可以工作)的设置,我自己用大量代码将图像写入文档目录,这非常干净和最新。您的代码会立即运行,但是,它显示的最终视频会旋转 90 度。我想知道为什么您的屏幕截图没有显示...您有什么想法吗?
不知道,Bob - 我可能破解了肖像测试代码,你尝试过吗?
可能 - UI 代码在此答案中退居媒体处理之后。
嗯,我真的无法让它工作,我已经尝试了一整天。我也有相机反馈太宽的问题。我一直在仔细查看您的屏幕截图,但我认为那里也一样。如果你仔细观察现实生活时钟中的数字,你会发现那里的数字更小,你可以看到数字 8 是最好的。我想我要问一个新的问题,这让我发疯;)【参考方案2】:
-
在 iOS 上
AVCaptureMovieFileOutput
在切换文件时丢帧。在 osx 上不会发生这种情况。头文件中有关于这个的讨论,见captureOutputShouldProvideSampleAccurateRecordingStart
。
您的 2. 和 3. 的组合应该可以工作。您需要使用AVCaptureVideoDataOutput
和AVAssetWriter
而不是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 的差异。
帧有显示时间戳,即它们应该出现的时间。如果您有帧 f0 和 f1,那么您知道应该显示 f0 的时间间隔。同样,您的环形缓冲区也代表时间间隔,对于每个延迟或实时视图,您查找该时间的帧并在其绘制回调中在 GL/Metal 中绘制它,这以固定速率发生 - 通常是屏幕刷新率。以上是关于iOS - 创建多个延时实时摄像机预览视图的主要内容,如果未能解决你的问题,请参考以下文章
有没有人能够在 iOS 上的单独视图中同时播放视频文件和显示实时摄像机源?