Last active
October 12, 2023 06:42
-
-
Save mbotsu/691813c2195bd2f7e47627417fb4d462 to your computer and use it in GitHub Desktop.
copyNextSampleBuffer resume example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import SwiftUI | |
import AVKit | |
// copyNextSampleBuffer | |
// https://developer.apple.com/documentation/avfoundation/avassetreaderoutput/1385732-copynextsamplebuffer | |
struct ContentView: View { | |
@State var progress = 0 | |
@State var videoFrameNo = 0.0 | |
@State var videoLength = 0.0 | |
@State var uiImage: UIImage? | |
@State var message = "Status Message" | |
let url = Bundle.main.url(forResource: "IMG_3678", withExtension: "MOV")! | |
var body: some View { | |
VStack { | |
if let image = uiImage { | |
Image(uiImage: image).resizable() | |
.aspectRatio(contentMode: .fit) | |
Text(message) | |
Text("処理中 ... \(progress)%") | |
.font(.system(size: 12).monospaced()) | |
ProgressView(value: videoFrameNo, | |
total: videoLength) | |
.accentColor(Color.green) | |
.scaleEffect(x: 0.8, anchor: .center) | |
} | |
} | |
.padding() | |
.task { | |
await runMovieFile(movieFile: url) | |
} | |
} | |
func runMovieFile(movieFile: URL) async { | |
if videoFrameNo > 0 { | |
return | |
} | |
let asset = AVAsset(url: movieFile) | |
guard let videoTrack = try! await asset.loadTracks(withMediaType: .video).first else { return } | |
let player = AVPlayer() | |
let playerItem = AVPlayerItem(asset: asset) | |
player.replaceCurrentItem(with: playerItem) | |
let transform = try! await videoTrack.load(.preferredTransform) | |
let orientation = getPreferredOrientation(transform) | |
let count = countOfVideoFrame(asset: asset, videoTrack: videoTrack) | |
videoLength = Double(count) | |
let frame = videoResume(asset: asset, videoTrack: videoTrack, orientation: orientation, offset: 0) | |
if frame == count { | |
message += "\nComplete" | |
} else { | |
message += "\nError \(frame) / \(count)" | |
} | |
} | |
func videoResume(asset: AVAsset, videoTrack: AVAssetTrack, orientation: CGImagePropertyOrientation, offset: Int) -> Int { | |
let reader = createReader(asset: asset, videoTrack: videoTrack) | |
reader.startReading() | |
guard let trackOutput = reader.outputs.first else { return offset } | |
var frame = 1 | |
while let sampleBuffer = trackOutput.copyNextSampleBuffer() { | |
if frame < offset { | |
frame += 1 | |
continue | |
} | |
autoreleasepool { | |
if let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { | |
uiImage = UIImage(pixelBuffer: imageBuffer, orientation: orientation) | |
} | |
} | |
frame += 1 | |
videoFrameNo = Double(frame) | |
progress = Int(videoFrameNo / videoLength * 100.0) | |
} | |
if reader.status == .failed { | |
if frame < offset { | |
frame = offset | |
} | |
message += "\nretry frameNo: \(frame)" | |
return videoResume(asset: asset, videoTrack: videoTrack, orientation: orientation, offset: frame) | |
} | |
return frame | |
} | |
} | |
let videoColorProperties = [ | |
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, | |
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, | |
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2] | |
let outputVideoSettings = [ | |
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, | |
AVVideoColorPropertiesKey: videoColorProperties, | |
kCVPixelBufferMetalCompatibilityKey as String: true | |
] as [String: Any] | |
func createReader(asset: AVAsset, | |
videoTrack: AVAssetTrack) -> AVAssetReader { | |
let trackOutput = AVAssetReaderTrackOutput( | |
track: videoTrack, | |
outputSettings: outputVideoSettings) | |
let reader = try! AVAssetReader(asset: asset) | |
reader.add(trackOutput) | |
return reader | |
} | |
func countOfVideoFrame(asset: AVAsset, videoTrack: AVAssetTrack) -> Int { | |
let reader = createReader(asset: asset, videoTrack: videoTrack) | |
let trackOutput = reader.outputs.first! | |
reader.startReading() | |
var count = 1 | |
while let _ = trackOutput.copyNextSampleBuffer() { | |
count += 1 | |
} | |
return count | |
} | |
func getPreferredOrientation(_ transform: CGAffineTransform) -> CGImagePropertyOrientation { | |
let angle = Int(atan2(transform.b, transform.a) * (180 / Double.pi)) | |
switch angle { | |
case 180: | |
return .down | |
case 90: | |
return .right | |
case 270: | |
return .left | |
default: | |
return .up | |
} | |
} | |
extension CIImage { | |
func toCGImage(orientation: CGImagePropertyOrientation) -> CGImage? { | |
let orientedImage = self.oriented(orientation) | |
let context: CIContext = CIContext.init(options: nil) | |
guard let cgImage: CGImage = context.createCGImage(orientedImage, from: orientedImage.extent) else { return nil } | |
return cgImage | |
} | |
} | |
extension UIImage { | |
convenience init?(pixelBuffer: CVPixelBuffer, orientation: CGImagePropertyOrientation) { | |
let ciImage = CIImage(cvPixelBuffer: pixelBuffer) | |
guard let cgImage = ciImage.toCGImage(orientation: orientation) else { | |
return nil | |
} | |
self.init(cgImage: cgImage) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Video evaluating code behavior.
A retry occurs every time the app goes to the background.
You can check the increase in retry logs at the bottom of the screen.
sample.mov