在 iOS 开发中,屏幕录制是一项强大的功能,尤其在应用演示、教育教程或游戏录屏等场景中非常有用。Apple 提供了一个名为 ReplayKit 的框架,允许开发者直接在应用中添加屏幕录制功能。本文将详细介绍如何使用 Swift 和 ReplayKit 结合 AVFoundation 来实现屏幕录制功能,并获取录制文件的路径。
1. 引入必要的框架
首先,需要在项目中引入 ReplayKit 和 AVFoundation 框架。这些框架提供了录制屏幕和处理视频文件所需的 API:
import Foundation
import ReplayKit
import AVFoundation
2. 创建屏幕录制器类
创建一个名为 ScreenRecorder
的类来封装屏幕录制的逻辑。这个类将负责设置视频文件写入器、开始和停止录制以及处理录制过程中的数据。
class ScreenRecorder {
private var assetWriter: AVAssetWriter?
private var videoInput: AVAssetWriterInput?
private let screenRecorder = RPScreenRecorder.shared()
private var isRecording = false
private var sessionStarted = false
var statusUpdate: ((String) -> Void)?
var savePathUpdate: ((URL) -> Void)?
}
3. 设置视频文件写入器
创建一个方法 setupWriter
来配置视频文件的写入。此方法将初始化 AVAssetWriter
并为视频设置必要的参数,如编码格式、分辨率等。
private func setupWriter(fileName: String) -> Bool {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = documentsURL.appendingPathComponent(fileName)
do {
assetWriter = try AVAssetWriter(outputURL: fileURL, fileType: .mp4)
let outputSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: UIScreen.main.bounds.width,
AVVideoHeightKey: UIScreen.main.bounds.height
]
videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
if assetWriter!.canAdd(videoInput!) {
assetWriter!.add(videoInput!)
}
} catch {
statusUpdate?("设置视频写入器失败: \(error)")
return false
}
return true
}
4. 开始和停止录制
实现 startRecording
和 stopRecording
方法来控制录制的开始和结束。这些方法将处理录制的启动、数据捕获、文件写入和资源释放。
func startRecording(withFileName fileName: String) {
guard !isRecording else {
statusUpdate?("录制已经在进行中。")
return
}
guard RPScreenRecorder.shared().isAvailable else {
statusUpdate?("屏幕录制不可用。")
return
}
if !setupWriter(fileName: fileName) {
return
}
assetWriter?.startWriting()
screenRecorder.startCapture { [weak self] (sampleBuffer, bufferType, error) in
guard let self = self else { return }
if let error = error {
self.statusUpdate?("捕捉过程中发生错误: \(error)")
return
}
if bufferType == .video {
DispatchQueue.main.async {
self.handleSampleBuffer(sampleBuffer)
}
}
} completionHandler: { [weak self] error in
guard let self = self else { return }
if let error = error {
self.statusUpdate?("开始捕捉失败: \(error)")
} else {
self.isRecording = true
self.statusUpdate?("录制已开始。")
}
}
}
func stopRecording() {
guard isRecording else {
statusUpdate?("当前没有进行中的录制。")
return
}
screenRecorder.stopCapture { [weak self] (error) in
guard let self = self else { return }
if let error = error {
self.statusUpdate?("停止捕捉失败: \(error)")
return
}
self.videoInput?.markAsFinished()
self.assetWriter?.finishWriting {
if self.assetWriter?.status == .completed {
self.statusUpdate?("录制已停止。")
if let url = self.assetWriter?.outputURL {
self.savePathUpdate?(url)
} else {
self.statusUpdate?("无法获取录制文件的URL。")
}
} else {
print("录屏文件保存失败: \(self.assetWriter?.error?.localizedDescription ?? "未知错误")")
}
self.cleanup()
}
}
}
5. 处理视频样本
在录制过程中,需要实时处理从 ReplayKit 捕获的视频样本。使用 handleSampleBuffer
方法来追加样本数据到视频文件中。
private func handleSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
guard CMSampleBufferGetPresentationTimeStamp(sampleBuffer).isValid,
let videoInput = videoInput, videoInput.isReadyForMoreMediaData else {
return
}
if !sessionStarted {
assetWriter?.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
sessionStarted = true
}
videoInput.append(sampleBuffer)
}
通过上述步骤,您可以在 iOS 应用中实现屏幕录制功能,并在录制完成后获取视频文件的路径。
以下是完整的代码
import Foundation
import ReplayKit
import AVFoundation
class ScreenRecorder {
private var assetWriter: AVAssetWriter?
private var videoInput: AVAssetWriterInput?
private let screenRecorder = RPScreenRecorder.shared()
private var isRecording = false
private var sessionStarted = false
var statusUpdate: ((String) -> Void)?
var savePathUpdate: ((URL) -> Void)?
// 设置视频文件写入器
private func setupWriter(fileName: String) -> Bool {
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = documentsURL.appendingPathComponent(fileName)
do {
assetWriter = try AVAssetWriter(outputURL: fileURL, fileType: .mp4)
let outputSettings: [String: Any] = [
AVVideoCodecKey: AVVideoCodecType.h264,
AVVideoWidthKey: UIScreen.main.bounds.width,
AVVideoHeightKey: UIScreen.main.bounds.height
]
videoInput = AVAssetWriterInput(mediaType: .video, outputSettings: outputSettings)
if assetWriter!.canAdd(videoInput!) {
assetWriter!.add(videoInput!)
}
} catch {
statusUpdate?("设置视频写入器失败: \(error)")
return false
}
return true
}
// 开始录制
func startRecording(withFileName fileName: String) {
guard !isRecording else {
statusUpdate?("录制已经在进行中。")
return
}
guard RPScreenRecorder.shared().isAvailable else {
statusUpdate?("屏幕录制不可用。")
return
}
if !setupWriter(fileName: fileName) {
return
}
assetWriter?.startWriting()
screenRecorder.startCapture { [weak self] (sampleBuffer, bufferType, error) in
guard let self = self else { return }
if let error = error {
self.statusUpdate?("捕捉过程中发生错误: \(error)")
return
}
if bufferType == .video {
DispatchQueue.main.async {
self.handleSampleBuffer(sampleBuffer)
}
}
} completionHandler: { [weak self] error in
guard let self = self else { return }
if let error = error {
self.statusUpdate?("开始捕捉失败: \(error)")
} else {
self.isRecording = true
self.statusUpdate?("录制已开始。")
}
}
}
// 处理视频样本
private func handleSampleBuffer(_ sampleBuffer: CMSampleBuffer) {
guard CMSampleBufferGetPresentationTimeStamp(sampleBuffer).isValid,
let videoInput = videoInput, videoInput.isReadyForMoreMediaData else {
return
}
if !sessionStarted {
assetWriter?.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer))
sessionStarted = true
}
videoInput.append(sampleBuffer)
}
// 停止录制
func stopRecording() {
guard isRecording else {
statusUpdate?("当前没有进行中的录制。")
return
}
screenRecorder.stopCapture { [weak self] (error) in
guard let self = self else { return }
if let error = error {
self.statusUpdate?("停止捕捉失败: \(error)")
return
}
self.videoInput?.markAsFinished()
self.assetWriter?.finishWriting {
if self.assetWriter?.status == .completed {
self.statusUpdate?("录制已停止。")
if let url = self.assetWriter?.outputURL {
self.savePathUpdate?(url)
} else {
self.statusUpdate?("无法获取录制文件的URL。")
}
} else {
print("录屏文件保存失败: \(self.assetWriter?.error?.localizedDescription ?? "未知错误")")
}
self.cleanup()
}
}
}
// 清理资源
private func cleanup() {
isRecording = false
sessionStarted = false
assetWriter = nil
videoInput = nil
}
}