在此代码中,我们将相机预览为镜像效果,然后将其水平翻转,同时将其录制到文件中以避免镜像效果,但在录制过程中,视频方向设置为上下颠倒,人脸位于相机底部腿位于相机的顶部,这是错误的。现在,我们如何解决这个问题并始终以纵向模式保存?请在这方面帮助我。谢谢。这是我的代码片段 -
import AVFoundation
import Accelerate.vImage
import UIKit
import CoreImage
enum CameraState {
case idle, start, capturing, end
}
final class VideoDataManager: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
/// Delegate to receive the frames captured by the device's camera.
var delegate: VideoServiceProtocol?
var cameraState = CameraState.idle
private var _videoOutput: AVCaptureVideoDataOutput?
private var assetWriter: AVAssetWriter?
private var assetWriterInput: AVAssetWriterInput?
private var adpater: AVAssetWriterInputPixelBufferAdaptor?
private var time: Double = 0
override init() {
super.init()
configureSession("FRONT")
}
/// Start capturing frames from the camera.
func startRunning() {
DispatchQueue.global(qos: .background).async {
self.captureSession.startRunning()
}
}
/// Stop capturing frames from the camera.
func stopRunning() {
captureSession.stopRunning()
}
var captureSession = AVCaptureSession()
func switchCamera(_ cameraType:String?) {
let session: AVCaptureSession = captureSession
session.beginConfiguration()
var position = AVCaptureDevice.Position.back
if cameraType == "FRONT" {
position = AVCaptureDevice.Position.front
}
guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position) else { return }
for input in session.inputs {
session.removeInput(input)
}
do {
let input = try AVCaptureDeviceInput(device: camera)
session.addInput(input)
} catch {
return
}
if let videoOutput = session.outputs.first {
// Handle mirroring based on cameraType for preview
if let connection = videoOutput.connection(with: .video), connection.isVideoMirroringSupported {
connection.isVideoMirrored = position == .front
}
videoOutput.connection(with: .video)?.videoOrientation = .portrait
}
session.commitConfiguration()
captureSession = session
}
/// Initialize the capture session.
///
func configureSession(_ cameraType:String?) {
if captureSession.isRunning {
stopRunning()
for input in captureSession.inputs {
captureSession.removeInput(input)
}
}
captureSession.sessionPreset = AVCaptureSession.Preset.photo
var position = AVCaptureDevice.Position.back
if cameraType == "FRONT" {
position = AVCaptureDevice.Position.front
}
guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: position) else { return }
do {
let input = try AVCaptureDeviceInput(device: camera)
captureSession.addInput(input)
} catch {
return
}
let videoOutput = AVCaptureVideoDataOutput()
videoOutput.videoSettings = [
(kCVPixelBufferPixelFormatTypeKey as String): NSNumber(value: kCVPixelFormatType_32BGRA)
]
videoOutput.alwaysDiscardsLateVideoFrames = true
let dataOutputQueue = DispatchQueue(label: "video data queue", qos: .userInitiated, attributes: [], autoreleaseFrequency: .workItem)
if captureSession.canAddOutput(videoOutput) {
captureSession.addOutput(videoOutput)
if let connection = videoOutput.connection(with: .video), connection.isVideoMirroringSupported {
connection.isVideoMirrored = position == .front
connection.videoOrientation = .portrait
}
}
videoOutput.setSampleBufferDelegate(self, queue: dataOutputQueue)
_videoOutput = videoOutput
}
// MARK: Methods of the AVCaptureVideoDataOutputSampleBufferDelegate
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return
}
CVPixelBufferLockBaseAddress(pixelBuffer, CVPixelBufferLockFlags.readOnly)
delegate?.feedVideoDataIntoModel(didOutput: pixelBuffer)
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags.readOnly)
let timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer).seconds
switch cameraState {
case .start:
// Set up recorder
guard let fileUrl = Utility.getExcerciseRecordingFilePath() else { return }
try? FileManager.default.removeItem(at: fileUrl)
let writer = try! AVAssetWriter(outputURL: fileUrl, fileType: .mp4)
let settings = _videoOutput?.recommendedVideoSettingsForAssetWriter(writingTo: .mp4)
let input = AVAssetWriterInput(mediaType: .video, outputSettings: settings)
input.mediaTimeScale = CMTimeScale(bitPattern: 600)
input.expectsMediaDataInRealTime = true
let adapter = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: input, sourcePixelBufferAttributes: nil)
if writer.canAdd(input) {
writer.add(input)
}
input.transform = getVideoTransform(for: connection.videoOrientation)
writer.startWriting()
writer.startSession(atSourceTime: .zero)
assetWriter = writer
assetWriterInput = input
adpater = adapter
cameraState = .capturing
time = timestamp
MMHLogger.smartLog("Camera State.... start...")
case .capturing:
MMHLogger.smartLog("Video is capturing.... in capturing state")
MMHLogger.smartLog("Video isReady in capturing state Mode:>>>>>>\(String(describing: assetWriterInput?.isReadyForMoreMediaData))")
if let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) {
if assetWriterInput?.isReadyForMoreMediaData == true {
let transformedPixelBuffer = applyMirrorEffectRemovalTransformation(pixelBuffer, originalOrientation: connection.videoOrientation)
let time = CMTime(seconds: timestamp - time, preferredTimescale: CMTimeScale(600))
adpater?.append(transformedPixelBuffer, withPresentationTime: time)
}
}
break
case .end:
MMHLogger.smartLog("Video is ended in end state....")
MMHLogger.smartLog("Video isReady in end state Mode:>>>>>>\(String(describing: assetWriterInput?.isReadyForMoreMediaData))")
guard assetWriterInput?.isReadyForMoreMediaData == true, assetWriter!.status != .failed else { break }
assetWriterInput?.markAsFinished()
assetWriter?.finishWriting { [weak self] in
self?.assetWriter = nil
self?.assetWriterInput = nil
MMHLogger.smartLog("Video is recorded")
NotificationCenter.default.post(name: Notification.Name("STOPRECORDING"), object: nil)
}
default:
break
}
}
func applyMirrorEffectRemovalTransformation(_ pixelBuffer: CVPixelBuffer, originalOrientation: AVCaptureVideoOrientation) -> CVPixelBuffer {
let mirroredPixelBuffer = createMirroredPixelBuffer(pixelBuffer)
return mirroredPixelBuffer
}
// Create a mirrored pixel buffer
func createMirroredPixelBuffer(_ pixelBuffer: CVPixelBuffer) -> CVPixelBuffer {
var mirroredPixelBuffer: CVPixelBuffer?
CVPixelBufferCreate(nil, CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer), CVPixelBufferGetPixelFormatType(pixelBuffer), nil, &mirroredPixelBuffer)
guard let mirroredPixelBuffer = mirroredPixelBuffer else {
return pixelBuffer
}
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
CVPixelBufferLockBaseAddress(mirroredPixelBuffer, [])
let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
let mirroredBaseAddress = CVPixelBufferGetBaseAddress(mirroredPixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
let mirroredBytesPerRow = CVPixelBufferGetBytesPerRow(mirroredPixelBuffer)
for y in 0..<CVPixelBufferGetHeight(pixelBuffer) {
memcpy(mirroredBaseAddress! + y * mirroredBytesPerRow, baseAddress! + (CVPixelBufferGetHeight(pixelBuffer) - y - 1) * bytesPerRow, bytesPerRow)
}
CVPixelBufferUnlockBaseAddress(mirroredPixelBuffer, [])
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
return mirroredPixelBuffer
}
// Update the getVideoTransform method to accept an orientation parameter
private func getVideoTransform(for orientation: AVCaptureVideoOrientation) -> CGAffineTransform {
switch orientation {
case .portraitUpsideDown:
return CGAffineTransform(rotationAngle: .pi)
case .landscapeLeft:
return CGAffineTransform(rotationAngle: .pi / 2)
case .landscapeRight:
return CGAffineTransform(rotationAngle: -.pi / 2)
default:
return CGAffineTransform.identity
}
}
func capture(state: CameraState) {
switch state {
case .idle:
cameraState = .idle
case .start:
cameraState = .start
case .capturing:
cameraState = .capturing
case .end:
cameraState = .end
}
}
}
编辑:
我还再次尝试更改视频方向并将其再次保存在另一个 DD 文件中,以使该视频始终处于纵向模式,但不幸的是失败了,在新文件中再次导出时显示“无法保存”错误消息。
func rotateAndSaveVideo(url: URL) {
let asset = AVAsset(url: url)
guard let rotatedFileURL = getWritableFileURL(),
FileManager.default.isWritableFile(atPath: rotatedFileURL.path) else {
return
}
// Create a video composition
let composition = AVMutableComposition()
// Add the video track
guard let videoTrack = composition.addMutableTrack(
withMediaType: .video,
preferredTrackID: kCMPersistentTrackID_Invalid
) else {
return
}
// Load the source video track
if let sourceVideoTrack = asset.tracks(withMediaType: .video).first {
try? videoTrack.insertTimeRange(
CMTimeRange(start: .zero, duration: asset.duration),
of: sourceVideoTrack,
at: .zero
)
// Apply rotation to the video track
let transform = sourceVideoTrack.preferredTransform.rotated(by: .pi)
videoTrack.preferredTransform = transform
// Export the rotated video
if let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) {
exporter.outputURL = rotatedFileURL
exporter.outputFileType = .mp4
exporter.exportAsynchronously(completionHandler: {
switch exporter.status {
case .completed:
print("Succeed")
case .failed:
if let error = exporter.error {
print("Export failed with error: \(error.localizedDescription)")
}
default:
break
}
})
}
}
}
func getWritableFileURL() -> URL? {
// Get the documents directory URL
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return nil
}
// Append a unique filename to the documents directory URL
let fileName = "rotatedVideo.mp4" // You can choose any desired filename
let rotatedFileURL = documentsDirectory.appendingPathComponent(fileName)
// Check if the file is writable or create the necessary directories
do {
if FileManager.default.isWritableFile(atPath: rotatedFileURL.path) {
return rotatedFileURL
} else {
try FileManager.default.createDirectory(
at: rotatedFileURL,
withIntermediateDirectories: true,
attributes: nil
)
return rotatedFileURL
}
} catch {
print("Error creating directories: \(error.localizedDescription)")
return nil
}
}
将相机方向旋转 180 度后解决了此问题 -
extension VideoDataManager {
func rotateAndSaveVideo(url: URL) {
let asset = AVAsset(url: url)
guard let rotatedFileURL = getWritableFileURL() else { return }
print("Destination URL: \(rotatedFileURL)")
// Create a video composition
let composition = AVMutableComposition()
// Add the video track
guard let videoTrack = composition.addMutableTrack(
withMediaType: .video,
preferredTrackID: kCMPersistentTrackID_Invalid
) else { return }
// Load the source video track
if let sourceVideoTrack = asset.tracks(withMediaType: .video).first {
try? videoTrack.insertTimeRange(
CMTimeRange(start: .zero, duration: asset.duration),
of: sourceVideoTrack,
at: .zero
)
// Apply rotation to the video track
let transform = sourceVideoTrack.preferredTransform.rotated(by: .pi)
videoTrack.preferredTransform = transform
// Export the rotated video
if let exporter = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetHighestQuality) {
exporter.outputURL = rotatedFileURL
exporter.outputFileType = .mp4
exporter.exportAsynchronously(completionHandler: {
switch exporter.status {
case .completed:
print("Export succeeded")
case .failed:
if let error = exporter.error {
print("Export failed with error: \(error)")
} else {
print("Export failed with an unknown error")
}
default:
break
}
})
}
}
}
func getWritableFileURL() -> URL? {
// Get the documents directory URL
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
return nil
}
// Append a unique filename to the documents directory URL
let fileName = "rotatedVideo.mp4" // You can choose any desired filename
let rotatedFileURL = documentsDirectory.appendingPathComponent(fileName)
debugPrint(Utility.getExcerciseRecordingRotatedFilePath() ?? "mmm")
debugPrint(rotatedFileURL)
// Remove the file if it already exists
do {
if FileManager.default.fileExists(atPath: rotatedFileURL.path) {
try FileManager.default.removeItem(at: rotatedFileURL)
}
} catch {
print("Error removing existing file: \(error.localizedDescription)")
}
// Check if the file is writable or create the necessary directories
do {
if FileManager.default.isWritableFile(atPath: rotatedFileURL.path) {
return rotatedFileURL
} else {
try FileManager.default.createDirectory(
at: documentsDirectory,
withIntermediateDirectories: true,
attributes: nil
)
return rotatedFileURL
}
} catch {
print("Error creating directories: \(error.localizedDescription)")
return nil
}
}
}
从 - 结束捕获部分调用此方法
guard let url = Utility.getExcerciseRecordingFilePath() else { return }
self?.rotateAndSaveVideo(url: url)
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
NotificationCenter.default.post(name: Notification.Name("STOPRECORDING"), object: nil)
}