Skip to content

Instantly share code, notes, and snippets.

@mbotsu
Last active October 12, 2023 06:42
Show Gist options
  • Save mbotsu/691813c2195bd2f7e47627417fb4d462 to your computer and use it in GitHub Desktop.
Save mbotsu/691813c2195bd2f7e47627417fb4d462 to your computer and use it in GitHub Desktop.
copyNextSampleBuffer resume example
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)
}
}
@mbotsu
Copy link
Author

mbotsu commented Oct 12, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment