Skip to content

Commit

Permalink
Specify output dimensions.
Browse files Browse the repository at this point in the history
  • Loading branch information
sturmen committed Jan 21, 2024
1 parent 6fa80db commit 1686374
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 45 deletions.
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -4,11 +4,11 @@

## Purpose

As of January 2024, Apple's MV-HEVC format for stereoscopic video is very new and barely supported by anything. However, there are millions of iPhones (iPhone 15 Pro/Pro Max) that can capture spatial video already. There was no FOSS tool capable of splitting the stereo pair available, especially not in formats suited for post-production.
As of January 2024, Apple's MV-HEVC format for stereoscopic video is very new and barely supported by anything. However, there are millions of iPhones (iPhone 15 Pro/Pro Max) that can capture spatial video already. There was no available FOSS tool capable of splitting the stereo pair, especially not in formats suited for post-production.

## Features

There is only one feature: it takes an MV-HEVC file and outputs the left and right eyes as separate files in the current directory. The output format is ProRes 422 HQ, video only. The user is expected to be familiar with tools such as ffmpeg for all other needs.
There is only one feature: it takes an MV-HEVC file and outputs the left and right eyes as separate files in the current directory. The output format is ProRes 422 HQ, video only. The user is expected to be familiar with tools such as ffmpeg for all other needs, including remuxing the audio back in.

## Requirements

Expand All @@ -25,7 +25,7 @@ In the future I may also try to figure out how to get this added to Homebrew.

## Usage

In Terminal: `mvhevcsplit MOV_0001.MOV`
In Terminal: `mvhevcsplit 1920 1080 MOV_0001.MOV`

`output_left.mov` and `output_right.mov`, if they already exist, **will be deleted** and then the new contents will be output to the current directory.

Expand Down
99 changes: 62 additions & 37 deletions mvhevcsplit/Converter.swift
Expand Up @@ -10,18 +10,55 @@ import AVFoundation
import VideoToolbox

class Converter {
let outputHeight, outputWidth: Int

let assetWriterInput = AVAssetWriterInput(
mediaType: .video,
outputSettings: [
AVVideoWidthKey: 1920,
AVVideoHeightKey: 1080,
AVVideoCodecKey: AVVideoCodecType.proRes422HQ,
]
)
init(height: Int, width: Int) {
self.outputHeight = height
self.outputWidth = width
}

let decoderOutputSettings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String : kCVPixelFormatType_422YpCbCr16,
AVVideoDecompressionPropertiesKey: [
kVTDecompressionPropertyKey_RequestedMVHEVCVideoLayerIDs: [0, 1] as CFArray,
],
]

let semaphore = DispatchSemaphore(value: 0)

var completedFrames = 0

func incrementFrameCount() {
completedFrames += 1
}

func getCurrentFrameCount() -> Int {
return completedFrames
}

func initWriter(outputUrl: URL) -> (assetWriter: AVAssetWriter, adaptor: AVAssetWriterInputPixelBufferAdaptor) {
let assetWriter = try! AVAssetWriter(
outputURL: outputUrl,
fileType: .mov
)

let assetWriterInput = AVAssetWriterInput(
mediaType: .video,
outputSettings: [
AVVideoWidthKey: outputWidth,
AVVideoHeightKey: outputHeight,
AVVideoCodecKey: AVVideoCodecType.proRes422HQ,
]
)

let adaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: assetWriterInput)

assetWriter.add(assetWriterInput)
assetWriter.startWriting()
assetWriter.startSession(atSourceTime: .zero)
return (assetWriter, adaptor)
}

func transcodeMovie(filePath: String, firstEye: Bool) {
do {

Expand All @@ -41,33 +78,28 @@ class Converter {

let sourceMovieUrl = URL(fileURLWithPath: filePath)
let sourceMovieAsset = AVAsset(url: sourceMovieUrl)
let outputSettings: [String: Any] = [
kCVPixelBufferPixelFormatTypeKey as String : kCVPixelFormatType_422YpCbCr16,
AVVideoDecompressionPropertiesKey: [
kVTDecompressionPropertyKey_RequestedMVHEVCVideoLayerIDs: [0, 1] as CFArray,
],
]

let loadingSemaphore = DispatchSemaphore(value: 0)
var tracks: [AVAssetTrack]?
sourceMovieAsset.loadTracks(withMediaType: .video, completionHandler: { (foundtracks, error) in
tracks = foundtracks
loadingSemaphore.signal()
self.semaphore.signal()
})
let loadingTimeoutResult = loadingSemaphore.wait(timeout: .now() + 60*60*24)

let loadingTimeoutResult = self.semaphore.wait(timeout: .now() + 60*60*24)
switch loadingTimeoutResult {
case .success:
print("loaded video track")
case .timedOut:
print("loading video track exceeded hardcoded limit of 24 hours")
}

guard let tracks = tracks else {
print("could not load any tracks!")
return
}
let assetReaderTrackOutput = AVAssetReaderTrackOutput(
track: tracks.first!,
outputSettings: outputSettings
outputSettings: decoderOutputSettings
)
assetReaderTrackOutput.alwaysCopiesSampleData = false
let assetReader = try AVAssetReader(asset: sourceMovieAsset)
Expand All @@ -77,19 +109,12 @@ class Converter {
print("could not start reading")
return
}
let assetWriter = try! AVAssetWriter(
outputURL: outputUrl,
fileType: .mov
)
let adaptor = AVAssetWriterInputPixelBufferAdaptor(assetWriterInput: assetWriterInput)

assetWriter.add(assetWriterInput)
assetWriter.startWriting()
assetWriter.startSession(atSourceTime: .zero)

let serialQueue = DispatchQueue(label: "writer")

let (assetWriter, adaptor) = initWriter(outputUrl: outputUrl)

print("about to start writing")
let writingSemaphore = DispatchSemaphore(value: 0)
adaptor.assetWriterInput.requestMediaDataWhenReady(on: serialQueue) { [weak self] in
print("output stream ready to be written to")
guard let self = self else {
Expand All @@ -105,12 +130,12 @@ class Converter {
} else {
print("advancing due to null sample, reader status is \(assetReader.status)")
}
self.assetWriterInput.markAsFinished()
writingSemaphore.signal()
adaptor.assetWriterInput.markAsFinished()
self.semaphore.signal()
continue
}
presentationTs = nextSampleBuffer.presentationTimeStamp

guard let taggedBuffers = nextSampleBuffer.taggedBuffers else { return }
let eyeBuffer = if (firstEye) {
taggedBuffers.first(where: {
Expand All @@ -127,15 +152,15 @@ class Converter {
case let .pixelBuffer(eyePixelBuffer) = eyeBuffer {
adaptor.append(eyePixelBuffer, withPresentationTime: presentationTs)
while(!adaptor.assetWriterInput.isReadyForMoreMediaData) {
print("waiting for asset writer to be ready again...")
// waiting...
}
completedFrames += 1
print("encoded \(completedFrames) frames for \(outputFilename)")
incrementFrameCount()
print("encoded \(getCurrentFrameCount()) frames for \(outputFilename)")
}
}
}
}
let encodingTimeoutResult = writingSemaphore.wait(timeout: .now() + 60*60*24)
let encodingTimeoutResult = semaphore.wait(timeout: .now() + 60*60*24)
switch encodingTimeoutResult {
case .success:
print("encoding completed for \(outputFilename), flushing to disk... ")
Expand All @@ -144,9 +169,9 @@ class Converter {
}

assetWriter.finishWriting() {
writingSemaphore.signal()
self.semaphore.signal()
}
let writingTimeoutResult = writingSemaphore.wait(timeout: .now() + 60*60*24)
let writingTimeoutResult = self.semaphore.wait(timeout: .now() + 60*60*24)
switch writingTimeoutResult {
case .success:
print("writing \(outputFilename) to disk completed")
Expand Down
18 changes: 13 additions & 5 deletions mvhevcsplit/main.swift
Expand Up @@ -33,19 +33,27 @@ func main() {

let arguments = CommandLine.arguments

guard arguments.count > 1 else {
print("Usage: \(arguments[0]) <MV-HEVC file>")
guard arguments.count == 4 else {
print("Usage: \(arguments[0]) <output width> <output height> <MV-HEVC file>")
return
}

VTRegisterProfessionalVideoWorkflowVideoDecoders()

let path = arguments[1]
guard let width = Int(arguments[1]) else {
print("parameter 1 is not a valid integer")
return
}
guard let height = Int(arguments[2]) else {
print("parameter 2 is not a valid integer")
return
}
let path = arguments[3]
printFileSize(filePath: path)
print("starting first eye")
Converter().transcodeMovie(filePath: path, firstEye: true)
Converter(height: height, width: width).transcodeMovie(filePath: path, firstEye: true)
print("starting second eye")
Converter().transcodeMovie(filePath: path, firstEye: false)
Converter(height: height, width: width).transcodeMovie(filePath: path, firstEye: false)
}

main()

0 comments on commit 1686374

Please sign in to comment.